最近一直想總結下最近學習shiro權限管理的收穫,惰性使然,沒有去整理。春節放假前一天沒什麼工作,閒來總結下。
shiro是一個用來管理權限的框架。經過兩個項目的整合,個人認爲,整合shiro的重點在於配置文件的配置還有Realm自定義攔截器的編寫。當然,想要良好的把shiro用於開發環境,前提需要有一套合適的表結構,最基礎的權限模塊應包含:用戶表、角色表、權限表。再複雜一些的,可以再加入部門表、菜單表、數據權限表等。至於表結構的設計,在此不做詳述,總之要根據自己的業務需求去設計。下面開始按流程,一步步整合shiro。
第一部分:SpringMVC整合shiro的步驟:
1. 導入jar包(Maven)
<!-- Spring 整合Shiro需要的依賴 -->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-core</artifactId>
<version>1.2.1</version>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-web</artifactId>
<version>1.2.1</version>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-ehcache</artifactId>
<version>1.2.1</version>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.2.5</version>
</dependency>
<!--導入eheache的緩存包 -->
<dependency>
<groupId>net.sf.ehcache</groupId>
<artifactId>ehcache-core</artifactId>
<version>2.6.11</version>
</dependency>
2. 添加配置文件
SpringMvc整合shiro過程中,配置文件是很煩人的一件事,尤其是在配置出錯的時候,控制檯會報一大長串的錯誤。這時候大家需要心平氣和的來對待,因爲生氣也沒有用,活不還是得自己幹嘛....
1. 首先,在resource文件夾下,創建spring-shiro.xml文件。創建完畢後,先來配置一下(具體配置內容在下面),讓項目去加載這個配置文件,這裏有兩種配置方式。
方式①
找到項目的web.xml文件,在<context-param><context-param/>中,加入<param-value>值爲 classpath:spring-shiro.xml.如圖:
項目結構有可能不一樣,web.xml:
此處加入shiro配置文件:
方式②
在applicationContext.xml,引入shiro配置文件。
2. 加載shiro過濾器
在web.xml中配置shiro過濾器加載
<!-- 配置Shiro過濾器,先讓Shiro過濾系統接收到的請求 -->
<!-- 這裏filter-name必須對應spring-shiro.xml中定義的<bean id="shiroFilter"/> -->
<!-- 使用[/*]匹配所有請求,保證所有的可控請求都經過Shiro的過濾 -->
<!-- 通常會將此filter-mapping放置到最前面(即其他filter-mapping前面),以保證它是過濾器鏈中第一個起作用的 -->
<filter>
<filter-name>shiroFilter</filter-name>
<filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
<init-param>
<param-name>targetFilterLifecycle</param-name>
<param-value>true</param-value> <!--該值缺省爲false,表示生命週期由SpringApplicationContext管理,設置爲true則表示由ServletContainer管理 -->
</init-param>
</filter>
<filter-mapping>
<filter-name>shiroFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
此時,spring-shiro.xml中我們還沒有配置<bean id="shiroFilter"/>,下一步將配置spring-shiro.xml
3. 配置 spring-shiro.xml(這裏整合了ehcache作爲權限緩存)
在這裏我直接貼上配置好的文件,至於bean的功能會有註釋
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.0.xsd"
default-lazy-init="true">
<description>Shiro Configuration</description>
<import resource="spring-ehcache.xml"></import>
<!-- 項目自定義的Realm,需自己編寫該類 -->
<bean id="jhShiroRealm" class="com.jiahe.common.shiro.ShiroRealm"/>
<!-- 會話管理器 -->
<bean id="sessionManager" class="org.apache.shiro.web.session.mgt.DefaultWebSessionManager">
<!-- session的失效時長(1分鐘/600000),單位毫秒 -->
<property name="globalSessionTimeout" value="1800000"/>
<!-- 刪除失效的session -->
<property name="deleteInvalidSessions" value="true"/>
<property name="sessionDAO" ref="sessionDAO"/>
<property name="sessionIdCookieEnabled" value="true"/>
<property name="sessionIdCookie" ref="sessionIdCookie"/>
<property name="sessionValidationSchedulerEnabled" value="true"/>
<!--
Shiro提供了會話驗證調度器,用於定期的驗證會話是否已過期,如果過期將停止會話;
出於性能考慮,一般情況下都是獲取會話時來驗證會話是否過期並停止會話的;
但是如在web環境中,如果用戶不主動退出是不知道會話是否過期的,因此需要定期的檢測會話是否過期,
Shiro提供了會話驗證調度器SessionValidationScheduler來做這件事情。
-->
<property name="sessionValidationScheduler" ref="sessionValidationScheduler"/>
<property name="cacheManager" ref="shiroEhcacheManager"/>
</bean>
<!--安全管理器-->
<bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
<property name="realm" ref="jhShiroRealm" />
<property name="sessionManager" ref="sessionManager"/>
<property name="cacheManager" ref="shiroEhcacheManager"/>
</bean>
<!-- Shiro Filter -->
<bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
<property name="securityManager" ref="securityManager" />
<property name="loginUrl" value="/login.htm" /> <!--沒有登錄的用戶跳轉的界面-->
<property name="successUrl" value="" /> <!--登錄成功後跳轉頁面,若不配置,則可在邏輯中實現-->
<property name="unauthorizedUrl" value="/403.htm" /> <!--沒有權限跳轉的頁面,自己編寫該界面-->
<property name="filterChainDefinitions"> <!--全局過濾器 -->
<value>
<!-- anon代表不需要授權即可訪問,對靜態資源設置匿名訪問 -->
/css/** = anon
/js/** = anon
/static/** = anon
/json/** = anon
/images/** = anon
/tools/** = anon
/login.htm = anon
/toLogin.htm = anon
/swagger-ui.html = anon
/webjars/** = anon
/v2/** = anon
/swagger-resources/** = anon
/configuration/** = anon
/kickout.htm = anon
/verify.htm = anon
<!-- 所有的請求(除去配置的靜態資源請求或請求地址爲anon的請求)都要通過登錄驗證,如果未登錄則跳到unauthorizedUrl指定的url -->
/** = authc
</value>
</property>
</bean>
<!-- 會話DAO,sessionManager裏面的session需要保存在會話Dao裏,沒有會話Dao,session是瞬時的,沒法從 sessionManager裏面拿到session -->
<bean id="sessionDAO" class="org.apache.shiro.session.mgt.eis.EnterpriseCacheSessionDAO">
<!-- 用於生成會話ID,默認就是JavaUuidSessionIdGenerator,使用java.util.UUID生成 -->
<property name="sessionIdGenerator" ref="sessionIdGenerator"/>
<property name="activeSessionsCacheName" value="shiro-activeSessionCache"/>
</bean>
<!-- 會話ID生成器 -->
<bean id="sessionIdGenerator" class="org.apache.shiro.session.mgt.eis.JavaUuidSessionIdGenerator"></bean>
<!-- 會話Cookie模板,sessionManager創建會話Cookie的模板 -->
<bean id="sessionIdCookie" class="org.apache.shiro.web.servlet.SimpleCookie">
<constructor-arg value="sid"/>
<property name="httpOnly" value="true"/>
<property name="maxAge" value="-1"/>
</bean>
<!-- 會話驗證調度器 -->
<bean id="sessionValidationScheduler" class="org.apache.shiro.session.mgt.ExecutorServiceSessionValidationScheduler">
<!-- 設置調度時間間隔,單位毫秒,默認就是1小時 -->
<property name="interval" value="1800000"/>
<!-- 設置會話驗證調度器進行會話驗證時的會話管理器 -->
<property name="sessionManager" ref="sessionManager"/>
</bean>
<!-- 保證實現了Shiro內部生命週期函數的bean執行 -->
<bean id="lifecycleBeanPostProcessor" class="org.apache.shiro.spring.LifecycleBeanPostProcessor"/>
<!-- 啓用shrio授權註解攔截方式 -->
<bean class="org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor">
<property name="securityManager" ref="securityManager" />
</bean>
<!--登出系統 交給shiro處理-->
<!--<bean id="logout" class="org.apache.shiro.web.filter.authc.LogoutFilter">
<property name="redirectUrl" value="/login.htm" />
</bean>-->
</beans>
4. 添加spring-ehcache.xml配置
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:cache="http://www.springframework.org/schema/cache"
xmlns:p="http://www.springframework.org/schema/p"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.3.xsd
http://www.springframework.org/schema/cache http://www.springframework.org/schema/cache/spring-cache-4.3.xsd
">
<cache:annotation-driven cache-manager ="springCacheManager" />
<bean id="ehcacheManager" class="org.springframework.cache.ehcache.EhCacheManagerFactoryBean">
<property name="configLocation" value="classpath:ehcache_jh.xml"/>
</bean>
<bean id="springCacheManager" class="org.springframework.cache.ehcache.EhCacheCacheManager" p:cacheManager-ref= "ehcacheManager"/>
<!-- 用戶授權信息Cache -->
<bean id="shiroEhcacheManager" class="org.apache.shiro.cache.ehcache.EhCacheManager">
<property name="cacheManager" ref="ehcacheManager"></property>
</bean>
</beans>
配置完之後,需要在applicationContext.xml中加載緩存配置文件
5. 配置ehcache_jh.xml緩存
<?xml version="1.0" encoding="UTF-8"?>
<ehcache>
<!--指定一個目錄:當 EHCache 把數據寫到硬盤上時, 將把數據寫到這個目錄下. <diskStore path="d:\\tempDirectory"/> -->
<diskStore path="java.io.tmpdir/ehcache"/>
<!--
eternal:緩存中對象是否爲永久的,如果是,超時設置將被忽略,對象從不過期。
maxElementsInMemory:緩存中允許創建的最大對象數
overflowToDisk:設置基於內存的緩存中的對象數目達到上限後,是否把溢出的對象寫到基於硬盤的緩存中
timeToIdleSeconds:設置對象空閒最長時間,以秒爲單位, 超過這個時間,對象過期。當對象過期時,EHCache會把它從緩存中清除。如果此值爲0,表示對象可以無限期地處於空閒狀態。
timeToLiveSeconds:設置對象生存最長時間,超過這個時間,對象過期。如果此值爲0,表示對象可以無限期地存在於緩存中.該屬性值必須大於或等於 timeToIdleSeconds 屬性值
maxElementsOnDisk:在磁盤上緩存的element的最大數目,默認值爲0,表示不限制。
diskPersistent: 設定在虛擬機重啓時是否進行磁盤存儲,默認爲false
diskExpiryThreadIntervalSeconds:對象檢測線程運行時間間隔。標識對象狀態的線程多長時間運行一次。
maxEntriesLocalHeap=: 堆內存中最大緩存對象數,0沒有限制(必須設置)
memoryStoreEvictionPolicy:緩存滿了之後的淘汰算法。
1 FIFO,先進先出
2 LFU,最少被使用,緩存的元素有一個hit屬性,hit值最小的將會被清出緩存。
3 LRU,最近最少使用的,緩存的元素有一個時間戳,當緩存容量滿了,而又需要騰出地方來緩存新的元素的時候,那麼現有緩存元素中時間戳離當前時間最遠的元素將被清出緩存。
-->
<defaultCache eternal="false"
maxElementsInMemory="10000"
overflowToDisk="false"
timeToIdleSeconds="0"
timeToLiveSeconds="0"
memoryStoreEvictionPolicy="LFU" />
<!--
設定具體的命名緩存的數據過期策略。每個命名緩存代表一個緩存區域
緩存區域(region):一個具有名稱的緩存塊,可以給每一個緩存塊設置不同的緩存策略。
如果沒有設置任何的緩存區域,則所有被緩存的對象,都將使用默認的緩存策略。即:<defaultCache.../>
Hibernate 在不同的緩存區域保存不同的類/集合。
對於類而言,區域的名稱是類名。如:com.atguigu.domain.Customer
對於集合而言,區域的名稱是類名加屬性名。如com.atguigu.domain.Customer.orders
-->
<!-- shiro 會話緩存-->
<cache name="shiro-activeSessionCache"
eternal="false"
maxElementsInMemory="10000"
timeToIdleSeconds="1800000"
timeToLiveSeconds="1800000"
overflowToDisk="false"
statistics="true"/>
<!--<cache name="sysDataCache"
maxElementsInMemory="10000"
maxElementsOnDisk="1000"
eternal="false"
overflowToDisk="false"
diskSpoolBufferSizeMB="20"
timeToIdleSeconds="3600"
timeToLiveSeconds="3600"
memoryStoreEvictionPolicy="LFU" />-->
<!--<cache name="sysDictCache"
maxElementsInMemory="10000"
maxElementsOnDisk="1000"
eternal="true"
overflowToDisk="false"
diskSpoolBufferSizeMB="20"
timeToIdleSeconds="3600"
timeToLiveSeconds="3600"
memoryStoreEvictionPolicy="LFU" />-->
<!-- 默認緩存 -->
<!--<cache name="DefaultRegion"
maxElementsInMemory="100000"
eternal="true"
maxElementsOnDisk="100000"
overflowToDisk="true"
memoryStoreEvictionPolicy="LRU" /> -->
</ehcache>
6. 在spring-mvc.xml 配置文件中開啓shiro註解(不配置的話,無法使用shiro權限註解)
之前在spring-shiro.xml中配置後,不起作用。具體原因沒找到
<!-- AOP 式方法級權限檢查 開啓Shiro的註解(如@RequiresRoles,@RequiresPermissions)-->
<bean id="lifecycleBeanPostProcessor" class="org.apache.shiro.spring.LifecycleBeanPostProcessor"/>
<!-- 切面自動代理:相當於以前的AOP標籤配置 -->
<bean class="org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator"
depends-on="lifecycleBeanPostProcessor">
<!-- 設置aop的代理使用CGLIB代理 -->
<property name="proxyTargetClass" value="true"/>
</bean>
至此,基本的配置就完畢了,剩下的就是對自定義Realm攔截器的編寫。
3.自定義Realm
自定義攔截器分爲兩個部分,一個是登錄認證doGetAuthenticationInfo方法,一個是授權方法doGetAuthorizationInfo。
首先,新建ShiroReaml類,並繼承AuthorizingRealm抽象類,並重寫doGetAuthenticationInfo和doGetAuthorizationInfo方法。
注意:有很多朋友配置好shiro之後,登錄的時候是會走登錄驗證的方法,但是不走權限認證的方法。在此說明一下,當用戶登錄的時候,系統會根據你的配置文件,在用戶登錄時自動調用shiro登錄認證邏輯,但是不會調用授權方法。若想調用授權方法,需要開發人員在接口上使用shiro註解,當程序調用了該接口之後,系統纔會走授權認證的邏輯。
這裏直接貼上我的邏輯,有需要實體類的朋友可以@我
package com.jiahe.common.shiro;
import com.jiahe.system.entity.User;
import com.jiahe.system.model.MenuModal;
import com.jiahe.system.service.*;
import com.jiahe.system.service.impl.UserRoleMemberServiceImpl;
import org.apache.commons.lang3.StringUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.*;
import org.apache.shiro.authz.AuthorizationException;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.springframework.beans.factory.annotation.Autowired;
import java.util.*;
/**
* @auther chenqian
* @date 2018/11/28 13:17
* shiro 攔截器 實現登錄驗證和角色賦權
*/
public class ShiroRealm extends AuthorizingRealm {
private Logger log = LogManager.getLogger();
@Autowired
private UserServiceI userServiceI;
@Autowired
private UserRoleMemberServiceImpl memberServiceI;
@Autowired
private FunctionNodeServiceI nodeServiceI;
@Autowired
private PermisstionRangeServiceI rangeServiceI;
@Autowired
private FeaturePointServiceI pointServiceI;
/**
* 1.登錄驗證
* @param authcToken
* @return
* @throws AuthenticationException
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authcToken)
throws AuthenticationException {
log.info("1.1 ===> 用戶登錄驗證");
UsernamePasswordToken token = (UsernamePasswordToken) authcToken;
log.info("1.2 ===> 獲取當前用戶登錄的 Token :" + token);
String accountName = token.getUsername();
// 讓shiro框架去驗證賬號密碼
if(!StringUtils.isEmpty(accountName)){
User user = userServiceI.findUserByUserName(accountName);
if(user != null) {
log.info("1.3 ===> 登錄驗證成功");
return new SimpleAuthenticationInfo(user.getCode(), user.getPassword(), getName());
}
}
log.info("1.4 ===> 驗證失敗");
return null;
}
/**
* 2.授權
* 從輸入參數principalCollection得到身份信息,
* 根據身份信息到數據庫查找權限信息,
* 將權限信息添加給授權信息對象
* @param principals 身份集合
* @return
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
log.info("2.1 ====> 進入角色授權");
// 待授權用戶
String currentUser = (String)super.getAvailablePrincipal(principals);
// 從用戶表中查找該用戶信息
User user = userServiceI.findUserByUserName(currentUser);
// 權限信息對象,用來存放用戶的所有 角色(role)權限(permission)
SimpleAuthorizationInfo simpleAuthorInfo = new SimpleAuthorizationInfo();
// 查詢用戶角色信息
List<Map> roleInfo = memberServiceI.findRoleIdsByUserId(user.getObjectId());
// 角色名稱
List<String> roleNames = new ArrayList<String>();
// 角色id
List<String> rolesIds = new ArrayList<String>();
if (roleInfo.size() > 0){
for (Map info : roleInfo){
roleNames.add((String) info.get("roleName"));
rolesIds.add((String) info.get("roleId"));
}
log.info("2.2 ====> 用戶角色授權:" + roleNames);
simpleAuthorInfo.addRoles(roleNames);
log.info("2.3 ====> 用戶權限授權:start" );
simpleAuthorInfo.addStringPermissions(this.getUserPermissions(rolesIds,user.getObjectId()));
}else {
log.info("2.4 ====> 授權失敗,用戶不具備角色權限!" );
}
return simpleAuthorInfo;
}
/**
* 授權過程
* @param rolesIds 角色ids
* @return
*/
private Set<String> getUserPermissions(List<String> rolesIds,String userkey){
// 權限
Set<String> permissions = new HashSet<>();
// 菜單信息
List<MenuModal> nodesInfo = nodeServiceI.findRolesNodeByRoleIds(rolesIds,userkey);
// 菜單id
List<String> nodeIds = new ArrayList<String>();
// 角色菜單
for (MenuModal node : nodesInfo){
nodeIds.add(node.getObjectId());
permissions.add("function:node:" + node.getCode());
}
log.info("2.6 ====> 角色菜單權限授權完成");
if (nodeIds.size() > 0){
// 菜單的操作權限
List<Map> points = pointServiceI.getPointsByNodeIds(nodeIds);
for (Map point : points){
permissions.add((String) point.get("nodeCode") + ":menuPoint:" + (String) point.get("pointCode"));
}
// 用戶的操作權限
List<Map> userPoints = pointServiceI.getUserPointsByNodeIdsAndRoleIds(nodeIds, rolesIds);
for (Map point : userPoints){
permissions.add((String) point.get("nodeCode") + ":userPoint:" + (String) point.get("pointCode"));
}
log.info("2.7 ====> 用戶權限授權結束:end" );
}
log.info("2.8 ====> 授權信息集合: " + permissions);
return permissions;
}
//清除緩存
public void clearCached() {
PrincipalCollection principals = SecurityUtils.getSubject().getPrincipals();
super.clearCache(principals);
}
}
4.登錄邏輯
/**
* 用戶登錄
* @param request
* @param userName
* @param usrPwd
* @param capt
*/
@RequestMapping(value = "toLogin", method = RequestMethod.POST)
public void validateLogin(HttpServletRequest request, @RequestParam String userName,
@RequestParam String usrPwd, @RequestParam String capt){
Json json = new Json(true, "登錄成功!");
String sessionCapt = (String) request.getSession().getAttribute(Constants.KAPTCHA_SESSION_KEY);
// 當前Subject
Subject currentUser = SecurityUtils.getSubject();
String md5Pwd = new Md5Hash(usrPwd).toString();
UsernamePasswordToken token = new UsernamePasswordToken(userName, md5Pwd);
// 傳遞token給shiro 的realm
try{
if (!sessionCapt.equals(capt)){
json.setSuccess(false);
json.setMsg("驗證碼錯誤!");
writeJson(json);
return;
}
currentUser.login(token);
}catch (UnknownAccountException uae) {
json.setSuccess(false);
json.setMsg("用戶名不存在!");
}catch (IncorrectCredentialsException ice) {
json.setSuccess(false);
json.setMsg("密碼不正確!");
}catch (Exception ex) {
json.setSuccess(false);
json.setMsg("系統錯誤!");
}
writeJson(json);
}
5. shiro註解用法
在controller中的方法上使用註解,就像之前說的那樣,當程序調用該註解標註的接口時,shiro纔會去走授權認證的邏輯。如果此時緩存中有該用戶的權限,則直接從ehcache中取用戶的權限進行判斷。
/**
* 列表頁面
* @return
*/
@RequiresPermissions("function:node:YHGL") // shiro權限註解
@RequestMapping(value = "list" ,method = RequestMethod.GET)
public ModelAndView orglist() {
mv.setViewName(url+"orglist");
return mv;
}
shiro本身有一套整合了jsp的前端註解,有需要做細粒度控制的朋友可以瞭解下(對按鈕權限的控制),同時支持自定義前端註解,非常長方便。馬上要下班了,就寫到這裏。抽時間補齊SpringBoot整合shiro的案例。