JavaWeb許可權設計原理

weixin_33895657發表於2018-10-16

每個系統都有許可權設計,本篇主要將初始的許可權設計的原理,不依賴任何框架,以直觀的角度剖析web的許可權設計。

許可權設計的原理知識

什麼是許可權管理

只要有使用者參與的系統一般都有許可權管理,許可權管理實現對使用者訪問系統的控制。按照安全規則或者安全策略控制使用者可以訪問而且只能訪問自己被授權的資源。
許可權管理包括使用者認證使用者授權兩部分。

使用者認證

概念

使用者認證-----使用者訪問系統,系統需要驗證使用者身份的合法性。常用的驗證方法:1.使用者名稱密碼驗證。2.指紋驗證。3.證照驗證。系統驗證使用者身份合法,使用者才可以訪問資源。

使用者認證的流程
11345146-347213089069d957.png
關鍵物件

subject :主體,理解為使用者,可能是程式,都要去訪問系統的資源,系統需要對subject進行身份驗證。
principal :身份資訊,通常是唯一的,一個主題可以有多個身份資訊,但是隻能有一個主身份資訊(primary principal)。
credential :憑證資訊,可以是密碼,證照,指紋等。
主體在進行身份認證時需要提供身份資訊和憑證資訊

使用者授權

概念

使用者授權,簡單理解為訪問控制,在使用者認證通過後,系統對使用者訪問資源進行控制,當使用者具有資源的訪問許可權方可訪問。

授權流程
11345146-f99d604a612dd7c8.png
關鍵物件

授權的過程可以理解為 who 對 what(which)進行how操作
who : 主體,即subject,subject在認證通過後,系統可以進行訪問控制。
what(which): 資源(Resource),subject必須具備資源訪問許可權才可以訪問改許可權。資源包括很多方面,比如:使用者列表頁面,商品修改選單等。資源分為資源型別和資源例項:
例如系統的使用者資訊就是資源型別,相當於java類。
系統中id為1的使用者就是資源例項,相當於java物件。
how : 許可權(permission),針對資源的許可權或許可,subject必須具有permission方可訪問資源,如何訪問/操作需要定義permission,許可權比如:使用者新增,修改,刪除等。

許可權模型

主體(賬號、密碼)
資源(資源名稱、訪問地址)
許可權(許可權名稱、資源id)
角色(角色名稱)
角色和許可權的關係,使用者和角色的關係。
如下圖:


11345146-991d97ea43ee9731.png

通常企業開發中將資源和許可權合併成一張許可權表,如下:
資源(資源名稱,訪問地址)
許可權(許可權名稱,資源id)
合併為:
許可權(許可權名稱,資源名稱,資源訪問地址)


11345146-8265dd3e3f1ffcfb.png

上圖是許可權管理的通用模型,當然在實際開發中也可以根據自己的需要修改。

分配許可權

使用者需要分配相應的許可權才可以訪問相應的資源。許可權是對資源的操作許可。
通常給使用者分配資源許可權需要將許可權資訊持久化,比如儲存到關聯式資料庫中。
把使用者資訊,許可權管理,角色資訊寫入資料庫中。

基於角色的訪問控制

RBAC(role based access control),基於角色的訪問控制。
比如:
系統角色包括 :部門經理、總經理。。(角色針對使用者來劃分)
系統程式碼中實現:
//如果該user是部門經理則可以訪問if中的程式碼
if(user.hasRole('部門經理')){
//系統資源內容
//使用者報表檢視
}
問題:
角色針對人劃分的,人作為使用者在系統中屬於活動內容,如果該 角色可以訪問的資源出現變更,需要修改你的程式碼了,比如:需要變更為部門經理和總經理都可以進行使用者報表檢視,程式碼改為:
if(user.hasRole('部門經理') || user.hasRole('總經理') ){
//系統資源內容
//使用者報表檢視
}
基於角色的訪問控制是不利於系統維護(可擴充套件性不強)。

基於資源的訪問控制

RBAC(Resource based access control),基於資源的訪問控制。
資源在系統中是不變的,比如資源有:類中的方法,頁面中的按鈕等。
對資源的訪問需要具有permission許可權,程式碼可以寫成:
if(user.hasPermission ('使用者報表檢視(許可權識別符號)')){
//系統資源內容
//使用者報表檢視
}
上邊的方法就可以解決使用者角色變更不用修改上邊許可權控制的程式碼。
如果需要變更許可權只需要在分配許可權模組去操作,給部門經理或總經理增或刪除許可權。
建議使用基於資源的訪問控制實現許可權管理。

許可權管理解決方案

粗粒度和細粒度許可權

粗粒度許可權管理,對資源型別的許可權管理。資源型別比如:選單,url連線,使用者新增頁面,使用者資訊,類方法,頁面按鈕。
粗粒度許可權管理比如:超級管理員可以訪問使用者新增頁面,使用者資訊等全部頁面。
部門管理員可以訪問使用者資訊頁面,包括頁面中的按鈕。

細粒度許可權管理,對資源例項的許可權管理。資源例項就是資源型別的具體化,比如:行政部門的員工,id為1的使用者的檢視頁面等。
細粒度許可權管理就是資料級別的許可權管理

