JAVAWEB開發之權限管理(一)——權限管理詳解(權限管理原理以及方案)、不使用權限框架的原始授權方式詳解

知識清單

1.瞭解基於資源的權限管理方式
2. 掌握權限數據模型
3. 掌握基於url的權限管理(不使用Shiro權限框架的情況下實現權限管理)
4. shiro實現用戶認證
5. shiro實現用戶授權
6. shiro與企業web項目整合開發的方法

權限管理原理知識

什麼是權限管理

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

用戶認證

用戶認證概念

用戶認證—— 用戶去訪問系統,系統需要驗證用戶身份的合法性。最常用的用戶身份認證方法:1.用戶密碼方式、2.指紋打卡機、3.基於證書的驗證方法。系統驗證用戶身份合法,用戶方可訪問系統的資源。

用戶認證流程


關鍵對象

subject:主體,理解爲用戶,可能是程序,都要去訪問系統的資源,系統需要對subject進行身份認證。
principal:身份信息,通常是唯一的,一個主體可以有多個身份信息,但是只能有一個主身份信息(primary  principal)。
credential:憑證信息,可以是密碼、證書、指紋等。
總結:主體在進行身份認證時需要提供身份信息和憑證信息。

用戶授權

用戶授權概念

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

授權流程


其中橙色爲授權流程

關鍵對象

授權的過程可以理解爲  who  對 what(which) 進行how操作
who:主體,即subject,subject在認證通過後,系統進行訪問控制。
what(which):資源(Resource) ,subject必須具備資源訪問權限纔可以訪問該資源。資源包括很多方面比如:用戶列表頁面、商品修改菜單、商品id爲001的商品信息。
資源分爲資源類型和資源實例
例如系統的用戶信息就是資源類型,相當於Java類。
系統中id爲001的用戶就是資源實例,相當於new的Java對象。
how:權限/許可(permission),針對資源的權限或許可,subject必須具有permission方可訪問資源,如何訪問/操作需要定義permission,權限比如:用戶添加、用戶添加、商品刪除。

權限模型

主體(賬號、密碼)
資源(資源名稱,訪問地址)
權限(權限名稱、資源id)
角色(角色名稱)
角色和權限關係(角色id、權限id)
如下圖:

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

上圖被稱爲權限管理的通用模型,不過在企業開發中根據系統自身特點還會對上圖進行修改,但是用戶、角色、權限、用戶角色關係、角色權限關係是必不可少的。

分配權限

用戶需要分配相應的權限纔可以訪問相應的資源。權限是對資源的操作許可。
通常給用戶分配資源權限需要將權限信息持久化,比如存儲在關係數據庫中。
把用戶信息、權限管理、用戶分配的權限信息寫入到數據庫(權限數據模型)。

權限控制(授權核心)

基於角色的訪問控制

RBAC (Role  based access  control) 基於角色的訪問控制
比如:
系統角色包括:部門經理、總經理...(角色針對用戶進行劃分)
系統中代碼實現:
//如果該user是部門經理則可以訪問if中的代碼
if(user.getRole("部門經理")){
    // 系統資源內容
    // 用戶報表查看
}
問題:
角色是針對人進行劃分的,人作爲用戶在系統中屬於活動內容,如果該角色可以訪問的資源出現變更,則需要修改代碼,比如:需要變更爲部門經理和總經理都可以進行用戶報表查看,代碼改爲:
if(user.getRole("部門經理") || user.getRole("總經理")){
    // 系統資源內容
    // 用戶報表查看
}
由此可以發現基於角色的訪問控制是不利於系統維護的(可擴展性不強)

基於資源的訪問控制

RBAC (Resource  based  access control)  基於資源的訪問控制
資源在系統中是不變的,比如資源有:類中的方法,頁面中的按鈕
對資源的訪問需要具有permission權限,代碼可以寫爲:
if(user.hasPermission("用戶報表查看(權限標識符)")){
    // 系統資源內容
    // 用戶報表查看
}
上面的方法就可以解決用戶角色變更而不用修改上邊權限控制的代碼。
如果需要變更權限只需要在分配權限模塊去操作,給部門經理或總經理增加或解除權限
建議使用基於資源的訪問控制實現權限管理。

權限管理解決方案

什麼是粗粒度權限和細粒度權限?

粗粒度權限管理,是對資源類型的管理,資源類型比如:菜單、url連接、用戶添加頁面、用戶信息、類方法、頁面中按鈕。
粗粒度權限管理比如:超級管理員可以訪問用戶添加頁面、用戶信息等全部頁面。
部門管理員可以訪問用戶信息頁面,包括頁面中所有按鈕。

細粒度的權限管理,對資源實例的權限管理。資源實例就是資源類型的具體化,比如:用戶id爲001的修改連接,1110班的用戶信息、行政部的員工。
細粒度的權限管理就是數據級別的權限管理。
細粒度權限管理比如:部門經理只可以訪問本部門的員工信息,用戶只可以看到自己的菜單,大區經理只能查看本轄區的銷售訂單...

