最近一直想总结下最近学习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的案例。