細粒度許可權管理比如:部門經理只可以訪問本部門的員工資訊,使用者只可以看到自己的選單,大區經理只能檢視本轄區的銷售訂單。。

粗粒度和細粒度例子:
系統有一個使用者列表查詢頁面,對使用者列表查詢分許可權,如果粗顆粒管理,張三和李四都有使用者列表查詢的許可權,張三和李四都可以訪問使用者列表查詢。
進一步進行細顆粒管理,張三(行政部)和李四(開發部)只可以查詢自己本部門的使用者資訊。張三隻能檢視行政部 的使用者資訊,李四隻能檢視開發部門的使用者資訊。

如何實現粗粒度和細粒度許可權管理

如何實現粗粒度許可權管理?
粗粒度許可權管理比較容易將許可權管理的程式碼抽取出來在系統架構級別統一處理。比如:通過springmvc的攔截器實現授權。

如何實現細粒度許可權管理?
對細粒度許可權管理在資料級別是沒有共性可言,針對細粒度許可權管理就是系統業務邏輯的一部分,如果在業務層去處理相對比較簡單,如果將細粒度許可權管理統一在系統架構級別去抽取,比較困難,即使抽取的功能可能也存在擴充套件不強。
建議細粒度許可權管理在業務層去控制。
比如:部門經理只查詢本部門員工資訊,在service介面提供一個部門id的引數,controller中根據當前使用者的資訊得到該 使用者屬於哪個部門,呼叫service時將部門id傳入service,實現該使用者只查詢本部門的員工。

基於url攔截的方式實現

基於url攔截的方式實現在實際開發中比較常用的一種方式。
對於web系統,通過filter過慮器實現url攔截,也可以springmvc的攔截器實現基於url的攔截。

基於許可權框架的方式實現

對於粗粒度許可權管理,建議使用優秀許可權管理框架來實現,節省開發成功,提高開發效率。
shiro就是一個優秀許可權管理框架。

基於url的許可權管理

基於url的許可權管理流程

11345146-e995fdfb4a87b099.png

搭建環境

資料庫

mysql資料庫中建立表:使用者表、角色表、許可權表(實質上是許可權和資源的結合 )、使用者角色表、角色許可權表。


11345146-923ae686d360843c.png

建立好的表如下:


11345146-07b56872dcb235f5.png

shiro_sql_table.sql:

