
應(yīng)老大的要求,給項(xiàng)目集成shiro權(quán)限管理,之前沒(méi)有接觸過(guò),這幾天也是查了一些資料,初步實(shí)現(xiàn)了登錄驗(yàn)證和url的權(quán)限管理。
對(duì)Shiro的理解
shiro是Apache的開(kāi)源框架,是一種功能強(qiáng)大且易用的安全框架,主要有四種功能。
- 認(rèn)證——用戶(hù)的身份識(shí)別,常用作用戶(hù)的登錄
- 權(quán)限——訪問(wèn)的控制,對(duì)于每次訪問(wèn)都會(huì)檢查是否具有訪問(wèn)權(quán)限
- 會(huì)話(huà)管理——會(huì)話(huà)管理,用戶(hù)session管理器,用戶(hù)相關(guān)的時(shí)間敏感的狀態(tài)
- 密碼加密——把JDK中復(fù)雜的密碼加密方式進(jìn)行封裝,保護(hù)或隱藏?cái)?shù)據(jù)防止被偷窺
網(wǎng)上找到架構(gòu)圖如下:

shiro包含三個(gè)核心的組件:Subject,SecurityManager 和 Realms
- Subject是與程序交互的對(duì)象,一般稱(chēng)之為用戶(hù),Subject 實(shí)例都必須綁定到一個(gè)SecurityManager上。一個(gè)applicationCode與Subject 的交互,在運(yùn)行時(shí)shiro會(huì)自動(dòng)將次subject轉(zhuǎn)化為與SecurityManager的特定subject的交互 。
- SecurityManager是shiro的核心,協(xié)調(diào)各個(gè)模塊的運(yùn)行,無(wú)需我們對(duì)其操作,我們只要操作subject就好,其他都交給SecurityManager去管理。
- Realms是應(yīng)用程序與持久化數(shù)據(jù)之間交互的橋梁,需要我們自己編程去實(shí)現(xiàn)相應(yīng)的功能。比如登錄認(rèn)證需要實(shí)現(xiàn)對(duì)doGetAuthenticationInfo方法的重寫(xiě),權(quán)限認(rèn)證需要實(shí)現(xiàn)對(duì)doGetAuthorizationInfo方法的重寫(xiě)
集成shiro
添加依賴(lài)
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.3.2</version>
</dependency>
權(quán)限與角色表結(jié)構(gòu)
總共分四張表,用戶(hù)角色表、角色表、角色權(quán)限表以及權(quán)限初始化表,用戶(hù)與角色掛鉤,角色與權(quán)限掛鉤,每個(gè)用戶(hù)可以擔(dān)任多個(gè)角色,每個(gè)角色可以擁有多個(gè)權(quán)限,權(quán)限初始化表主要是對(duì)Shiro 攔截的請(qǐng)求做聲明,提供訪問(wèn)頁(yè)面與權(quán)限的關(guān)系。
角色表
DROP TABLE IF EXISTS `role`;
CREATE TABLE `role` (
`id` bigint(64) DEFAULT NULL,
`name` varchar(32) DEFAULT NULL COMMENT '角色名稱(chēng)'
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
角色權(quán)限表
DROP TABLE IF EXISTS `role_permission`;
CREATE TABLE `role_permission` (
`id` bigint(64) NOT NULL,
`rid` bigint(64) DEFAULT NULL COMMENT '角色I(xiàn)D',
`pname` varchar(255) DEFAULT NULL COMMENT '權(quán)限ID',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
用戶(hù)角色表
DROP TABLE IF EXISTS `user_role`;
CREATE TABLE `user_role` (
`id` bigint(64) NOT NULL,
`uid` bigint(64) DEFAULT NULL COMMENT '用戶(hù)ID',
`rid` bigint(64) DEFAULT NULL COMMENT '角色I(xiàn)D',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
權(quán)限初始化表
DROP TABLE IF EXISTS `permission_init`;
CREATE TABLE `permission_init` (
`id` bigint(11) NOT NULL,
`url` varchar(255) DEFAULT NULL COMMENT '鏈接',
`permission_init` varchar(255) DEFAULT NULL COMMENT '需要具備的權(quán)限',
`sort` int(11) DEFAULT '0' COMMENT '排序',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
權(quán)限初始化表用來(lái)存儲(chǔ)訪問(wèn)鏈接與其需要具備的權(quán)限,在shiro的配置類(lèi)中進(jìn)行動(dòng)態(tài)加載。
Shiro配置類(lèi)
/**
* shiro配置
*
* @title
* @author liyan
* @date 2018年5月23日 下午2:43:45
*/
@Configuration
public class ShiroConfig {
@Autowired
private PermissionService permissionService;
@Bean
public ShiroFilterFactoryBean shiroFilter(SecurityManager sec urityManager) {
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
shiroFilterFactoryBean.setSecurityManager(securityManager);
// 設(shè)置 SecurityManager
shiroFilterFactoryBean.setSecurityManager(securityManager);
// 設(shè)置登錄頁(yè)
shiroFilterFactoryBean.setLoginUrl("/login.html");
// 登錄成功后要跳轉(zhuǎn)的鏈接
shiroFilterFactoryBean.setSuccessUrl("/list.html");
// 未授權(quán)界面;
shiroFilterFactoryBean.setUnauthorizedUrl("/403.html");
// 權(quán)限控制map.
Map<String, String> filterChainDefinitionMap = new LinkedHashMap<String, String>();
// 從數(shù)據(jù)庫(kù)獲取訪問(wèn)鏈接的初始化權(quán)限,對(duì)shiro攔截的請(qǐng)求做聲明
List<PermissionInit> permissionList = permissionService.getPermissionInit();
for (PermissionInit permission : permissionList) {
filterChainDefinitionMap.put(permission.getUrl(), permission.getPermissionInit());
}
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
return shiroFilterFactoryBean;
}
@Bean
public SecurityManager securityManager() {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
// 設(shè)置realm.
securityManager.setRealm(myShiroRealm());
// 注入記住我管理器;
securityManager.setRememberMeManager(rememberMeManager());
return securityManager;
}
/**
* 權(quán)限認(rèn)證realm
*
* @return
*/
@Bean
public ShiroRealm shiroRealms() {
ShiroRealm shiroRealm = new ShiroRealm();
return shiroRealm;
}
/**
* cookie管理對(duì)象;記住我功能
*
* @return
*/
public CookieRememberMeManager rememberMeManager() {
CookieRememberMeManager cookieRememberMeManager = new CookieRememberMeManager();
cookieRememberMeManager.setCookie(rememberMeCookie());
// rememberMe cookie加密的密鑰 建議每個(gè)項(xiàng)目都不一樣 默認(rèn)AES算法 密鑰長(zhǎng)度(128 256 512 位)
cookieRememberMeManager.setCipherKey(Base64.decode("3AvVhmFLUs0KTA3Kprsdag=="));
return cookieRememberMeManager;
}
/**
* cookie對(duì)象;
*
* @return
*/
public SimpleCookie rememberMeCookie() {
// 這個(gè)參數(shù)是cookie的名稱(chēng),對(duì)應(yīng)前端的checkbox的name = rememberMe
SimpleCookie simpleCookie = new SimpleCookie("rememberMe");
// <!-- 記住我cookie生效時(shí)間30天 ,單位秒;-->
simpleCookie.setMaxAge(2592000);
return simpleCookie;
}
}
在不添加cookie對(duì)象管理的時(shí)候,登錄接口里如果remeberMe為true,調(diào)用會(huì)報(bào)異常。需要注意的一點(diǎn)是,我在permission_init表中初始化的頁(yè)面鏈接沒(méi)有加頁(yè)面的后綴.html,比如字段url的值為/create,過(guò)濾器攔截一直失敗,只有改成/create.html才成功攔截進(jìn)入到realm中。ShiroRealm是需要我們自己繼承AuthorizingRealm類(lèi),并且重寫(xiě)doGetAuthenticationInfo和doGetAuthorizationInfo方法。
UsernamePasswordToken token = new UsernamePasswordToken(username, password, rememberMe);
實(shí)現(xiàn)ShiroRealm
登錄認(rèn)證
在Shiro中,最終的認(rèn)證都是通過(guò)realm來(lái)獲取應(yīng)用程序的用戶(hù)角色以及權(quán)限等信息,在對(duì)登錄認(rèn)證過(guò)程中,shiro會(huì)調(diào)用reaml的doGetAuthenticationInfo(AuthenticationToken authcToken)方法,驗(yàn)證通過(guò)后會(huì)返回一個(gè)封裝了用戶(hù)信息的SimpleAuthenticationInfo實(shí)例
/**
* 登錄驗(yàn)證: Authentication 是用來(lái)驗(yàn)證用戶(hù)身份
*
* @param token
* @return
* @throws AuthenticationException
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authcToken)
throws AuthenticationException {
UsernamePasswordToken token = (UsernamePasswordToken) authcToken;
String name = token.getUsername();
String password = String.valueOf(token.getPassword());
// 密碼進(jìn)行加密處理 明文為 password+name
String paw = password + name;
String pawDES = DesEncrypt.encryptBasedDes(paw);
// 從數(shù)據(jù)庫(kù)獲取對(duì)應(yīng)用戶(hù)名密碼的用戶(hù)
UserInfo userInfo = logionService.getUserInfo(name, pawDES);
if (null == userInfo) {
return null;
} else {
// 更新登錄時(shí)間 last login time
userInfo.setLastLoginTime(new Date());
logionService.updateLoginTime(userInfo);
}
System.out.println("身份認(rèn)證成功,登錄用戶(hù):" + name);
return new SimpleAuthenticationInfo(String.valueOf(userInfo.getUserId()), password, getName());
}
權(quán)限認(rèn)證
訪問(wèn)的url只有在shiro的配置類(lèi)里面添加了權(quán)限的filterChainDefinitions配置后,才會(huì)被攔截并跳轉(zhuǎn)到重寫(xiě)的doGetAuthorizationInfo(PrincipalCollection principals)方法中,該方法獲取當(dāng)前用戶(hù)的角色和擁有的權(quán)限信息,并封裝到SimpleAuthorizationInfo的實(shí)例中,驗(yàn)證通過(guò)的允許訪問(wèn),未經(jīng)授權(quán)的會(huì)跳轉(zhuǎn)到shiro配置類(lèi)里面預(yù)先定義好的頁(yè)面中,參考上面描述。
/**
* 權(quán)限認(rèn)證:Authorization用來(lái)進(jìn)行權(quán)限驗(yàn)證
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
String userId = (String) SecurityUtils.getSubject().getPrincipal();
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
Long userid = StringUtil.parseLong(userId);
// 查詢(xún)用戶(hù)所屬角色,放入到Authorization里。
Set<String> roleSet = permissionService.getUserRole(userid);
info.setRoles(roleSet);
// 查詢(xún)用戶(hù)所擁有的權(quán)限(permission),放入到Authorization里。
Set<String> permissionSet = permissionService.getUserPermission(userid);
info.setStringPermissions(permissionSet);
return info;
}
接口調(diào)用
在登錄接口中將登錄信息封裝在UsernamePasswordToken中,再調(diào)用subject的login方法執(zhí)行登錄,這時(shí)會(huì)進(jìn)入realm中進(jìn)行登錄信息的驗(yàn)證工作。
UsernamePasswordToken token = new UsernamePasswordToken(username, password, rememberMe);
Subject currentSubject=SecurityUtils.getSubject();
currentSubject.login(token);
currentSubject.getSession().setAttribute("name", username);
serviceResult = new ServiceResult(HttpConstants.RESUTL_OK, "登錄成功");