Shiro最佳实践(八)Spring集成与Redis

Apache Shiro
placeholder image
admin 发布于: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

自定义Realm认证

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();
    }
}

shiro-spring配置文件

<?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>


运行结果

image.png

image.png

image.png

image.png

image.png

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,简化示例未使用数据库。

 点赞


 发表评论

当前回复:作者

 评论列表


留言区