CREATE TABLE `sys_permission` (
  `id` bigint(20) NOT NULL COMMENT '主鍵',
  `name` varchar(128) NOT NULL COMMENT '資源名稱',
  `type` varchar(32) NOT NULL COMMENT '資源型別:menu,button,',
  `url` varchar(128) DEFAULT NULL COMMENT '訪問url地址',
  `percode` varchar(128) DEFAULT NULL COMMENT '許可權程式碼字串',
  `parentid` bigint(20) DEFAULT NULL COMMENT '父結點id',
  `parentids` varchar(128) DEFAULT NULL COMMENT '父結點id列表串',
  `sortstring` varchar(128) DEFAULT NULL COMMENT '排序號',
  `available` char(1) DEFAULT NULL COMMENT '是否可用,1:可用,0不可用',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
 
/*Table structure for table `sys_role` */
 
CREATE TABLE `sys_role` (
  `id` varchar(36) NOT NULL,
  `name` varchar(128) NOT NULL,
  `available` char(1) DEFAULT NULL COMMENT '是否可用,1:可用,0不可用',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
 
/*Table structure for table `sys_role_permission` */
 
CREATE TABLE `sys_role_permission` (
  `id` varchar(36) NOT NULL,
  `sys_role_id` varchar(32) NOT NULL COMMENT '角色id',
  `sys_permission_id` varchar(32) NOT NULL COMMENT '許可權id',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
 
/*Table structure for table `sys_user` */
 
CREATE TABLE `sys_user` (
  `id` varchar(36) NOT NULL COMMENT '主鍵',
  `usercode` varchar(32) NOT NULL COMMENT '賬號',
  `username` varchar(64) NOT NULL COMMENT '姓名',
  `password` varchar(32) NOT NULL COMMENT '密碼',
  `salt` varchar(64) DEFAULT NULL COMMENT '鹽',
  `locked` char(1) DEFAULT NULL COMMENT '賬號是否鎖定,1:鎖定,0未鎖定',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
 
/*Table structure for table `sys_user_role` */
 
CREATE TABLE `sys_user_role` (
  `id` varchar(36) NOT NULL,
  `sys_user_id` varchar(32) NOT NULL,
  `sys_role_id` varchar(32) NOT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

shiro_sql_table_data.sql

insert  into `sys_permission`(`id`,`name`,`type`,`url`,`percode`,`parentid`,`parentids`,`sortstring`,`available`) values 
(1,'許可權','','',NULL,0,'0/','0','1'),(11,'商品管理','menu','/item/queryItem.action',NULL,1,'0/1/','1.','1'),
(12,'商品新增','permission','/item/add.action','item:create',11,'0/1/11/','','1'),
(13,'商品修改','permission','/item/editItem.action','item:update',11,'0/1/11/','','1'),
(14,'商品刪除','permission','','item:delete',11,'0/1/11/','','1'),
(15,'商品查詢','permission','/item/queryItem.action','item:query',11,'0/1/15/',NULL,'1'),
(21,'使用者管理','menu','/user/query.action','user:query',1,'0/1/','2.','1'),
(22,'使用者新增','permission','','user:create',21,'0/1/21/','','1'),
(23,'使用者修改','permission','','user:update',21,'0/1/21/','','1'),
(24,'使用者刪除','permission','','user:delete',21,'0/1/21/','','1');
 
/*Data for the table `sys_role` */
 
insert  into `sys_role`(`id`,`name`,`available`) values 
    ('ebc8a441-c6f9-11e4-b137-0adc305c3f28','商品管理員','1'),
    ('ebc9d647-c6f9-11e4-b137-0adc305c3f28','使用者管理員','1');
 
/*Data for the table `sys_role_permission` */
 
insert  into `sys_role_permission`(`id`,`sys_role_id`,`sys_permission_id`) values 
    ('ebc8a441-c6f9-11e4-b137-0adc305c3f21','ebc8a441-c6f9-11e4-b137-0adc305c','12'),
    ('ebc8a441-c6f9-11e4-b137-0adc305c3f22','ebc8a441-c6f9-11e4-b137-0adc305c','11'),
    ('ebc8a441-c6f9-11e4-b137-0adc305c3f24','ebc9d647-c6f9-11e4-b137-0adc305c','21'),
    ('ebc8a441-c6f9-11e4-b137-0adc305c3f25','ebc8a441-c6f9-11e4-b137-0adc305c','15'),
    ('ebc9d647-c6f9-11e4-b137-0adc305c3f23','ebc9d647-c6f9-11e4-b137-0adc305c','22'),
    ('ebc9d647-c6f9-11e4-b137-0adc305c3f26','ebc8a441-c6f9-11e4-b137-0adc305c','13');
 
/*Data for the table `sys_user` */
 
insert  into `sys_user`(`id`,`usercode`,`username`,`password`,`salt`,`locked`) values 
    ('lisi','lisi','李四','bf07fd8bbc73b6f70b8319f2ebb87483','uiwueylm','0'),
    ('zhangsan','zhangsan','張三','cb571f7bd7a6f73ab004a70322b963d5','eteokues','0');
 
/*Data for the table `sys_user_role` */
 
insert  into `sys_user_role`(`id`,`sys_user_id`,`sys_role_id`) values 
    ('ebc8a441-c6f9-11e4-b137-0adc305c3f28','zhangsan','ebc8a441-c6f9-11e4-b137-0adc305c'),
    ('ebc9d647-c6f9-11e4-b137-0adc305c3f28','lisi','ebc9d647-c6f9-11e4-b137-0adc305c');

整個工程如下:


11345146-4b03e267c6e6e846.png

系統登陸

系統登入相當於使用者身份認證,使用者登入成功,要在Session中記錄使用者的身份資訊。
操作流程:
使用者進入登入頁面。
輸入使用者名稱和密碼進行登陸。
進行使用者名稱和密碼校驗。
如果校驗通過,在Session中記錄使用者身份資訊。

使用者身份資訊

建立專門類用於記錄使用者身份資訊。

/**
 * 使用者身份資訊,存入Session  由於Tomcat正常關閉時會將Session序列化的本地硬碟上,所以實現Serializable介面
 * @author xushu
 *
 */
public class ActiveUser implements Serializable {
    private String userid; //使用者id(主鍵)
    private String usercode; // 使用者賬號
    private String username; // 使用者姓名
    ....
        ....
}
mapper

mapper介面:根據使用者賬號查詢使用者(sys_user)資訊 (使用逆向工程生成許可權相關的PO類和mapper介面)
如下所示:


11345146-29abb412144b9f35.png

11345146-dec09c77b3b0d3a7.png
service(進行使用者名稱和密碼校驗)

介面功能:根據使用者的身份和密碼進行認證,如果認證通過,返回使用者身份資訊。
認證過程:
根據使用者身份(賬號)查詢資料庫,如果查詢不到 則丟擲使用者不存在
對輸入的密碼和資料庫密碼進行比對,如果一致,認證通過。
新建許可權管理Service介面 新增身份認證方法

/**
 * 認證授權服務介面
 * @author liuxun
 *
 */
public interface SysService {
    //根據使用者的身份和密碼進行認證,如果認證通過,返回使用者身份資訊
    public ActiveUser authenticat(String usercode,String password) throws Exception;
    
    //根據使用者賬號查詢使用者資訊
    public SysUser findSysUserByUserCode(String userCode) throws Exception;
        ......
}

方法實現:

public class SysServiceImpl implements SysService {
    @Autowired
    private SysUserMapper sysUserMapper;
 
    public ActiveUser authenticat(String usercode, String password) throws Exception {
 
        /**
         * 認證過程: 根據使用者身份(賬號)查詢資料庫,如果查詢不到則使用者不存在 
         * 對輸入的密碼和資料庫密碼進行比對,如果一致則認證通過
         */
        // 根據使用者賬號查詢資料庫
        SysUser sysUser = this.findSysUserByUserCode(usercode);
 
        if (sysUser == null) {
            // 丟擲異常
            throw new CustomException("使用者賬號不存在");
        }
 
        // 資料庫密碼(MD5加密後的密碼)
        String password_db = sysUser.getPassword();
 
        // 對輸入的密碼和資料庫密碼進行比對,如果一致,認證通過
        // 對頁面輸入的密碼進行MD5加密
        String password_input_md5 = new MD5().getMD5ofStr(password);
        if (!password_db.equalsIgnoreCase(password_input_md5)) {
            //丟擲異常
            throw new CustomException("使用者名稱或密碼錯誤");
        }
        //得到使用者id
        String userid = sysUser.getId();
        
        //認證通過,返回使用者身份資訊
        ActiveUser activeUser = new ActiveUser();
        activeUser.setUserid(userid);
        activeUser.setUsercode(usercode);
        activeUser.setUsername(sysUser.getUsername());
 
        return activeUser;
    }
 
    public SysUser findSysUserByUserCode(String userCode) throws Exception {
        SysUserExample sysUserExample = new SysUserExample();
        SysUserExample.Criteria criteria = sysUserExample.createCriteria();
        criteria.andUsercodeEqualTo(userCode);
 
        List<SysUser> list = sysUserMapper.selectByExample(sysUserExample);
        if (list != null && list.size() > 0) {
            return list.get(0);
        }
 
        return null;
    }
   
       ......
}

配置Service,往類Service中使用@Autowire 需要註冊Service 註冊有兩種方法(註解或配置檔案),在架構時沒有配置掃描Service 需要在配置檔案中註冊Service

<!-- 認證和授權的Service -->
   <bean id="sysService" class="liuxun.ssm.service.impl.SysServiceImpl"></bean>
controller(記錄Session)
//使用者登入提交方法
    @RequestMapping("/login")
    public String login(HttpSession session,String randomcode,String usercode,String password) throws Exception{
        // 校驗驗證碼,防止惡性攻擊
        // 從Session中獲取正確的驗證碼
        String validateCode = (String) session.getAttribute("validateCode");
        
        //輸入的驗證碼和Session中的驗證碼進行對比
        if (!randomcode.equalsIgnoreCase(validateCode)) {
            //丟擲異常
            throw new CustomException("驗證碼輸入錯誤");
        }
        
        //呼叫Service校驗使用者賬號和密碼的正確性
        ActiveUser activeUser = sysService.authenticat(usercode, password);
        
        //如果Service校驗通過,將使用者身份記錄到Session
        session.setAttribute("activeUser", activeUser);
        //重定向到商品查詢頁面
        return "redirect:/first.action";
    }

使用者認證攔截器

anonymousURL.properties配置匿名URL

配置可以匿名訪問的URL。這個意思是,還沒有登陸就能訪問的連線,一般的後臺管理系統就是登陸註冊連結。

11345146-964d2bb335b08e3a.png

編寫身份認真攔截器
//用於使用者認證校驗、使用者許可權校驗
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        
        //得到請求的url
        String url = request.getRequestURI();
        
        //判斷是否是公開地址
        //實際開發中需要將公開地址配置在配置檔案中
        //從配置檔案中取出可以匿名訪問的URL
        List<String> open_urls = ResourcesUtil.getKeyList("anonymousURL");
        for (String open_url : open_urls) {
            if (url.indexOf(open_url)>=0) {
                //如果是公開地址 則放行
                return true;
            }
        }
        
        //判斷使用者身份在Session中是否存在
        HttpSession session = request.getSession();
        ActiveUser activeUser = (ActiveUser) session.getAttribute("activeUser");
        //如果使用者身份在session中存在則放行
        if (activeUser!=null) {
            return true;
        }
        //執行到這裡攔截,跳轉到登入頁面,使用者進行身份認證
        request.getRequestDispatcher("/WEB-INF/jsp/login.jsp").forward(request, response);
        
        //如果返回false表示攔截器不繼續執行handler,如果返回true表示放行
        return false;
    }

配置認證攔截器
<!-- 攔截器 -->
    <mvc:interceptors>
        <mvc:interceptor>
            <!-- 使用者認證攔截 -->
            <mvc:mapping path="/**"/>
            <bean class="xushu.ssm.controller.interceptor.LoginInterceptor"></bean>
        </mvc:interceptor>
    </mvc:interceptors>
使用者授權
commonURL.properties配置公用訪問地址

在此配置檔案中配置公用訪問地址,公用訪問地址只需要通過使用者認證,不需要對公用訪問地址分配許可權即可訪問。這個意思是,只要登陸進去了,不管什麼使用者都可以進行訪問的連結

11345146-5be18552faf08add.png

獲取使用者許可權範圍的選單

思路:
在使用者認證時,認證通過,根據使用者id從資料庫獲取使用者許可權範圍內的選單,將選單的集合儲存在Session中。
編輯儲存使用者身份資訊的類ActiveUser 如下所示:

public class ActiveUser implements Serializable {
    private String userid; //使用者id(主鍵)
    private String usercode; // 使用者賬號
    private String username; // 使用者姓名
    
    private List<SysPermission> menus; //選單
        //......setter和getter方法
}

自定義許可權Mapper
因為使用逆向工程生成的Mapper是不建議去修改的 因為它的程式碼聯絡非常緊密,一旦修改錯誤 就會牽一髮而動全身。所以需要自定義一個許可權的Mapper(SysPermissionMapperCustom)
在SysPermissionMapperCustom.xml中新增根據使用者id查詢使用者許可權的選單

<!-- 根據使用者id查詢選單 -->
<select id="findMenuListByUserId" parameterType="string" resultType="liuxun.ssm.po.SysPermission">
   SELECT 
      * 
    FROM
      sys_permission 
    WHERE TYPE = 'menu' 
      AND id IN 
      (SELECT 
        sys_permission_id 
      FROM
        sys_role_permission 
      WHERE sys_role_id IN 
        (SELECT 
          sys_role_id 
        FROM
          sys_user_role 
        WHERE sys_user_id = #{userid}))
</select>

在SysPermissionMapperCustom.java介面中新增對應的方法

public interface SysPermissionMapperCustom {
    //根據使用者id查詢選單
    public List<SysPermission> findMenuListByUserId(String userid) throws Exception;
  .......
}

在許可權Service介面中新增對應的方法 在實現中注入SysPermissionMapperCustom
SysServiceImpl.java中新增如下內容

@Override
    public List<SysPermission> findMenuListByUserId(String userid) throws Exception {
        return sysPermissionMapperCustom.findMenuListByUserId(userid);
    }
獲取使用者許可權範圍的URL

思路:
在使用者認證時,認證通過後,根據使用者id從資料庫中獲取使用者許可權範圍的URL,將URL的集合儲存在Session中。
修改ActiveUser 新增URL的許可權集合

public class ActiveUser implements Serializable {
    private String userid; //使用者id(主鍵)
    private String usercode; // 使用者賬號
    private String username; // 使用者姓名
    
    private List<SysPermission> menus; //選單
    private List<SysPermission> permissions; //許可權
    //...setter和getter方法
}

在SysPermissionMapperCustom.xml中新增根據使用者id查詢使用者許可權的URL

<!-- 根據使用者id查詢URL -->
<select id="findPermissionListByUserId" parameterType="string" resultType="liuxun.ssm.po.SysPermission">
   SELECT 
      * 
    FROM
      sys_permission 
    WHERE TYPE = 'permission' 
      AND id IN 
      (SELECT 
        sys_permission_id 
      FROM
        sys_role_permission 
      WHERE sys_role_id IN 
        (SELECT 
          sys_role_id 
        FROM
          sys_user_role 
        WHERE sys_user_id = #{userid}))
</select>

在SysPermissionMapperCustom.java介面中新增對應的方法

//根據使用者id查詢許可權URL
    public List<SysPermission> findPermissionListByUserId(String userid) throws Exception;

SysServiceImpl.java中新增如下內容

@Override
    public List<SysPermission> findPermissionListByUserId(String userid) throws Exception {
        return sysPermissionMapperCustom.findPermissionListByUserId(userid);
    }
使用者認證通過後取出選單和URL放入Session

修改許可權SysServiceImpl中使用者認證方法的程式碼

//得到使用者id
        String userid = sysUser.getId();
        //根據使用者id查詢選單
        List<SysPermission> menus = this.findMenuListByUserId(userid);
        //根據使用者id查詢許可權url
        List<SysPermission> permissions = this.findPermissionListByUserId(userid);
        
        //認證通過,返回使用者身份資訊
        ActiveUser activeUser = new ActiveUser();
        activeUser.setUserid(userid);
        activeUser.setUsercode(usercode);
        activeUser.setUsername(sysUser.getUsername());
        
        //放入許可權範圍的選單和url
        activeUser.setMenus(menus);
        activeUser.setPermissions(permissions);
選單動態顯示
<c:if test="${activeUser.menus!=null }">
                <ul>
                <c:forEach items="${activeUser.menus }" var="menu">
                    <li><div>
                        <a title="${menu.name }" ref="1_1" href="#"
                            rel="${baseurl }/${menu.url }" icon="icon-log"><span
                            class="icon icon-log"> </span><span class="nav"><a href=javascript:addTab('${menu.name }','${baseurl }/${menu.url }')>${menu.name }</a></span></a>
                    </div></li>
                </c:forEach>
                </ul>
            </c:if>
授權攔截器
public class PermissionInterceptor implements HandlerInterceptor{
    //在執行handler之前執行的
    //用於使用者認證校驗、使用者許可權校驗
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        
        //得到請求的url
        String url = request.getRequestURI();
        
        //判斷是否是公開地址
        //實際開發中需要將公開地址配置在配置檔案中
        //從配置檔案中取出可以匿名訪問的URL
        List<String> open_urls = ResourcesUtil.getKeyList("anonymousURL");
        for (String open_url : open_urls) {
            if (url.indexOf(open_url)>=0) {
                //如果是公開地址 則放行
                return true;
            }
        }
        
        //從配置檔案中獲取公用訪問url
        List<String> common_urls = ResourcesUtil.getKeyList("commonURL");
        //遍歷公用地址 如果是公開地址則放行
        for (String common_url : common_urls) {
            if (url.indexOf(common_url)>0) {
                //如果是公開,則放行
                return true;
            }
        }
        
        //判斷使用者身份在Session中是否存在
        HttpSession session = request.getSession();
        ActiveUser activeUser = (ActiveUser) session.getAttribute("activeUser");
        //從Session中取出許可權範圍的URL
        List<SysPermission> permissions = activeUser.getPermissions();
        for (SysPermission sysPermission : permissions) {
            //許可權url
            String permission_url = sysPermission.getUrl();
            if (url.indexOf(permission_url)>0) {
                return true;
            }
        }
        
        //執行到這裡攔截,跳轉到無權訪問的提示頁面
        request.getRequestDispatcher("/WEB-INF/jsp/refuse.jsp").forward(request, response);
        
        //如果返回false表示攔截器不繼續執行handler,如果返回true表示放行
        return false;
    }
   ......
}
配置授權攔截器

注意:要將授權攔截器配置在使用者認證攔截器的下邊,這是因為SpringMVC攔截器的放行方法是順序執行的,如果是Struts的話則正好相反。

<!-- 攔截器 -->
    <mvc:interceptors>
        <mvc:interceptor>
            <!-- 使用者認證攔截 -->
            <mvc:mapping path="/**"/>
            <bean class="liuxun.ssm.controller.interceptor.LoginInterceptor"></bean>
        </mvc:interceptor>
        <mvc:interceptor>
            <!-- 資源攔截 -->
            <mvc:mapping path="/**"/>
            <bean class="liuxun.ssm.controller.interceptor.PermissionInterceptor"></bean>
        </mvc:interceptor>
    </mvc:interceptors>

關鍵程式碼如下

PO類ActiveUser.java 存放使用者身份和許可權資訊的類

package liuxun.ssm.po;
 
import java.io.Serializable;
import java.util.List;
 
/**
 * 使用者身份資訊,存入Session  由於Tomcat正常關閉時會將Session序列化的本地硬碟上,所以實現Serializable介面
 * @author liuxun
 *
 */
public class ActiveUser implements Serializable {
    private static final long serialVersionUID = 1L;
    
    private String userid; //使用者id(主鍵)
    private String usercode; // 使用者賬號
    private String username; // 使用者姓名
    
    private List<SysPermission> menus; //選單
    private List<SysPermission> permissions; //許可權
    // 提供對應setter和getter方法
    ......
}

自定義許可權的Mapper
SysPermissionMapperCustom.java

package liuxun.ssm.mapper;
 
import java.util.List;
import liuxun.ssm.po.SysPermission;
import liuxun.ssm.po.SysPermissionExample;
import org.apache.ibatis.annotations.Param;
/**
 * 許可權mapper
 * @author liuxun
 *
 */
public interface SysPermissionMapperCustom {
    //根據使用者id查詢選單
    public List<SysPermission> findMenuListByUserId(String userid) throws Exception;
    //根據使用者id查詢許可權URL
    public List<SysPermission> findPermissionListByUserId(String userid) throws Exception;
}

SysPermissionMapperCustom.xml

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="liuxun.ssm.mapper.SysPermissionMapperCustom">
 
<!-- 根據使用者id查詢選單 -->
<select id="findMenuListByUserId" parameterType="string" resultType="liuxun.ssm.po.SysPermission">
   SELECT 
      * 
    FROM
      sys_permission 
    WHERE TYPE = 'menu' 
      AND id IN 
      (SELECT 
        sys_permission_id 
      FROM
        sys_role_permission 
      WHERE sys_role_id IN 
        (SELECT 
          sys_role_id 
        FROM
          sys_user_role 
        WHERE sys_user_id = #{userid}))
</select>
<!-- 根據使用者id查詢URL -->
<select id="findPermissionListByUserId" parameterType="string" resultType="liuxun.ssm.po.SysPermission">
   SELECT 
      * 
    FROM
      sys_permission 
    WHERE TYPE = 'permission' 
      AND id IN 
      (SELECT 
        sys_permission_id 
      FROM
        sys_role_permission 
      WHERE sys_role_id IN 
        (SELECT 
          sys_role_id 
        FROM
          sys_user_role 
        WHERE sys_user_id = #{userid}))
</select>
</mapper>

自定義許可權的Service介面以及實現類
SysService.java

package liuxun.ssm.service;
 
import java.util.List;
 
import liuxun.ssm.po.ActiveUser;
import liuxun.ssm.po.SysPermission;
import liuxun.ssm.po.SysUser;
 
/**
 * 認證授權服務介面
 * @author liuxun
 *
 */
public interface SysService {
    //根據使用者的身份和密碼進行認證,如果認證通過,返回使用者身份資訊
    public ActiveUser authenticat(String usercode,String password) throws Exception;
    
    //根據使用者賬號查詢使用者資訊
    public SysUser findSysUserByUserCode(String userCode) throws Exception;
    
    //根據使用者id查詢許可權範圍內的選單
    public List<SysPermission> findMenuListByUserId(String userid) throws Exception;
    
    //根據使用者id查詢許可權範圍內的url
    public List<SysPermission> findPermissionListByUserId(String userid) throws Exception;
}

SysServiceImpl.java

package liuxun.ssm.service.impl;
 
import java.util.List;
 
import org.springframework.beans.factory.annotation.Autowired;
 
import liuxun.ssm.exception.CustomException;
import liuxun.ssm.mapper.SysPermissionMapperCustom;
import liuxun.ssm.mapper.SysUserMapper;
import liuxun.ssm.po.ActiveUser;
import liuxun.ssm.po.SysPermission;
import liuxun.ssm.po.SysUser;
import liuxun.ssm.po.SysUserExample;
import liuxun.ssm.service.SysService;
import liuxun.ssm.util.MD5;
 
public class SysServiceImpl implements SysService {
    @Autowired
    private SysUserMapper sysUserMapper;
    
    @Autowired
    private SysPermissionMapperCustom sysPermissionMapperCustom;
 
    public ActiveUser authenticat(String usercode, String password) throws Exception {
 
        /**
         * 認證過程: 根據使用者身份(賬號)查詢資料庫,如果查詢不到則使用者不存在 
         * 對輸入的密碼和資料庫密碼進行比對,如果一致則認證通過
         */
        // 根據使用者賬號查詢資料庫
        SysUser sysUser = this.findSysUserByUserCode(usercode);
 
        if (sysUser == null) {
            // 丟擲異常
            throw new CustomException("使用者賬號不存在");
        }
 
        // 資料庫密碼(MD5加密後的密碼)
        String password_db = sysUser.getPassword();
        
        // 對輸入的密碼和資料庫密碼進行比對,如果一致,認證通過
        // 對頁面輸入的密碼進行MD5加密
        String password_input_md5 = new MD5().getMD5ofStr(password);
        if (!password_db.equalsIgnoreCase(password_input_md5)) {
            //丟擲異常
            throw new CustomException("使用者名稱或密碼錯誤");
        }
        //得到使用者id
        String userid = sysUser.getId();
        //根據使用者id查詢選單
        List<SysPermission> menus = this.findMenuListByUserId(userid);
        //根據使用者id查詢許可權url
        List<SysPermission> permissions = this.findPermissionListByUserId(userid);
        
        //認證通過,返回使用者身份資訊
        ActiveUser activeUser = new ActiveUser();
        activeUser.setUserid(userid);
        activeUser.setUsercode(usercode);
        activeUser.setUsername(sysUser.getUsername());
        
        //放入許可權範圍的選單和url
        activeUser.setMenus(menus);
        activeUser.setPermissions(permissions);
        
        return activeUser;
    }
 
    public SysUser findSysUserByUserCode(String userCode) throws Exception {
        SysUserExample sysUserExample = new SysUserExample();
        SysUserExample.Criteria criteria = sysUserExample.createCriteria();
        criteria.andUsercodeEqualTo(userCode);
 
        List<SysUser> list = sysUserMapper.selectByExample(sysUserExample);
        if (list != null && list.size() > 0) {
            return list.get(0);
        }
 
        return null;
    }
    
    @Override
    public List<SysPermission> findMenuListByUserId(String userid) throws Exception {
        return sysPermissionMapperCustom.findMenuListByUserId(userid);
    }
 
    @Override
    public List<SysPermission> findPermissionListByUserId(String userid) throws Exception {
        return sysPermissionMapperCustom.findPermissionListByUserId(userid);
    }
}

登入控制器

package liuxun.ssm.controller;
 
import javax.servlet.http.HttpSession;
 
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
 
import liuxun.ssm.exception.CustomException;
import liuxun.ssm.po.ActiveUser;
import liuxun.ssm.service.SysService;
 
/**
 * 登入和退出
 * @author liuxun
 *
 */
@Controller
public class LoginController {
    @Autowired
    private SysService sysService;
    
    //使用者登入提交方法
    @RequestMapping("/login")
    public String login(HttpSession session,String randomcode,String usercode,String password) throws Exception{
        // 校驗驗證碼,防止惡性攻擊
        // 從Session中獲取正確的驗證碼
        String validateCode = (String) session.getAttribute("validateCode");
        
        //輸入的驗證碼和Session中的驗證碼進行對比
        if (!randomcode.equalsIgnoreCase(validateCode)) {
            //丟擲異常
            throw new CustomException("驗證碼輸入錯誤");
        }
        
        //呼叫Service校驗使用者賬號和密碼的正確性
        ActiveUser activeUser = sysService.authenticat(usercode, password);
        
        //如果Service校驗通過,將使用者身份記錄到Session
        session.setAttribute("activeUser", activeUser);
        //重定向到商品查詢頁面
        return "redirect:/first.action";
    }
    
    //使用者退出
    @RequestMapping("/logout")
    public String logout(HttpSession session) throws Exception{
        //session失效
        session.invalidate();
        //重定向到商品查詢頁面
        return "redirect:/first.action";
    }
}

身份認證攔截器LoginInterceptor.java

package liuxun.ssm.controller.interceptor;
 
import java.util.List;
 
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
 
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
 
import liuxun.ssm.po.ActiveUser;
import liuxun.ssm.util.ResourcesUtil;
 
/**
 * 測試攔截器1
 * @author liuxun
 *
 */
public class LoginInterceptor implements HandlerInterceptor{
    //在執行handler之前執行的
    //用於使用者認證校驗、使用者許可權校驗
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        
        //得到請求的url
        String url = request.getRequestURI();
        
        //判斷是否是公開地址
        //實際開發中需要將公開地址配置在配置檔案中
        //從配置檔案中取出可以匿名訪問的URL
        List<String> open_urls = ResourcesUtil.getKeyList("anonymousURL");
        for (String open_url : open_urls) {
            if (url.indexOf(open_url)>=0) {
                //如果是公開地址 則放行
                return true;
            }
        }
        
        //判斷使用者身份在Session中是否存在
        HttpSession session = request.getSession();
        ActiveUser activeUser = (ActiveUser) session.getAttribute("activeUser");
        //如果使用者身份在session中存在則放行
        if (activeUser!=null) {
            return true;
        }
        //執行到這裡攔截,跳轉到登入頁面,使用者進行身份認證
        request.getRequestDispatcher("/WEB-INF/jsp/login.jsp").forward(request, response);
        
        //如果返回false表示攔截器不繼續執行handler,如果返回true表示放行
        return false;
    }
 
    //在執行handler返回modelAndView之前執行
    //如果需要向頁面提供一些公用的資料或配置一些檢視資訊,使用此方法實現 從modelAndView入手
    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView)
            throws Exception {
        System.out.println("HandlerInterceptor2...postHandle");
    }
 
    //執行handler之後執行此方法
    //作為系統統一異常處理,進行方法執行效能監控,在preHandler中設定一個時間點 在afterCompletion設定一個時間點 二者時間差就是執行時長
    //實現系統,統一日誌記錄
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception modelAndView)
            throws Exception {
        System.out.println("HandlerInterceptor2...afterCompletion");
    }
 
}

資源授權攔截器PermissionInterceptor

package liuxun.ssm.controller.interceptor;
 
import java.security.acl.Permission;
import java.util.List;
 
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
 
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
 
import liuxun.ssm.po.ActiveUser;
import liuxun.ssm.po.SysPermission;
import liuxun.ssm.util.ResourcesUtil;
 
/**
 * 授權攔截器
 * @author liuxun
 *
 */
public class PermissionInterceptor implements HandlerInterceptor{
    //在執行handler之前執行的
    //用於使用者認證校驗、使用者許可權校驗
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        
        //得到請求的url
        String url = request.getRequestURI();
        
        //判斷是否是公開地址
        //實際開發中需要將公開地址配置在配置檔案中
        //從配置檔案中取出可以匿名訪問的URL
        List<String> open_urls = ResourcesUtil.getKeyList("anonymousURL");
        for (String open_url : open_urls) {
            if (url.indexOf(open_url)>=0) {
                //如果是公開地址 則放行
                return true;
            }
        }
        
        //從配置檔案中獲取公用訪問url
        List<String> common_urls = ResourcesUtil.getKeyList("commonURL");
        //遍歷公用地址 如果是公開地址則放行
        for (String common_url : common_urls) {
            if (url.indexOf(common_url)>0) {
                //如果是公開,則放行
                return true;
            }
        }
        
        //判斷使用者身份在Session中是否存在
        HttpSession session = request.getSession();
        ActiveUser activeUser = (ActiveUser) session.getAttribute("activeUser");
        //從Session中取出許可權範圍的URL
        List<SysPermission> permissions = activeUser.getPermissions();
        for (SysPermission sysPermission : permissions) {
            //許可權url
            String permission_url = sysPermission.getUrl();
            if (url.indexOf(permission_url)>0) {
                return true;
            }
        }
        
        //執行到這裡攔截,跳轉到無權訪問的提示頁面
        request.getRequestDispatcher("/WEB-INF/jsp/refuse.jsp").forward(request, response);
        
        //如果返回false表示攔截器不繼續執行handler,如果返回true表示放行
        return false;
    }
 
    //在執行handler返回modelAndView之前執行
    //如果需要向頁面提供一些公用的資料或配置一些檢視資訊,使用此方法實現 從modelAndView入手
    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView)
            throws Exception {
        System.out.println("HandlerInterceptor2...postHandle");
    }
 
    //執行handler之後執行此方法
    //作為系統統一異常處理,進行方法執行效能監控,在preHandler中設定一個時間點 在afterCompletion設定一個時間點 二者時間差就是執行時長
    //實現系統,統一日誌記錄
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception modelAndView)
            throws Exception {
        System.out.println("HandlerInterceptor2...afterCompletion");
    }
 
}

攔截器配置

<!-- 攔截器 -->
    <mvc:interceptors>
        <mvc:interceptor>
            <!-- 使用者認證攔截 -->
            <mvc:mapping path="/**"/>
            <bean class="liuxun.ssm.controller.interceptor.LoginInterceptor"></bean>
        </mvc:interceptor>
        <mvc:interceptor>
            <!-- 資源攔截 -->
            <mvc:mapping path="/**"/>
            <bean class="liuxun.ssm.controller.interceptor.PermissionInterceptor"></bean>
        </mvc:interceptor>
    </mvc:interceptors>

總結

使用基於url攔截的許可權管理方式,實現起來比較簡單,不依賴框架,使用web提供filter就可以實現。
問題:
需要將所有的url全部配置起來,有些繁瑣,不易維護,url(資源)和許可權表示方式不規範。

相關文章