Shiro 分布式架构下 Session 的共享实现

参考资料:http://blog.csdn.net/lishehe/article/details/45223823

  • 说在前面:
    共享的方式有很多,传统的做法是通过配置 web 容器,通过容器间 session 的复制达到共享的目的(不推荐),现在常用的做法是通过单独存储session达到共享目的,将session存储到 Mysql 、Memcache、Redis中,等到使用的时候再从中取出来即可。由于各种存储载体本身的限制,大家可以根据具体情况采用不同实现方案,这里介绍 Redis 的实现方案。

  • 非集成下的配置

<!-- 保证实现了 Shiro 内部 lifecycle 函数的 bean 执行 -->
    <bean id="lifecycleBeanPostProcessor" class="org.apache.shiro.spring.LifecycleBeanPostProcessor" />

<!-- 用户授权信息Cache(本机内存实现)-->
    <bean id="cacheManager" class="org.apache.shiro.cache.MemoryConstrainedCacheManager"/>

    <!-- shiro 的自带 ehcahe 缓存管理器 --> 
    <bean id="cacheManager" class="org.springframework.cache.ehcache.EhCacheCacheManager">
        <property name="cacheManagerConfigFile" value="classpath:config/shiro/ehcache-shiro.xml"/>
    </bean>

    <!--自定义Realm -->
    <bean id="myRealm" class="com.system.shiro.MyRealm"/>

<!-- 凭证匹配器 -->
    <bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
        <property name="realm" ref="myRealm" />
        <!-- redis 缓存 -->
        <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="/index.jsp" />    
    <property name="successUrl" value="/loginSuccess.shtml" />    
    <property name="filterChainDefinitions">    
        <value>    
            <!-- 静态资源放行 -->
            /statics/** = anon 
            /common/** = anon 
            /error/** = anon 
            <!-- 登录资源放行 -->
            /toLogin/** = anon
            /login/** = anon
            <!-- shiro 自带登出 -->
            /logout = logout                
        </value>    
    </property>    
</bean>

上面是 shiro 非集群下的配置,DefaultWebSecurityManager 类不需要注入sessionManager 属性,它会使用默认的 ServletContainerSessionManager 作为sessionManager 。如下图

这里写图片描述

setSessionManager 默认设置 servlet容器实现的sessionManager,sessionManager 会管理 session 的创建、删除等等。如果我们需要让 session 在集群中共享,就需要替换这个默认的 sessionManager。官网原话如下:

If you want your session configuration settings and clustering to be portable across servlet containers    
    (e.g. Jetty in testing, but Tomcat or JBoss in production), or you want to control specific session/clustering     
    features, you can enable Shiro's native session management.    

    The word 'Native' here means that Shiro's own enterprise session management implementation will be used to support     
    all Subject and HttpServletRequest sessions and bypass the servlet container completely. But rest assured - Shiro     
    implements the relevant parts of the Servlet specification directly so any existing web/http related code works as     
    expected and never needs to 'know' that Shiro is transparently managing sessions.    

    **DefaultWebSessionManager**    

    To enable native session management for your web application, you will need to configure a native web-capable     
    session manager to override the default servlet container-based one. You can do that by configuring an instance of     
    DefaultWebSessionManager on Shiro's SecurityManager. 

我们可以看到如果要用集群,就需要用本地会话,这里 shiro 给我准备了一个默认的native session manager,DefaultWebSessionManager,所以我们要修改 spring 配置文件,注入 DefaultWebSessionManager。我们继续看DefaultWebSessionManager的源码,发现其父类 DefaultSessionManager 中有sessionDAO 属性,这个属性是真正实现了session储存的类,这个就是我们自己实现的 redis session的储存类。

package com.system.shiro;

import java.io.Serializable;
import java.util.Collection;
import java.util.HashSet;
import java.util.Set;

import org.apache.shiro.session.Session;
import org.apache.shiro.session.UnknownSessionException;
import org.apache.shiro.session.mgt.eis.AbstractSessionDAO;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.system.utils.RedisManager;
import com.system.utils.SerializerUtil;

public class RedisSessionDao extends AbstractSessionDAO {

    private Logger logger = LoggerFactory.getLogger(this.getClass());

    private RedisManager redisManager;

    /** 
     * The Redis key prefix for the sessions  
     */  
    private static final String KEY_PREFIX = "shiro_redis_session:"; 

    @Override
    public void update(Session session) throws UnknownSessionException {
        this.saveSession(session);
    }

    @Override
    public void delete(Session session) {
        if (session == null || session.getId() == null) {
            logger.error("session or session id is null");
            return;
        }
        redisManager.del(KEY_PREFIX + session.getId());
    }

    @Override
    public Collection<Session> getActiveSessions() {
        Set<Session> sessions = new HashSet<Session>();  
        Set<byte[]> keys = redisManager.keys(KEY_PREFIX + "*");  
        if(keys != null && keys.size()>0){  
            for(byte[] key : keys){  
                Session s = (Session)SerializerUtil.deserialize(redisManager.get(SerializerUtil.deserialize(key)));  
                sessions.add(s);  
            } 
        }
        return sessions;  
    }

    @Override
    protected Serializable doCreate(Session session) {
        Serializable sessionId = this.generateSessionId(session);
        this.assignSessionId(session, sessionId);
        this.saveSession(session);
        return sessionId;
    }

    @Override
    protected Session doReadSession(Serializable sessionId) {
         if(sessionId == null){  
                logger.error("session id is null");  
                return null;  
            }  

            Session s = (Session)redisManager.get(KEY_PREFIX + sessionId);  
            return s;  
    }

    private void saveSession(Session session) throws UnknownSessionException{
        if (session == null || session.getId() == null) {
            logger.error("session or session id is null");  
            return;
        }
        //设置过期时间
        long expireTime = 1800000l;
        session.setTimeout(expireTime);
        redisManager.setEx(KEY_PREFIX + session.getId(), session, expireTime);
    }

    public void setRedisManager(RedisManager redisManager) {
        this.redisManager = redisManager;
    }

    public RedisManager getRedisManager() {
        return redisManager;
    }
}

