spring boot+redis+shrio+会话

之前已经在几个项目中使用过shiro,可以说对shiro已经有了一定的了解,但是最近在处理一个shiro项目的问题时却遇到了空前的挑战。


问题描述:用户登录后用户用着用着就突然自动退出了,而且没有任何规律,有的测试同事反馈一天也不会出现1次,但是有的时候却发现经常自动退出,完全没有看出来任何规律。

问题分析处理步骤:

1、检查httpwatch请求记录;
        发现有的时候用户的sessionid突然就变了;

2、怀疑是nginx配置问题(由于新参与这个项目,环境啥的都不了解)
    经和运维沟通了解到当前系统是单点、而且nginx已经配置了会话保持,也就是说不存在乱跳的问题。

3、检查了几遍代码理论上程序不会造成这个问题,苦逼啊。然后就在系统中添加了session监控日志;

在程序入口中添加注解:@ServletComponentScan

@WebListener
@Log
public class SessionListener implements HttpSessionListener {


    public void sessionCreated(HttpSessionEvent arg0) {
// TODO Auto-generated method stub
        log.info("createSession:"+arg0.getSession().getId());
    }


    public void sessionDestroyed(HttpSessionEvent arg0) {
        log.info("destoryedSession:"+arg0.getSession().getId());
    }
}

经检测发现有的会话几分钟就过期了,之前session使用的系统默认的超时时间,虽然感觉不应该是session会话时间配置的问题,但是还是抱着试试看的态度改了下;最后事实证明的确没卵关系。

4、奇怪的问题又发生了,有的时候用户的sessionid虽然没有变,但是session里边的用户信息却丢失了,我靠瞬间凌乱了。后来同事提醒是不是会话污染导致的,然后又是一顿折腾。

在shiroconfig类中配置会话(这里只列出部分代码,后边会给出完整代码):


@Bean(name = "sessionIdCookie")
    public SimpleCookie getSessionIdCookie() {
        SimpleCookie cookie = new SimpleCookie("mysid");
        cookie.setHttpOnly(true);
        cookie.setMaxAge(-1);
        return cookie;
    }
@Bean(name = "sessionManager")
    public SessionManager getSessionManage() {
        DefaultWebSessionManager sessionManager = new DefaultWebSessionManager();
        sessionManager.setGlobalSessionTimeout(3600000);
        sessionManager.setSessionValidationScheduler(getExecutorServiceSessionValidationScheduler());
        sessionManager.setSessionValidationSchedulerEnabled(true);
        sessionManager.setDeleteInvalidSessions(true);
        sessionManager.setSessionIdCookieEnabled(true);
        sessionManager.setSessionIdCookie(getSessionIdCookie());
      //  EnterpriseCacheSessionDAO cacheSessionDAO = new RedisSessionDAO();
        sessionManager.setCacheManager(redisCacheManager());
        sessionManager.setSessionDAO(redisSessionDAO());
        // -----可以添加session 创建、删除的监听器

        return sessionManager;
    }

经测试然而并没有什么卵用,坑爹啊。
5、和原来集成过shiro的项目比对,发现原来的使用的是war包部署,但是这个项目使用的是jar包部署,因此怀疑是不是spring boot内嵌的tomcat容器有啥问题,当跟领导表示要试试修改下容器的时候直接被否了,因此就没有进行测试【个人感觉这种可能性也是存在的,如果以后有也人遇到这种问题可以试试这个方案】。

6、绞尽乳汁的思考问题的可能性,有那么些时刻感觉自己真是黔驴技穷了,这个问题会不会自己处理不了给领导留下不好的印象【刚入职,很尴尬啊】。到此这个问题我已经出来了将近一周了。
6、仔细思考问题的可能原因,既然会话仍在session里边会有问题,那放在别的地方呢?然后就想到了放在redis中试试,说干就干,以下为完整代码。

spring boot+redis+shrio+会话

ShiroConfiguration2

package com.liao.configuration;

import com.liao.shiro.*;
import com.liao.util.PasswordHelper;
import lombok.Data;
import org.apache.shiro.authc.credential.HashedCredentialsMatcher;
import org.apache.shiro.session.mgt.ExecutorServiceSessionValidationScheduler;
import org.apache.shiro.session.mgt.SessionManager;
import org.apache.shiro.spring.LifecycleBeanPostProcessor;
import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.CookieRememberMeManager;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.apache.shiro.web.servlet.SimpleCookie;
import org.apache.shiro.web.session.mgt.DefaultWebSessionManager;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.core.RedisTemplate;

