之前已经在几个项目中使用过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.youkuaiyun.com/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;
}
}
经过一顿折腾终于完事了,测试了一天问题没有再复现【当然还需要再测试几天】。
重要事情说三遍ShiroConfiguration2 中getLifecycleBeanPostProcessor 方法一定要设置为静态方法
引用领导的一句话做下总结:
没有解决不了的问题,在你尝试走完所有道路之前,问题肯定是能处理的。