使用到的工具类如下:

RedisManager.java

package com.system.utils;

import java.io.Serializable;
import java.util.Set;

import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.dao.DataAccessException;
import org.springframework.data.redis.connection.RedisConnection;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;

@Component
public class RedisManager {

    @Autowired
    private RedisTemplate<Serializable, Serializable> redisTemplate;

    /**
     * 过期时间
     */
//  private Long expire;

    /**
     * 添加缓存数据(给定key已存在,进行覆盖)
     * @param key
     * @param obj
     * @throws DataAccessException
     */
    public <T> void set(String key, T obj) throws DataAccessException{
        final byte[] bkey = key.getBytes();
        final byte[] bvalue = SerializerUtil.serialize(obj);
        redisTemplate.execute(new RedisCallback<Void>() {
            @Override
            public Void doInRedis(RedisConnection connection) throws DataAccessException {
                connection.set(bkey, bvalue);
                return null;
            }
        });
    }

    /**
     * 添加缓存数据(给定key已存在,不进行覆盖,直接返回false)
     * @param key
     * @param obj
     * @return 操作成功返回true,否则返回false
     * @throws DataAccessException
     */
    public <T> boolean setNX(String key, T obj) throws DataAccessException{
        final byte[] bkey = key.getBytes();
        final byte[] bvalue = SerializerUtil.serialize(obj);
        boolean result = redisTemplate.execute(new RedisCallback<Boolean>() {
            @Override
            public Boolean doInRedis(RedisConnection connection) throws DataAccessException {
                return connection.setNX(bkey, bvalue);
            }
        });

        return result;
    }

    /**
     * 添加缓存数据,设定缓存失效时间
     * @param key
     * @param obj
     * @param expireSeconds 过期时间,单位 秒
     * @throws DataAccessException
     */
    public <T> void setEx(String key, T obj, final long expireSeconds) throws DataAccessException{
        final byte[] bkey = key.getBytes();
        final byte[] bvalue = SerializerUtil.serialize(obj);
        redisTemplate.execute(new RedisCallback<Boolean>() {
            @Override
            public Boolean doInRedis(RedisConnection connection) throws DataAccessException {
                connection.setEx(bkey, expireSeconds, bvalue);
                return true;
            }
        });
    }

    /**
     * 获取key对应value
     * @param key
     * @return
     * @throws DataAccessException
     */
    public <T> T get(final String key) throws DataAccessException{
        byte[] result = redisTemplate.execute(new RedisCallback<byte[]>() {
            @Override
            public byte[] doInRedis(RedisConnection connection) throws DataAccessException {
                return connection.get(key.getBytes());
            }
        });
        if (result == null) {
            return null;
        }
        return SerializerUtil.deserialize(result);
    }

