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

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

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

  • 非集成下的配置


    <bean id="lifecycleBeanPostProcessor" class="org.apache.shiro.spring.LifecycleBeanPostProcessor" />


    <bean id="cacheManager" class="org.apache.shiro.cache.MemoryConstrainedCacheManager"/>

     
    <bean id="cacheManager" class="org.springframework.cache.ehcache.EhCacheCacheManager">
        <property name="cacheManagerConfigFile" value="classpath:config/shiro/ehcache-shiro.xml"/>
    bean>

    
    <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="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
            
            /logout = logout                
        value>    
    property>    
bean>

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

Shiro 分布式架构下 Session 的共享实现_第1张图片

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 getActiveSessions() {
        Set sessions = new HashSet();  
        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 redisTemplate;

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

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

        return result;
    }

    /**
     * 添加缓存数据,设定缓存失效时间
     * @param key
     * @param obj
     * @param expireSeconds 过期时间,单位 秒
     * @throws DataAccessException
     */
    public  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() {
            @Override
            public Boolean doInRedis(RedisConnection connection) throws DataAccessException {
                connection.setEx(bkey, expireSeconds, bvalue);
                return true;
            }
        });
    }

    /**
     * 获取key对应value
     * @param key
     * @return
     * @throws DataAccessException
     */
    public  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() {
            @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 RedisCallbackbyte[]>>() {
            @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  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 deserialize(byte[] bytes){
        try {
            return (T) jdkSerializationRedisSerializer.deserialize(bytes);
        } catch (Exception e) {
            throw new RuntimeException("反序列化失败!", e);
        }
    }
}

整体配置文件如下:


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

    

    
    <bean id="lifecycleBeanPostProcessor" class="org.apache.shiro.spring.LifecycleBeanPostProcessor" />

    

     

      
    <bean id="redisCache" class="com.system.shiro.RedisCache">  
        <constructor-arg ref="redisManager">constructor-arg>  
    bean>

       
    <bean id="redisCacheManager" class="com.system.shiro.RedisCacheManager">  
        <property name="redisManager" ref="redisManager" />  
    bean>  

    
    <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" />

        
        <property name="cacheManager" ref="redisCacheManager" />
    bean>

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

          
        <property name="sessionDAO" ref="shiroRedisSessionDAO" />  

          
        <property name="sessionIdCookie" ref="shareSession" />

          
        <property name="globalSessionTimeout" value="1800000" />  

          
        <property name="deleteInvalidSessions" value="true" />  

          
        <property name="sessionValidationInterval" value="1800000" />  

          
        <property name="sessionValidationSchedulerEnabled" value="true" />     
    bean>

    

    
    <bean id="shiroRedisSessionDAO" class="com.system.shiro.RedisSessionDao">
        <property name="redisManager" ref="redisManager"/>
    bean>

      
    <bean id="shareSession" class="org.apache.shiro.web.servlet.SimpleCookie">  
          
        <constructor-arg name="name" value="SHAREJSESSIONID" />  
          
        <property name="path" value="/" />  
        <property name="httpOnly" value="true"/>  
    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="unauthorizedUrl" value="/error/forbidden.jsp" />
        
        <property name="filterChainDefinitions">
            
            
            
            <value>
                
                /statics/** = anon 
                /common/** = anon 
                /error/** = anon 

                
                /toLogin/** = anon
                /login/** = anon

                
                /logout = logout

                
                /role/** = authc,roles[superman],perms[superman:role:list]
                /right/** = authc,roles[superman],perms[superman:right:list]
                /manager/preEditPwd = authc
                /manager/editUserBase = authc
                
                /manager/** = authc,roles[superman],perms[superman:manager:list]
                /** = authc
            value>
        property>
    bean>

      
      
      
    <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 尽快做出调整。

你可能感兴趣的:(JavaWeb)