粗粒度和細粒度例子:
系統中有一個用戶查詢頁面,對用戶列表查詢分權限,如粗粒度管理,張三和李四都有用戶列表查詢的權限,張三和李四都可以訪問用戶列表查詢。
進一步進行細粒度的管理,張三(行政部)和李四(開發部)只可以查詢自己本部門的用戶信息,張三隻能查看行政部的用戶信息,李四隻能查詢開發部門的用戶信息。細粒度的權限管理就是數據級別的權限管理。

如何實現粗粒度和細粒度的權限管理

如何實現粗粒度的權限管理?
粗粒度權限管理比較容易將權限管理代碼抽取出來在系統架構級別統一管理。比如:通過SpringMVC的攔截器實現授權。
如何實現細粒度的權限管理?
對細粒度的權限管理在數據級別是沒有共性可言的,針對細粒度的權限管理就是系統業務邏輯的一部分,如果在業務層去處理相對簡單,如果將細粒度的權限管理統一在系統架構級別去抽取,比較困難,即使進行了抽取,功能也可能存在擴展性不全的弊端。建議細粒度權限管理放在業務層去控制。比如:部門經理只查詢本部門員工信息,在Service接口提供一個部門id的參數,controller中根據當前用戶信息得到該用戶屬於哪個部門,調用service時將部門id傳入service,實現該用戶只查詢本部門的員工。

基於url攔截的方式實現

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

使用權限管理框架來實現

對於粗粒度的權限管理,建議使用優秀的權限管理框架進行實現,節省開發成本,提高開發效率。
Shiro就是一個優秀的權限管理框架。

基於URL的權限管理

基於url的權限管理流程



搭建環境

數據庫

MySQL數據庫中創建表:用戶表、角色表、權限表(實質是權限和資源的結合)、用戶角色關係表、角色權限關係表

新建數據庫shiro, 爲了節約測試時間,在SpringMVC+mybatis基礎之上進行整合(導入以前的基本數據),並導入權限數據如下:

有關權限的SQL腳本如下:
shiro_sql_table.sql
/*
SQLyog v10.2 
MySQL - 5.1.72-community : Database - shiro
*********************************************************************
*/


/*!40101 SET NAMES utf8 */;

/*!40101 SET SQL_MODE=''*/;

/*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */;
/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */;
/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */;
/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */;
/*Table structure for table `sys_permission` */

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;

/*!40101 SET SQL_MODE=@OLD_SQL_MODE */;
/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */;
/*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */;
/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */;
shiro_sql_table_data.sql
/*
SQLyog v10.2 
MySQL - 5.1.72-community : Database - shiro
*********************************************************************
*/


/*!40101 SET NAMES utf8 */;

/*!40101 SET SQL_MODE=''*/;

/*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */;
/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */;
/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */;
/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */;
/*Data for the table `sys_permission` */

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');

/*!40101 SET SQL_MODE=@OLD_SQL_MODE */;
/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */;
/*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */;
/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */;
查看對應權限模型的數據如下:
sys_user  用戶表數據

sys_role  角色表

sys_permission 權限表

sys_user_role  用戶角色關係表

sys_role_permission  角色權限關係表

開發環境

JDK1.8
MyEclipse
技術架構:SpringMVC+Mybatis+jQuery easyUI

系統工程架構


系統登錄

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

用戶的身份信息

創建專門類用於記錄用戶身份信息。
/**
 * 用戶身份信息,存入Session  由於Tomcat正常關閉時會將Session序列化的本地硬盤上,所以實現Serializable接口
 * @author liuxun
 *
 */
public class ActiveUser implements Serializable {
	private String userid; //用戶id(主鍵)
	private String usercode; // 用戶賬號
	private String username; // 用戶姓名
	....
        ....
}

mapper

mapper接口:根據用戶賬號查詢用戶(sys_user)信息 (使用逆向工程生成權限相關的PO類和mapper接口)
如下所示:
  
將生成的代碼拷貝到項目中

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


編寫身份認證攔截器

//用於用戶認證校驗、用戶權限校驗
	@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="liuxun.ssm.controller.interceptor.LoginInterceptor"></bean>
	    </mvc:interceptor>
	</mvc:interceptors>

用戶授權

commonURL.properties配置公用訪問地址

在此配置文件中配置公用訪問地址,公用訪問地址只需要通過用戶認證,不需要對公用訪問地址分配權限即可訪問。

獲取用戶權限範圍的菜單

思路:
在用戶認證時,認證通過,根據用戶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攔截總結:
使用基於URL攔截的權限管理方式,實現起來比較簡單,不依賴框架使用過濾器或攔截器就可以實現
弊端:需要將所有的URL全部配置起來,比較繁瑣,不易維護,URL(資源)和權限表示方式不規範

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章