spring boot+redis+shrio+会话

来源:互联网 发布:mac后台怎么关闭 编辑:程序博客网 时间:2024/06/06 19:00

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


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

问题分析处理步骤:

1、检查httpwatch请求记录;        发现有的时候用户的sessionid突然就变了;2、怀疑是nginx配置问题(由于新参与这个项目,环境啥的都不了解)    经和运维沟通了解到当前系统是单点、而且nginx已经配置了会话保持,也就是说不存在乱跳的问题。

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

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

@WebListener@Logpublic 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@Configurationpublic 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;@Componentpublic 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 */@Componentpublic 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 */@Componentpublic 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 方法一定要设置为静态方法


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

原创粉丝点击