import java.util.LinkedHashMap;
import java.util.Map;
@Data
@Configuration
public class ShiroConfiguration2 {


    @Autowired
    private RedisTemplate redisTemplate;

    private static final Logger logger = LoggerFactory.getLogger(ShiroConfiguration.class);

    public RedisSessionDAO  redisSessionDAO(){
        RedisSessionDAO  sessionDAO=new RedisSessionDAO();
        sessionDAO.redisTemplate=redisTemplate;
        return sessionDAO;
    }

    @Bean
    public RedisCacheManager redisCacheManager() {
        RedisCacheManager manager=new RedisCacheManager();
        manager.setRedisTemplate(redisTemplate);
        return manager;
    }

    @Bean(name = "myShiroRealm")
    public MyShiroRealm myShiroRealm(RedisCacheManager redisCacheManager) {
        MyShiroRealm realm = new MyShiroRealm();
        realm.setCacheManager(redisCacheManager);
        return realm;
    }
    /**
    *在此重点说明这个方法,如果不设置为静态方法会导致bean对象无法注入进来,
    *我被这个问题坑的想死的心都有了,晚上感到4点多
    *我是在这篇博客里找到答案的:
    *http://blog.csdn.net/wuxuyang_7788/article/details/70141812
    */
    @Bean(name = "lifecycleBeanPostProcessor")
    public static LifecycleBeanPostProcessor getLifecycleBeanPostProcessor() {
        return new LifecycleBeanPostProcessor();
    }

    @Bean
    public DefaultAdvisorAutoProxyCreator getDefaultAdvisorAutoProxyCreator() {
        DefaultAdvisorAutoProxyCreator daap = new DefaultAdvisorAutoProxyCreator();
        daap.setProxyTargetClass(true);
        return daap;
    }

    @Bean(name = "securityManager")
    public DefaultWebSecurityManager securityManager(MyShiroRealm myShiroRealm) {
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        securityManager.setRealm(myShiroRealm);
        securityManager.setSessionManager(getSessionManage());
        securityManager.setCacheManager(redisCacheManager());
        //配置记住我
        securityManager.setRememberMeManager(getCookieRememberMeManager());
        return securityManager;
    }

    @Bean(name = "sessionManager")
    public SessionManager getSessionManage() {
        DefaultWebSessionManager sessionManager = new DefaultWebSessionManager();
        sessionManager.setGlobalSessionTimeout(3600000);
        sessionManager.setSessionValidationScheduler(getExecutorServiceSessionValidationScheduler());
        sessionManager.setSessionValidationSchedulerEnabled(true);
        sessionManager.setDeleteInvalidSessions(true);
        sessionManager.setSessionIdCookieEnabled(true);
        sessionManager.setSessionIdCookie(getSessionIdCookie());
      //  EnterpriseCacheSessionDAO cacheSessionDAO = new RedisSessionDAO();
        sessionManager.setCacheManager(redisCacheManager());
        sessionManager.setSessionDAO(redisSessionDAO());
        // -----可以添加session 创建、删除的监听器

        return sessionManager;
    }


    @Bean(name = "sessionValidationScheduler")
    public ExecutorServiceSessionValidationScheduler getExecutorServiceSessionValidationScheduler() {
        ExecutorServiceSessionValidationScheduler scheduler = new ExecutorServiceSessionValidationScheduler();
        scheduler.setInterval(900000);
        return scheduler;
    }



    @Bean
    public AuthorizationAttributeSourceAdvisor getAuthorizationAttributeSourceAdvisor(DefaultWebSecurityManager securityManager) {
        AuthorizationAttributeSourceAdvisor aasa = new AuthorizationAttributeSourceAdvisor();
        aasa.setSecurityManager(securityManager);
        return aasa;
    }

