Shiro最佳实践(八)Spring集成与Redis
Apache Shiroadmin 发布于:2019-08-04 19:24:44
阅读:loading
终于到这一篇了,也计划用Redis集成去存储用户会话实现分布式session管理为重点实现,在整合过程中个人严格遵循所有技术均采用比较新的版本,所以在此过程中废了相当大的劲儿,比如使用的Spring5的XML配置版,Spring-data-redis-2.0版本及lettuce5.1版本替代jedis实现等,所以本例中关键技术版本信息如下:
shiro 1.4.1
spring 5.0.4
spring-data-redis 2.0.5
jedis 2.9.1
lettuce 5.1.2
虽然在实践过程中较为费劲,但整理出来的这些技术版本相对比较新,可以说未来的几年肯定不会过时,然而这些东西都是作为丰富自己的知识储备,真正使用时肯定会基于SpringBoot最新版再去集成的,但是既然要弄必然是做到在个人能力范围的最优。
功能点包括自定义登录过滤器(自定义form表单、记住我、验证码、错误提示)、Redis集成(使用lettuce替代jedis、Redis-Cache)、Spring-data-redis2.0、自定义Realm认证、分布式Session管理,像登录采用Ajax、Redis中采用JSON序列化、限制单一登录等这些没有实现,采用默认的JDK实现,整个示例在集成Redis时参考了gitee上的项目hunt-admin,主要是集成Redis的SessionDao部分,另外这个项目里面登录时也采用了拖动时验证码,值得拥有,地址为:https://gitee.com/ouyangan/hunt-admin。
package cn.chendd.shiro.examples.commponent;
import ...
/**
* @author chendd
* @date 2019/6/15 1:22
* 自定义认证实现
*/
public class EncryptionRealmRedis extends AuthorizingRealm implements Serializable {
private static final long serialVersionUID = 818010090715013851L;
@Resource
private RedisTemplate<Object, Object> redisTemplate;
@Resource
private ISysUserService sysUserService;
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
SimpleAuthorizationInfo author = new SimpleAuthorizationInfo();
Set<String> roles = new HashSet<>();
roles.add("userManager");
author.setRoles(roles);
Set<String> perms = new HashSet<>();
perms.add("user:queryList");
author.addStringPermissions(perms);
return author;
}
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
UsernamePasswordToken userToken = (UsernamePasswordToken) token;
String userName = userToken.getUsername();
SysUser sysUser = sysUserService.getSysUserByUserName(userName);
if(sysUser == null){
throw new UnknownAccountException();
}
String passWord = sysUser.getPassWord();
if(! "ENABLE".equals(sysUser.getStatus())){
throw new LockedAccountException();
}
String encodePassword = encodePassword(new String(userToken.getPassword()) , userName);
if(!encodePassword.equals(passWord)){
throw new IncorrectCredentialsException();
} else if(false){
//密码错误次数过多
throw new ExcessiveAttemptsException();
}
//SimpleAuthenticationInfo auth = new SimpleAuthenticationInfo(sysUser , token.getCredentials() , this.getName());
SimpleAuthenticationInfo auth = new SimpleAuthenticationInfo(sysUser.getUserName() , token.getCredentials() , this.getName());
return auth;
}
//退出时移除Redis中的数据
@Override
protected void doClearCache(PrincipalCollection principals) {
redisTemplate.delete("shiro-cache-" + principals.getPrimaryPrincipal().toString());
}
public static String encodePassword(Object source, String salt) {
Md5Hash md5 = new Md5Hash(source, salt, 7);
return md5.toHex();
}
}
<?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:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd">
<aop:config proxy-target-class="true"></aop:config>
<bean class="org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor">
<property name="securityManager" ref="securityManager" />
</bean>
<bean id="customLoginFormFilter" class="cn.chendd.shiro.examples.commponent.CustomLoginFormFilter">
<property name="usernameParam" value="userName"/>
<property name="passwordParam" value="passWord"/>
<property name="rememberMeParam" value="remMe"/>
<!-- 可设置登录成功后的响应地址 -->
<!--<property name="successUrl" value="/index" />-->
</bean>
<bean id="customLogoutFilter" class="cn.chendd.shiro.examples.commponent.CustomLogoutFilter"></bean>
<bean id="sessionManager" class="org.apache.shiro.web.session.mgt.DefaultWebSessionManager">
<!--删除由直接关闭浏览器造成的无退出问题-->
<property name="deleteInvalidSessions" value="true" />
<!-- 定时执行session销毁 -->
<property name="sessionValidationScheduler" ref="sessionValidationScheduler"/>
<!--<property name="sessionValidationInterval" value="10000" />--><!-- 该参数未发现明显作用 -->
<!-- 需要让此session可以使用该定时调度器进行检测 -->
<property name="sessionValidationSchedulerEnabled" value="true"/>
<!-- 定义的是全局的session会话超时时间,此cao作会覆盖web.xml文件中的超时时间配置 -->
<property name="globalSessionTimeout" value="#{30 * 60 * 1000}" />
<!-- 所有的session一定要将id设置到Cookie之中,需要提供有Cookie的cao作模版-->
<property name="sessionIdCookie">
<bean class="org.apache.shiro.web.servlet.SimpleCookie">
<constructor-arg value="shiro.session.id.cookie" />
</bean>
</property>
<!-- 定义Session可以进行序列化的工具类 -->
<property name="sessionDAO" ref="sessionDAO"/>
<!-- 定义sessionIdCookie模版可以进行cao作的启用 -->
<property name="sessionIdCookieEnabled" value="true"/>
<!-- 引用session缓存的相关配置 -->
<!--<property name="cacheManager" ref="shiroRedisCacheManager" />-->
</bean>
<bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
<property name="realm" ref="encryptionRealm" />
<!--session过期策略-->
<property name="sessionManager" ref="sessionManager" />
<!--记住我-->
<property name="rememberMeManager" ref="rememberMeManager" />
<!-- 启用session的缓存 -->
<property name="cacheManager" ref="cacheManager" />
</bean>
<bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
<property name="securityManager" ref="securityManager" />
<property name="loginUrl" value="/login" />
<property name="successUrl" value="/index" />
<property name="unauthorizedUrl" value="/unauth" />
<property name="filterChainDefinitions">
<value>
/favicon.ico = anon
/logout = logout
/login = authc
/** = authc
</value>
</property>
<property name="filters">
<map>
<entry key="authc" value-ref="customLoginFormFilter"></entry>
<!--<entry key="logout" value-ref="customLogoutFilter"></entry>-->
</map>
</property>
</bean>
<bean id="rememberCookies" class="org.apache.shiro.web.servlet.SimpleCookie">
<constructor-arg value="rememberMe"></constructor-arg>
<property name="httpOnly" value="true"></property>
<property name="maxAge" value="#{60*60*24}"></property>
</bean>
<!-- 配置记住我管理器 -->
<bean id="rememberMeManager" class="org.apache.shiro.web.mgt.CookieRememberMeManager">
<!-- 加密的base64,末尾拼接以A=结束,此处给的是88888888 -->
<!--<property name="cipherKey" value="#{T(org.apache.shiro.codec.Base64).decode('ODg4ODg4ODgAA==')}"/>-->
<property name="cipherKey" value="#{T(org.apache.shiro.codec.Base64).decode('6ZmI6I2j5Y+R5aSn5ZOlAA==')}"/>
<property name="cookie" ref="rememberCookies"/>
</bean>
<!-- 解决shiro配置的没有权限访问时,unauthorizedUrl不跳转到指定路径的问题 -->
<bean class="org.springframework.web.servlet.handler.SimpleMappingExceptionResolver">
<property name="exceptionMappings">
<props>
<prop key="org.apache.shiro.authz.UnauthorizedException">/unauth</prop>
</props>
</property>
</bean>
<!-- session存储配置 -->
<!-- 定义Session ID生成管理器,默认session.getId生成的规则就是uuid -->
<bean id="sessionIdGenerator" class="org.apache.shiro.session.mgt.eis.JavaUuidSessionIdGenerator" />
<!-- 配置Session DAO的cao作处理,使用内存存储session -->
<bean id="sessionDAO" class="cn.chendd.shiro.examples.commponent.RedisSessionDAO">
<!--<bean id="sessionDAO" class="org.apache.shiro.session.mgt.eis.EnterpriseCacheSessionDAO">-->
<!-- 设置session缓存的名字,这个名字可以任意 -->
<!--<property name="activeSessionsCacheName" value="shiro-activeSessionCache"/>-->
<!-- 定义该Session DAOcao作中所使用的ID生成器 -->
<property name="sessionIdGenerator" ref="sessionIdGenerator"/>
<!--<property name="cacheManager" ref="shiroRedisCacheManager" />-->
</bean>
<!-- 配置session的定时验证检测程序类,以让无效的session释放 -->
<bean id="sessionValidationScheduler"
class="org.apache.shiro.session.mgt.quartz.QuartzSessionValidationScheduler">
<!-- 设置session的失效扫描间隔,会清空掉过期失效session数据,单位为毫秒,默认3600000 -->
<property name="sessionValidationInterval" value="1800000"/>
<!-- 随后还需要定义有一个会话管理器的程序类的引用 -->
<property name="sessionManager" ref="sessionManager"/>
</bean>
<bean id="encryptionRealm" class="cn.chendd.shiro.examples.commponent.EncryptionRealmRedis">
<property name="name" value="encryptionRealm" />
</bean>
<!--spring-data-redis2.0以上的配置-->
<bean id="redisStandaloneConfiguration" class="org.springframework.data.redis.connection.RedisStandaloneConfiguration">
<property name="database" value="7" />
<property name="hostName" value="192.168.244.134" />
<property name="port" value="6379" />
</bean>
<bean id="lettuceConnectionFactory" class="org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory">
<constructor-arg name="config" ref="redisStandaloneConfiguration" />
</bean>
<!--取消注释可采用jedis-->
<!--<bean id="jedisPoolConfig" class="redis.clients.jedis.JedisPoolConfig">-->
<!--<property name="maxIdle" value="10"/>-->
<!--<property name="maxTotal" value="8"/>-->
<!--<property name="maxWaitMillis" value="60000"/>-->
<!--<property name="testOnBorrow" value="true"/>-->
<!--<property name="testOnReturn" value="true"/>-->
<!--</bean>-->
<!-- JedisConnectionFactory -->
<!--取消注释可采用jedis-->
<!--<bean id="jedisConnectionFactory" class="org.springframework.data.redis.connection.jedis.JedisConnectionFactory">-->
<!--<property name="hostName" value="192.168.244.134"/>-->
<!--<property name="port" value="6379"/>-->
<!--<property name="database" value="7" />-->
<!--<property name="poolConfig" ref="jedisPoolConfig"/>-->
<!--</bean>-->
<bean id="redisTemplate" class="org.springframework.data.redis.core.RedisTemplate">
<!--取消注释可采用jedis-->
<!--<property name="connectionFactory" ref="jedisConnectionFactory"></property>-->
<property name="connectionFactory" ref="lettuceConnectionFactory"></property>
<property name="defaultSerializer">
<bean class="org.springframework.data.redis.serializer.JdkSerializationRedisSerializer"/>
</property>
<!--定义key默认序列化策略-->
<property name="keySerializer">
<bean class="org.springframework.data.redis.serializer.StringRedisSerializer"/>
</property>
<!--定义value默认策略-->
<property name="valueSerializer">
<bean class="org.springframework.data.redis.serializer.JdkSerializationRedisSerializer"/>
</property>
</bean>
<bean id="fastJsonRedisSerializer" class="com.alibaba.fastjson.support.spring.FastJsonRedisSerializer">
<constructor-arg name="type" value="java.lang.Object" />
</bean>
<bean id="cacheManager" class="cn.chendd.shiro.examples.commponent.MyRedisCacheManager">
<property name="redisTemplate" ref="redisTemplate" />
</bean>
<!-- 激活spring 缓存注解 -->
<!--<cache:annotation-driven cache-manager="springCacheManager" />-->
<bean id="genericFastJsonRedisSerializer" class="com.alibaba.fastjson.support.spring.GenericFastJsonRedisSerializer"></bean>
<bean id="stringRedisSerializer" class="org.springframework.data.redis.serializer.StringRedisSerializer"></bean>
<bean id="genericJackson2JsonRedisSerializer" class="org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer"></bean>
<bean id="jackson2JsonRedisSerializer" class="org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer">
<constructor-arg name="type" value="java.lang.Object"></constructor-arg>
</bean>
</beans>
shiro往深了整理也实在是太多了,更多的深入的将在SpringBoot的时候再进行整理了,比如说它在JSP中使用自定义标签判定角色和权限的方式个人也不太喜欢,因为这种方式最终还是需要做功能上的权限拦截,而且现在的JSP也不是以往的JSP,在开发中也被越来越少使用了,所以本次重点关注多的是后台功能上的拦截(启用Shiro的AOP拦截)。
至此回忆一下整个shiro的相关知识点吧,想起多少先写上,后续再补充。
1、自定义Realm类的doGetAuthenticationInfo函数,会在登录的时候调用,进行用户验证,当用户不存在、账号密码错误、用户被禁用、密码加密方式等都有相应的配置与实现,错误提示也是如此;
2、自定义Realm类的doGetAuthorizationInfo函数,是每次权限验证时触发,如果不使用缓存,该函数会被频繁性的多次调用,调用方式有shiro自定义标签权限验证、Aop注解拦截等场景;
3、自定义Realm类的doGetAuthenticationInfo函数,返回值如果存储的是用户实体对象类型时,在前台配合显示时出现了ClassCastException异常,最后通过存储的用户名解决;
4、遇到最频繁的问题,也是卡了我非常非常多时间的问题,就是这个ClassCastException异常问题了,主要是使用序列化时存储的对象与反序列化回来的对象不一致导致,所以这个SessionDao模块的实现非常重要;
https://gitee.com/88911006/chendd-examples
shiro-basic为普通Java类型项目示例;
shiro-web为Web类型项目示例;
shiro-spring为整合Spring的XML配置项目示例;
登录用户:chendd/chendd123或sunm/sunm123,简化示例未使用数据库。
点赞