    /**
     * 删除指定key数据
     * @param key
     * @return 返回操作影响记录数
     */
    public Long del(final String key){
        if (StringUtils.isEmpty(key)) {
            return 0l;
        }
        Long delNum = redisTemplate.execute(new RedisCallback<Long>() {
            @Override
            public Long doInRedis(RedisConnection connection) throws DataAccessException {
                byte[] keys = key.getBytes();
                return connection.del(keys);
            }
        });
        return delNum;
    }

    public Set<byte[]> keys(final String key){
        if (StringUtils.isEmpty(key)) {
            return null;
        }
        Set<byte[]> bytesSet = redisTemplate.execute(new RedisCallback<Set<byte[]>>() {
            @Override
            public Set<byte[]> doInRedis(RedisConnection connection) throws DataAccessException {
                byte[] keys = key.getBytes();
                return connection.keys(keys);
            }
        });

        return bytesSet;
    }

}

SerializerUtil.java

package com.system.utils;

import org.springframework.data.redis.serializer.JdkSerializationRedisSerializer;

/**
 * 序列化工具类
 * @author HandyZcy
 *
 */
public class SerializerUtil {

    private static final JdkSerializationRedisSerializer jdkSerializationRedisSerializer = new JdkSerializationRedisSerializer();

    /**
     * 序列化对象
     * @param obj
     * @return
     */
    public static <T> byte[] serialize(T obj){
        try {
            return jdkSerializationRedisSerializer.serialize(obj);
        } catch (Exception e) {
            throw new RuntimeException("序列化失败!", e);
        }
    }

    /**
     * 反序列化对象
     * @param bytes 字节数组
     * @param cls cls
     * @return
     */
    @SuppressWarnings("unchecked")
    public static <T> T deserialize(byte[] bytes){
        try {
            return (T) jdkSerializationRedisSerializer.deserialize(bytes);
        } catch (Exception e) {
            throw new RuntimeException("反序列化失败!", e);
        }
    }
}

整体配置文件如下:

<?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:context="http://www.springframework.org/schema/context"
    xmlns:util="http://www.springframework.org/schema/util"
    xsi:schemaLocation="http://www.springframework.org/schema/beans  
    http://www.springframework.org/schema/beans/spring-beans-4.3.xsd
    "
    >

    <description>Shiro安全配置</description>

    <!-- 分布式 配置参考:http://blog.csdn.net/lishehe/article/details/45223823 -->

    <!-- 保证实现了 Shiro 内部 lifecycle 函数的 bean 执行 -->
    <bean id="lifecycleBeanPostProcessor" class="org.apache.shiro.spring.LifecycleBeanPostProcessor" />

    <!-- 
    用户授权信息Cache(本机内存实现)
    <bean id="cacheManager" class="org.apache.shiro.cache.MemoryConstrainedCacheManager"/>
     -->

    <!-- shiro 的自带 ehcahe 缓存管理器
    <bean id="cacheManager" class="org.springframework.cache.ehcache.EhCacheCacheManager">
        <property name="cacheManagerConfigFile" value="classpath:config/shiro/ehcache-shiro.xml"/>
    </bean>
     --> 

    <!-- 自定义cacheManager -->  
    <bean id="redisCache" class="com.system.shiro.RedisCache">  
        <constructor-arg ref="redisManager"></constructor-arg>  
    </bean>

     <!-- 自定义redisManager-redis -->  
    <bean id="redisCacheManager" class="com.system.shiro.RedisCacheManager">  
        <property name="redisManager" ref="redisManager" />  
    </bean>  

    <!--自定义Realm -->
    <bean id="myRealm" class="com.system.shiro.MyRealm"/>

    <!-- 凭证匹配器 -->
    <bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
        <property name="realm" ref="myRealm" />
        <property name="sessionMode" value="http" />
        <property name="sessionManager" ref="defaultWebSessionManager" />

        <!-- redis 缓存 -->
        <property name="cacheManager" ref="redisCacheManager" />
    </bean>

    <bean id="defaultWebSessionManager" class="org.apache.shiro.web.session.mgt.DefaultWebSessionManager">    

        <!-- session存储的实现 -->  
        <property name="sessionDAO" ref="shiroRedisSessionDAO" />  

        <!-- sessionIdCookie的实现,用于重写覆盖容器默认的JSESSIONID -->  
        <property name="sessionIdCookie" ref="shareSession" />

        <!-- 设置全局会话超时时间,默认30分钟(1800000) -->  
        <property name="globalSessionTimeout" value="1800000" />  

        <!-- 是否在会话过期后会调用SessionDAO的delete方法删除会话 默认true -->  
        <property name="deleteInvalidSessions" value="true" />  

        <!-- 会话验证器调度时间 -->  
        <property name="sessionValidationInterval" value="1800000" />  

        <!-- 定时检查失效的session -->  
        <property name="sessionValidationSchedulerEnabled" value="true" />     
    </bean>

    <!--
        通过@Component 注解交由 Spring IOC 管理 
    <bean id="redisManager" class="com.system.utils.RedisManager"></bean>
     -->

    <!-- session会话存储的实现类 -->
    <bean id="shiroRedisSessionDAO" class="com.system.shiro.RedisSessionDao">
        <property name="redisManager" ref="redisManager"/>
    </bean>

    <!-- sessionIdCookie的实现,用于重写覆盖容器默认的JSESSIONID -->  
    <bean id="shareSession" class="org.apache.shiro.web.servlet.SimpleCookie">  
        <!-- cookie的name,对应的默认是 JSESSIONID -->  
        <constructor-arg name="name" value="SHAREJSESSIONID" />  
        <!-- jsessionId的path为 / 用于多个系统共享jsessionId -->  
        <property name="path" value="/" />  
        <property name="httpOnly" value="true"/>  
    </bean>

    <!-- 配置shiro的过滤器工厂类,id- shiroFilter要和我们在web.xml中配置的过滤器一致 -->
    <bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
        <!-- Shiro的核心安全接口,这个属性是必须的 -->
        <property name="securityManager" ref="securityManager" />
        <!-- 要求登录时的链接,非必须的属性,默认会自动寻找Web工程根目录下的"/login.jsp"页面 -->
        <property name="loginUrl" value="/index.jsp" />
        <!-- 登录成功后要跳转的连接 -->
        <property name="successUrl" value="/loginSuccess.shtml" />
        <!-- 用户访问未对其授权的资源时,所显示的连接 -->
        <property name="unauthorizedUrl" value="/error/forbidden.jsp" />
        <!-- 自定义权限配置:url 过滤在这里做 -->
        <property name="filterChainDefinitions">
            <!-- 参考:http://blog.csdn.net/jadyer/article/details/12172839 -->
            <!-- 
                Shiro验证URL时,URL匹配成功便不再继续匹配查找(所以要注意配置文件中的URL顺序,尤其在使用通配符时)故filterChainDefinitions的配置顺序为自上而下,以最上面的为准 
            -->
            <!-- Pattern里用到的是两颗星,这样才能实现任意层次的全匹配 -->
            <value>
                <!-- 静态资源放行 -->
                /statics/** = anon 
                /common/** = anon 
                /error/** = anon 

                <!-- 登录资源放行 -->
                /toLogin/** = anon
                /login/** = anon

                <!-- shiro 自带登出 -->
                /logout = logout

                <!-- 表示用户必需已通过认证,并拥有 superman 角色 && superman:role:list 权限才可以正常发起'/role'请求-->
                /role/** = authc,roles[superman],perms[superman:role:list]
                /right/** = authc,roles[superman],perms[superman:right:list]
                /manager/preEditPwd = authc
                /manager/editUserBase = authc
                <!-- 表示用户必需已通过认证,并拥有 superman 角色 && superman:manager:list 才可以正常发起'/manager'请求 -->
                /manager/** = authc,roles[superman],perms[superman:manager:list]
                /** = authc
            </value>
        </property>
    </bean>

    <!-- 开启Shiro的注解(如@RequiresRoles,@RequiresPermissions),需借助SpringAOP扫描使用Shiro注解的类,并在必要时进行安全逻辑验证 -->  
    <!-- 配置以下两个bean即可实现此功能 -->  
    <!-- Enable Shiro Annotations for Spring-configured beans. Only run after the lifecycleBeanProcessor has run -->  
    <bean class="org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator" depends-on="lifecycleBeanPostProcessor"/>  
    <bean class="org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor">  
        <property name="securityManager" ref="securityManager"/>  
    </bean>  
</beans>

如果感觉本文写的不太好,可参考LZ参考的博客,写的非常棒,只是缺少了几个工具类,我在这里给出了自己的实现。如有问题请留言,LZ 尽快做出调整。

评论 8 您还未登录,请先 登录 后发表或查看评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
©️2022 CSDN 皮肤主题:大白 设计师:CSDN官方博客 返回首页

打赏作者

uncleCG

你的鼓励将是我创作的最大动力

¥2 ¥4 ¥6 ¥10 ¥20
输入1-500的整数
余额支付 (余额:-- )
扫码支付
扫码支付:¥2
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、C币套餐、付费专栏及课程。

余额充值