    /**
     * 加载shiroFilter权限控制规则
     *
     */
    private void loadShiroFilterChain(ShiroFilterFactoryBean shiroFilterFactoryBean) {
        /////////////////////// 下面这些规则配置最好配置到配置文件中 ///////////////////////
        Map<String, String> filterChainDefinitionMap = new LinkedHashMap<String, String>();
        // authc:该过滤器下的页面必须验证后才能访问,它是Shiro内置的一个拦截器org.apache.shiro.web.filter.authc.FormAuthenticationFilter
//        // anon:它对应的过滤器里面是空的,什么都没做
//        logger.info("##################从数据库读取权限规则,加载到shiroFilter中##################");
//        filterChainDefinitionMap.put("/user/edit/**", "authc,perms[user:edit]");// 这里为了测试,固定写死的值,也可以从数据库或其他配置中读取

        filterChainDefinitionMap.put("/user/login", "anon");
        filterChainDefinitionMap.put("/user/logout", "anon");
        filterChainDefinitionMap.put("/user/authcode", "anon");
        filterChainDefinitionMap.put("/user/admin", "anon");
        //filterChainDefinitionMap.put("/user/**", "authc");// 这里为了测试,只限制/user,实际开发中请修改为具体拦截的请求规则
        //这个配置可以理解为拦截,authFilter主要用于拦截未登录请求,主动返回规范JSON;user标识如果启用了记住我,则自动登录
        filterChainDefinitionMap.put("/**", "authFilter,user");//anon 可以理解为不拦截
        //filterChainDefinitionMap.put("/**", "anon");//anon 可以理解为不拦截

        shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
        //shiroFilterFactoryBean.getFilters().put("jCaptchaValidate", getJCaptchaValidateFilter());
    }


    @Bean(name = "shiroFilter")
    public ShiroFilterFactoryBean getShiroFilterFactoryBean(DefaultWebSecurityManager securityManager) {

        ShiroFilterFactoryBean shiroFilterFactoryBean = new MShiroFilterFactoryBean();
        // 必须设置 SecurityManager
        shiroFilterFactoryBean.setSecurityManager(securityManager);
        // 如果不设置默认会自动寻找Web工程根目录下的"/login.jsp"页面
        shiroFilterFactoryBean.setLoginUrl("/pyramid/user/nologin");
        // 登录成功后要跳转的连接
        shiroFilterFactoryBean.setSuccessUrl("/user");
        shiroFilterFactoryBean.setUnauthorizedUrl("/403");
        shiroFilterFactoryBean.getFilters().put("authFilter",new AuthcFilter());


        loadShiroFilterChain(shiroFilterFactoryBean);
        return shiroFilterFactoryBean;
    }


    /**
     * @return
     */
    @Bean(name = "credentialsMatcher")
    public HashedCredentialsMatcher getHashedCredentialsMatcher() {
        HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
        hashedCredentialsMatcher.setHashAlgorithmName(PasswordHelper.algorithmName);
        hashedCredentialsMatcher.setHashIterations(PasswordHelper.hashIterations);
        hashedCredentialsMatcher.setStoredCredentialsHexEncoded(true);
        return hashedCredentialsMatcher;
    }


    @Bean(name = "rememberMeCookie")
    public SimpleCookie getRememberMeCookie() {
        SimpleCookie simpleCookie = new SimpleCookie("rememberMe");
        simpleCookie.setHttpOnly(true);
        simpleCookie.setMaxAge(2592000);//30天
        return simpleCookie;
    }


    @Bean(name = "sessionIdCookie")
    public SimpleCookie getSessionIdCookie() {
        SimpleCookie cookie = new SimpleCookie("mysid");
        cookie.setHttpOnly(true);
        cookie.setMaxAge(-1);
        return cookie;
    }


    @Bean(name = "rememberMeManager")
    public CookieRememberMeManager getCookieRememberMeManager() {
        CookieRememberMeManager cookieRememberMeManager =
                new CookieRememberMeManager();
        cookieRememberMeManager.setCipherKey(
                org.apache.shiro.codec.Base64.decode("4AvVhmFLUs0KTA3Kprsdag=="));
        cookieRememberMeManager.setCookie(getRememberMeCookie());
        return cookieRememberMeManager;
    }




}

MyShiroRealm

package com.liao.shiro;
import org.apache.commons.lang3.builder.ReflectionToStringBuilder;
import org.apache.commons.lang3.builder.ToStringStyle;
import org.apache.shiro.authc.*;
import org.apache.shiro.authc.credential.CredentialsMatcher;
import org.apache.shiro.authc.credential.HashedCredentialsMatcher;
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.apache.shiro.util.ByteSource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import javax.annotation.PostConstruct;
import java.util.HashSet;
import java.util.stream.Collectors;


@Component
public class MyShiroRealm extends AuthorizingRealm {

    private static final Logger logger = LoggerFactory.getLogger(MyShiroRealm.class);



    @Autowired
    private HashedCredentialsMatcher hashedCredentialsMatcher;

