隨筆:6 文章:1 評論:0 引用:0
          BlogJava 首頁 發新隨筆
          發新文章 聯系 聚合管理

          2012年10月26日

          (本文沿著思路來寫,不會一開始就給出最后結論。)

          很多項目需要對用戶操作進行鑒權。它們的需求可以歸納為下面幾點:
          1、基于角色的權限控制,一個用戶可能被授予多個角色;
          2、用戶對同一條記錄的不同操作(如查看和修改)需要分別授權;
          3、記錄之間可能存在父子關系,子記錄的權限自動從父記錄繼承,不需要明確授權。

          所以,可以看出這個權限模型存在四個基本要素:用戶、角色、操作、記錄。
          用戶:實際進行操作的單位。
          角色:用戶進行操作時的身份。
          操作:用戶執行的與記錄有關的動作。
          記錄:被操作的對象。

          因為鑒權只針對角色而非用戶,所以一個鑒權應該可以描述為:判斷某角色是否對某記錄擁有某操作的權限

          可以看出,每個權限都是一個“角色—操作—記錄”的三要素關聯關系。如果我們要設計一張表保存所有授權,那么這張表至少應該包含這三個字段。

          那么問題來了:隨著記錄的增加,這張表的記錄數將呈級數增長,特別是記錄之間存在父子關系的情況下,為父記錄授權就意味著同樣要為所有子記錄授權,刪除一條授權也會造成大量的查詢和更新,這張表的維護將成為噩夢,這樣的表設計沒有實用性。

          造成這種情況的根本原因是,記錄是經常變化的,而鑒權規則很少改變,二者之間存在脫節。

          在這里我們不得不重新思考授權的本質:授權本來就不是針對某條具體的記錄的。以博客系統為例,“作者有權刪除其創建的文章”這個規則中,角色是“作者”,操作是“刪除”,而記錄呢?“作者創建的文章”并不是一條具體的記錄。所以說,授權是對規則的描述,它針對的不是具體的記錄,而是更抽象的東西。

          這種更抽象的東西,我們暫把它叫做“資源”。那么鑒權的三要素,應該稱作“角色—操作—資源”。資源是對記錄的抽象,就如角色是對用戶的抽象一樣。這樣,一條權限就變成了完全抽象的:它既不針對具體的某個用戶,也不針對具體的某條記錄,它完全是對規則的描述。當一個用戶對一條記錄的操作需要鑒權時,需要進行映射,將用戶映射到角色,將記錄映射到資源,然后再搜索是否尋在允許的授權。

          因為“用戶—角色”是多對多的關系,“記錄—資源”也是多對多的關系(比如一篇博客文章可能是“我的文章”,也可能是“別人的文章”),所以“用戶—操作—記錄”先要被映射到“角色[]—操作—資源[]”([]表示有多個),然后再從匹配關系的組合中搜索是否存在允許的授權。如果存在則表示鑒權通過,否則鑒權不通過。

          至此為止,權限表設計已經從“角色—操作—記錄”改為“角色—操作—資源”,它符合“授權的本質”,所以不會有大量的級聯查詢和更新。但它仍然存在兩個問題:1)因為存在角色與資源的多對多組合,所以每次鑒權需要進行大量的判斷。2)我們沒有辦法實現從記錄到資源的映射,因為兩者并沒有直接關聯。為什么這么說呢?以博客系統為例,一篇文章在作者面前可以被映射為“我的文章”,在管理員面前可以被映射為“普通文章”,也許還會存在其他的映射,這是無法確定的。我們需要仔細考察這里面的關系。

          經過仔細考察,我發現這兩個問題其實是有關聯的。我們需要重新審視資源的概念:資源不是獨立存在的,而是與角色關聯的,不同的角色需要面對不同的資源。比如博客系統中,管理員面對用戶、角色等資源,而普通用戶則面對文章、博客等資源。所以記錄到資源的映射與角色有著密切關系,比如一篇文章在博客作者面前要么是“我的文章”,要么是“別人的文章”,而在管理員面前要么是“普通文章”,要么是“其他文章”。這是很合理的:一條記錄在不同的角色面前以不同的角度呈現。

          所以通過角色,我們可以將“用戶—操作—記錄”映射到“角色—操作—資源”的步驟改為:首先完成用戶到角色的映射,然后對每種角色,列出記錄到資源的映射,然后搜索是否存在允許的授權。這樣能夠明顯減少判斷的次數。

          我們還要考慮記錄的繼承關系。這個邏輯不復雜:當一條記錄搜索不到授權時,還要獲取其父記錄并搜索父記錄的授權。直到所有的父記錄都找不到授權,才能返回授權不通過。

          然后我們要考慮同步:對同一個“角色(不是用戶,因為這樣同步范圍更大)—操作—記錄(不是資源,因為這樣搜索更精準)”的鑒權需要進行同步鎖,這樣可以避免重復搜索浪費資源。

          最后我們要考慮如何緩存。通常為了提高鑒權效率,所有已經判斷的授權都要緩存起來可以避免重復搜索。緩存是對記錄(而非資源)授權的緩存,即緩存“用戶/角色—操作—記錄”,這樣可以避免重復搜索父記錄授權。

          當授權變更時,緩存如何維護?這個問題也要考慮。當添加一條授權時,我們不需要關心,因為經過一段時間的搜索,相應的緩存記錄就會自動補充起來;當一條“角色—操作—資源”授權被刪除時,需要刪除:1)所有對應的“角色—操作—記錄”緩存;2)找到角色對應的用戶,刪除所有對應的“用戶—操作—記錄”緩存。這個過程效率當然可能不高,但考慮到刪除授權本身不會很頻繁,所以應該能夠接受。

          至此為止,表設計和鑒權的邏輯過程我們都清晰了,然后是如何實現。鑒權過程的實現關鍵在于映射,特別是從記錄到資源的映射。這個映射是不可能光靠數據庫配置來完成的,必須要有代碼邏輯,比如博客系統的文章映射到“我的文章”,就需要判斷該用戶是不是文章的作者。這樣的邏輯我們可以抽象為一個接口:

          /**
           * 將記錄映射到資源的接口。不同類型的記錄應該有不同的實現類
           
          */
          public interface ResourceMapper {

              /**
               * 根據角色將記錄映射到資源
               * 
          @param recordId 記錄ID
               * 
          @param roleId   角色ID
               * 
          @return 資源ID
               
          */
              Long getResourceId(Long recordId, Long roleId);

              /**
               * 搜索父記錄,如果不存在則返回 null
               * 
          @param recordId  記錄ID
               * 
          @return 父記錄ID,如果不存在則返回 null
               
          */
              Long getParent(Long recordId);
          }

          至于其他的部分,本文就不贅述了。我要趕緊去實現一個看看。

          最后總結一下這個鑒權系統的主要部分:
          1、數據庫設計:角色表(ID,名稱),鑒權表(角色,操作,資源),資源表(ID,名稱,角色【如果為空則表示對所有角色可見】)
          2、鑒權的核心邏輯:映射 + 搜索
          3、鑒權的外圍邏輯:緩存
          4、需要用戶實現的邏輯:映射接口 ResourceMapper
          posted @ 2012-11-27 12:21 捏造的信仰 閱讀(2712) | 評論 (0)編輯 收藏
           
          要想使 DefaultHttpClient 對象使用自定義的 DNS 解析(比如將 blogjava.net 關聯到 127.0.0.1,使其訪問 "http://blogjava.net" 時請求本地服務器),可以用下面的辦法(我在官網上沒找到相關文章,是看了源代碼自己琢磨出來的,也不是道是不是標準做法)

          import org.apache.http.HttpResponse;
          import org.apache.http.client.methods.HttpGet;
          import org.apache.http.conn.ClientConnectionOperator;
          import org.apache.http.conn.DnsResolver;
          import org.apache.http.conn.scheme.SchemeRegistry;
          import org.apache.http.impl.client.DefaultHttpClient;
          import org.apache.http.impl.conn.BasicClientConnectionManager;
          import org.apache.http.impl.conn.DefaultClientConnectionOperator;

          import java.net.InetAddress;
          import java.net.UnknownHostException;
          import java.util.HashMap;
          import java.util.Map;

          /**
           * 在 httpclient 4.2 中使用自定義的 DNS 解析
           
          */
          public class CustomDnsResolverDemo {

              public static void main(String[] args) throws Exception {

                  // 創建自定義的 ConnectionManager
                  BasicClientConnectionManager connectionManager = new BasicClientConnectionManager() {

                      @Override
                      protected ClientConnectionOperator createConnectionOperator(SchemeRegistry schreg) {
                          return new DefaultClientConnectionOperator(schreg, new MyDnsResolver());
                      }
                  };

                  // 創建 HttpClient 對象
                  DefaultHttpClient httpclient = new DefaultHttpClient(connectionManager);

                  // 構造請求
                  HttpGet httpget = new HttpGet("https://www.google.com/");
                  System.out.println(httpget.getRequestLine());

                  // 發送請求并返回結果
                  HttpResponse response = httpclient.execute(httpget);
                  System.out.println(response.getEntity().getContentType());
                  System.out.println(response.getStatusLine());

                  // 這句不是必須的,只是讓程序結束更快點
                  httpclient.getConnectionManager().shutdown();
              }

              // 自定義的 DNS 解析類
              private static class MyDnsResolver implements DnsResolver {

                  private static final Map<String, InetAddress[]> MAPPINGS = new HashMap<String, InetAddress[]>();

                  static {
                      addResolve("www.google.com", "74.125.134.138");
                  }

                  private static void addResolve(String host, String ip) {
                      try {
                          MAPPINGS.put(host, new InetAddress[]{InetAddress.getByName(ip)});
                      } catch (UnknownHostException e) {
                          e.printStackTrace();
                      }
                  }

                  @Override
                  public InetAddress[] resolve(String host) throws UnknownHostException {
                      return MAPPINGS.containsKey(host) ? MAPPINGS.get(host) : new InetAddress[0];
                  }
              }
          }
          posted @ 2012-11-10 19:18 捏造的信仰 閱讀(3621) | 評論 (0)編輯 收藏
           
               摘要: 下面是一個例子,演示如何執行一個進程(類似于在命令行下鍵入命令),讀取進程執行的輸出,并根據進程的返回值判斷是否執行成功。一般來說,進程返回 0 表示執行成功,其他值表示失敗。  閱讀全文
          posted @ 2012-10-26 20:14 捏造的信仰 閱讀(5731) | 評論 (0)編輯 收藏
          CALENDER
          <2012年10月>
          30123456
          78910111213
          14151617181920
          21222324252627
          28293031123
          45678910

          常用鏈接

          留言簿

          隨筆檔案

          文章檔案

          搜索

          •  

          最新評論

          閱讀排行榜

          評論排行榜


          Powered By: 博客園
          模板提供滬江博客

          主站蜘蛛池模板: 富民县| 梓潼县| 神木县| 峡江县| 洛宁县| 龙井市| 乌鲁木齐县| 清水河县| 噶尔县| 休宁县| 嘉鱼县| 四子王旗| 牟定县| 那曲县| 邻水| 丰宁| 台南县| 博湖县| 台北市| 鄂温| 娱乐| 晋宁县| 漳浦县| 大石桥市| 北宁市| 兴安县| 全南县| 宁武县| 墨脱县| 丽水市| 蒙城县| 洛扎县| 方山县| 阆中市| 石林| 克山县| 鹿邑县| 南溪县| 葵青区| 博客| 富川|