    /**
     * 权限认证,为当前登录的Subject授予角色和权限
     *
     * @see {}经测试:本例中该方法的调用时机为需授权资源被访问时
     * @see {}经测试:并且每次访问需授权资源时都会执行该方法中的逻辑,这表明本例中默认并未启用AuthorizationCache
     * @see {}经测试:如果连续访问同一个URL(比如刷新),该方法不会被重复调用,Shiro有一个时间间隔(也就是cache时间,在ehcache-shiro.xml中配置),超过这个时间间隔再刷新页面,该方法会被执行
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        logger.info("##################执行Shiro权限认证##################");

        //在此加入自己逻辑即可


    }

    /**
     * 登录认证
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(
            AuthenticationToken authenticationToken) throws AuthenticationException {
        //UsernamePasswordToken对象用来存放提交的登录信息
        UsernamePasswordToken token = (UsernamePasswordToken) authenticationToken;

        //不做过多说明  加入验证逻辑即可
    }

    @PostConstruct
    public void initCredentialsMatcher() {
        setCredentialsMatcher(hashedCredentialsMatcher);
    }

    @Override
    public void setCredentialsMatcher(CredentialsMatcher credentialsMatcher) {
        super.setCredentialsMatcher(credentialsMatcher);
    }

    @Override
    public void clearCachedAuthorizationInfo(PrincipalCollection principals) {
        super.clearCachedAuthorizationInfo(principals);
    }

    @Override
    public void clearCachedAuthenticationInfo(PrincipalCollection principals) {
        super.clearCachedAuthenticationInfo(principals);
    }

    @Override
    public void clearCache(PrincipalCollection principals) {
        super.clearCache(principals);
    }

    public void clearAllCachedAuthorizationInfo() {
        getAuthorizationCache().clear();
    }

    public void clearAllCachedAuthenticationInfo() {
        getAuthenticationCache().clear();
    }

    public void clearAllCache() {
        clearAllCachedAuthenticationInfo();
        clearAllCachedAuthorizationInfo();
    }


}

ShiroCache

package com.liao.shiro;

import org.apache.shiro.cache.Cache;
import org.apache.shiro.cache.CacheException;
import org.springframework.data.redis.core.RedisTemplate;

import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Set;
import java.util.concurrent.TimeUnit;

/**
 * @author liao
 * @Time 2017/8/8
 */
@SuppressWarnings("unchecked")
public class ShiroCache <K, V> implements Cache<K, V> {


    private static final String REDIS_SHIRO_CACHE = "shiro-cache:";
    private String cacheKey;
    private RedisTemplate<K, V> redisTemplate;
    private long globExpire = 60;

    @SuppressWarnings("rawtypes")
    public ShiroCache(String name, RedisTemplate client) {
        this.cacheKey = REDIS_SHIRO_CACHE + name + ":";
        this.redisTemplate = client;
    }

    @Override
    public V get(K key) throws CacheException {
//        System.out.println("|"+getCacheKey(key)+"|");
//        if(redisTemplate.boundValueOps(getCacheKey(key))==null||"".equals(redisTemplate.boundValueOps(getCacheKey(key)))){
//            return null;
//        }
       // System.out.println("|"+redisTemplate.boundValueOps(getCacheKey(key))+"|");
        redisTemplate.boundValueOps(getCacheKey(key)).expire(globExpire, TimeUnit.MINUTES);
        return redisTemplate.boundValueOps(getCacheKey(key)).get();
    }

    @Override
    public V put(K key, V value) throws CacheException {
        V old = get(key);
        redisTemplate.boundValueOps(getCacheKey(key)).set(value);
        return old;
    }

    @Override
    public V remove(K key) throws CacheException {
        V old = get(key);
        redisTemplate.delete(getCacheKey(key));
        return old;
    }

    @Override
    public void clear() throws CacheException {
        redisTemplate.delete(keys());
    }

    @Override
    public int size() {
        return keys().size();
    }

    @Override
    public Set<K> keys() {
        return redisTemplate.keys(getCacheKey("*"));
    }

    @Override
    public Collection<V> values() {
        Set<K> set = keys();
        List<V> list = new ArrayList<>();
        for (K s : set) {
            list.add(get(s));
        }
        return list;
    }

    private K getCacheKey(Object k) {
        return (K) (this.cacheKey + k);
    }
}

RedisCacheManager

package com.liao.shiro;

import org.apache.shiro.cache.Cache;
import org.apache.shiro.cache.CacheException;
import org.apache.shiro.cache.CacheManager;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;

/**
 * @author liao
 * @Time 2017/8/8
 */
@Component
public class RedisCacheManager implements CacheManager {


    @Resource
    private RedisTemplate<String, Object> redisTemplate;

    @Override
    public <K, V> Cache<K, V> getCache(String name) throws CacheException {
        return new ShiroCache<K, V>(name, redisTemplate);
    }

    public RedisTemplate<String, ?> getRedisTemplate() {
        return redisTemplate;
    }

    public void setRedisTemplate(RedisTemplate<String, Object> redisTemplate) {
        this.redisTemplate = redisTemplate;
    }
}

RedisSessionDAO

package com.liao.shiro;

import org.liao.session.Session;
import org.apache.shiro.session.mgt.eis.EnterpriseCacheSessionDAO;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;
import java.io.Serializable;
import java.util.concurrent.TimeUnit;

/**
 * @author liao
 * @Time 2017/8/8
 */

@Component
public class RedisSessionDAO extends EnterpriseCacheSessionDAO {

    private static Logger logger = LoggerFactory.getLogger(RedisSessionDAO.class);

    // session 在redis过期时间是60分钟60*60
    private static int expireTime = 3600;

    private static String prefix = "shiro-session:";

    @Resource
    public  RedisTemplate<String, Object> redisTemplate;

    // 创建session,保存到数据库
    @Override
    protected Serializable doCreate(Session session) {
        Serializable sessionId = super.doCreate(session);
        logger.debug("创建session:{}", session.getId());
        redisTemplate.opsForValue().set(prefix + sessionId.toString(), session);
        return sessionId;
    }

    // 获取session
    @Override
    protected Session doReadSession(Serializable sessionId) {
        logger.debug("获取session:{}", sessionId);
        // 先从缓存中获取session,如果没有再去数据库中获取
        Session session = super.doReadSession(sessionId);
        if (session == null) {
            session = (Session) redisTemplate.opsForValue().get(prefix + sessionId.toString());
        }
        return session;
    }

    // 更新session的最后一次访问时间
    @Override
    protected void doUpdate(Session session) {
        super.doUpdate(session);
        logger.debug("获取session:{}", session.getId());
        String key = prefix + session.getId().toString();
        if (!redisTemplate.hasKey(key)) {
            redisTemplate.opsForValue().set(key, session);
        }
        redisTemplate.expire(key, expireTime, TimeUnit.SECONDS);
    }

    // 删除session
    @Override
    protected void doDelete(Session session) {
        logger.debug("删除session:{}", session.getId());
        super.doDelete(session);
        redisTemplate.delete(prefix + session.getId().toString());
    }
}

ShiroUser

package com.lenovo.pyramid.shiro;

import java.io.Serializable;
import java.util.Objects;

/**
 * 自定义Authentication对象,使得Subject除了携带用户的登录名外还可以携带更多信息. 
 */  
public  class ShiroUser implements Serializable {
    private static final long serialVersionUID = -1373760761780840081L;  
    public String loginName;  
    public String name;
    public Long userId;


    public ShiroUser(String loginName, String name,Long userId) {
        this.loginName = loginName;  
        this.name = name;
        this.userId = userId;
    }  

    public String getName() {  
        return name;  
    }  

    /** 
     * 本函数输出将作为默认的<shiro:principal/>输出. 
     */  
    @Override  
    public String toString() {  
        return loginName;  
    }  

    /** 
     * 重载hashCode,只计算loginName; 
     */  
    @Override  
    public int hashCode() {  
        return Objects.hashCode(loginName);
    }  

    /** 
     * 重载equals,只计算loginName; 
     */  
    @Override  
    public boolean equals(Object obj) {  
        if (this == obj) {  
            return true;  
        }  
        if (obj == null) {  
            return false;  
        }  
        if (getClass() != obj.getClass()) {  
            return false;  
        }  
        ShiroUser other = (ShiroUser) obj;  
        if (loginName == null) {  
            if (other.loginName != null) {  
                return false;  
            }  
        } else if (!loginName.equals(other.loginName)) {  
            return false;  
        }  
        return true;  
    }  
}  

经过一顿折腾终于完事了,测试了一天问题没有再复现【当然还需要再测试几天】。

重要事情说三遍ShiroConfiguration2getLifecycleBeanPostProcessor 方法一定要设置为静态方法


引用领导的一句话做下总结:
没有解决不了的问题,在你尝试走完所有道路之前,问题肯定是能处理的。

猜你喜欢

转载自blog.csdn.net/liaoguolingxian/article/details/77017225