一、背景
- 一般情况下,我们为了增加网站的性能,会用Nginx去转发请求负载到多台Tomcat上,但是这就会出现一个问题。当你在tomcatA上shiro登录成功后session保存到了A服务器上,接下来你的请求可能被分发到tomcatB,这时A服务器中的session无法共享到B中,B会认为你是没有登录的,就会跳转到登录页。这就是我们要解决的,将Shiro的Session共享, 使得一处登录,多处都能得到这个登录态。此处我们使用redis来存储这个Session。
二、配置代码
- 此处关于redis 的配置我就不写了。只写关于shiro实现session的相关代码
1.配置pom文件
- 需要引入shiro-redis这个jar包
<!-- shiro+redis缓存插件 -->
<dependency>
<groupId>org.crazycake</groupId>
<artifactId>shiro-redis</artifactId>
<version>3.2.0</version>
</dependency>
2.修改shiro配置类
- ShiroConfig类
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.session.mgt.eis.JavaUuidSessionIdGenerator;
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.DefaultWebSecurityManager;
import org.apache.shiro.web.servlet.SimpleCookie;
import org.apache.shiro.web.session.mgt.DefaultWebSessionManager;
import org.crazycake.shiro.RedisCacheManager;
import org.crazycake.shiro.RedisManager;
import org.crazycake.shiro.RedisSessionDAO;
import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.LinkedHashMap;
import java.util.Map;
@Configuration
public class ShiroConfig {
//redis session 过期时间24小时
private int shiroTimeout = 86400;
@Bean
public ShiroRedisConfig shiroRedisConfig(){
return new ShiroRedisConfig();
}
@Bean
public RedisManager redisManager(){
RedisManager redisManager = new RedisManager(); // crazycake 实现
redisManager.setHost(shiroRedisConfig().redisHost + ":" + shiroRedisConfig().redisPort);
redisManager.setPassword(shiroRedisConfig().redisPwd);
redisManager.setTimeout(shiroTimeout);
redisManager.setDatabase(shiroRedisConfig().redisDatabase);
return redisManager;
}
@Bean
public JavaUuidSessionIdGenerator sessionIdGenerator(){
return new JavaUuidSessionIdGenerator();
}
@Bean
public RedisSessionDAO sessionDAO(){
RedisSessionDAO sessionDAO = new RedisSessionDAO(); // crazycake 实现
sessionDAO.setExpire(shiroTimeout);
sessionDAO.setRedisManager(redisManager());
sessionDAO.setSessionIdGenerator(sessionIdGenerator()); // Session ID 生成器
return sessionDAO;
}
@Bean
public SimpleCookie cookie(){
SimpleCookie cookie = new SimpleCookie("shiro.sesssion"); // cookie的name,对应的默认是 JSESSIONID
cookie.setMaxAge(shiroTimeout);
cookie.setHttpOnly(true);
cookie.setPath("/"); // path为 / 用于多个系统共享JSESSIONID
return cookie;
}
@Bean(name="sessionManager")
public ShiroSessionManager shiroSessionManager() {
ShiroSessionManager sessionManager = new ShiroSessionManager();
sessionManager.setGlobalSessionTimeout(24*60*60*1000); // 设置session超时,单位毫秒,此处设置24小时
sessionManager.setDeleteInvalidSessions(true); // 删除无效session
sessionManager.setSessionIdCookie(cookie()); // 设置JSESSIONID
sessionManager.setSessionDAO(sessionDAO()); // 设置sessionDAO
return sessionManager;
}
// @Bean(name="sessionManager")
// public DefaultWebSessionManager defaultWebSessionManager() {
// DefaultWebSessionManager sessionManager = new DefaultWebSessionManager();
// sessionManager.setGlobalSessionTimeout(24*60*60*1000); // 设置session超时
// sessionManager.setDeleteInvalidSessions(true); // 删除无效session
// sessionManager.setSessionIdCookie(cookie()); // 设置JSESSIONID
// sessionManager.setSessionDAO(sessionDAO()); // 设置sessionDAO
// return sessionManager;
// }
@Bean(name="securityManager")
public SecurityManager securityManager(@Qualifier("authRealm") CustomerShiroRealm authRealm) {
System.err.println("--------------shiro已经加载----------------");
DefaultWebSecurityManager manager = new DefaultWebSecurityManager();
// 设置realm.
manager.setRealm(authRealm);
// 自定义session管理
manager.setSessionManager(shiroSessionManager());
return manager;
}
@Bean
public RedisCacheManager redisCacheManager(){
RedisCacheManager cacheManager = new RedisCacheManager(); // crazycake 实现
cacheManager.setRedisManager(redisManager());
cacheManager.setExpire(shiroTimeout);
return cacheManager;
}
/**
* 身份认证realm; (这个需要自己写,账号密码校验;权限等)
*
* @return
*/
//配置自定义的权限登录器
@Bean(name="authRealm")
public CustomerShiroRealm authRealm(@Qualifier("credentialsMatcher") CredentialsMatcher matcher) {
CustomerShiroRealm authRealm = new CustomerShiroRealm();
authRealm.setCredentialsMatcher(matcher);
return authRealm;
}
//配置自定义的密码比较器
@Bean(name="credentialsMatcher")
public CredentialsMatcher credentialsMatcher() {
return new CredentialsMatcher();
}
@Bean
public LifecycleBeanPostProcessor lifecycleBeanPostProcessor(){
return new LifecycleBeanPostProcessor();
}
@Bean
public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator(){
DefaultAdvisorAutoProxyCreator creator=new DefaultAdvisorAutoProxyCreator();
creator.setProxyTargetClass(true);
return creator;
}
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(@Qualifier("securityManager") SecurityManager manager) {
AuthorizationAttributeSourceAdvisor advisor=new AuthorizationAttributeSourceAdvisor();
advisor.setSecurityManager(manager);
return advisor;
}
/**
* ShiroFilterFactoryBean 处理拦截资源文件问题。
* 注意:单独一个ShiroFilterFactoryBean配置是或报错的,以为在
* 初始化ShiroFilterFactoryBean的时候需要注入:SecurityManager
*
* Filter Chain定义说明 1、一个URL可以配置多个Filter,使用逗号分隔 2、当设置多个过滤器时,全部验证通过,才视为通过
* 3、部分过滤器可指定参数,如perms,roles
*
*/
@Bean
public ShiroFilterFactoryBean shiroFilter(@Qualifier("securityManager") SecurityManager manager) {
ShiroFilterFactoryBean bean = new ShiroFilterFactoryBean();
// 必须设置 SecurityManager
bean.setSecurityManager(manager);
// 如果不设置默认会自动寻找Web工程根目录下的"/login.jsp"页面
bean.setLoginUrl("/admin/loginPage");
// 登录成功后要跳转的链接
bean.setSuccessUrl("/admin/successPage");
// 未授权界面;
bean.setUnauthorizedUrl("/403");
// 拦截器.
Map<String, String> filterChainDefinitionMap = new LinkedHashMap<String, String>();
// 配置不会被拦截的链接 顺序判断
filterChainDefinitionMap.put("/static/**", "anon");
filterChainDefinitionMap.put("/admin/login", "anon");
// 配置退出过滤器,其中的具体的退出代码Shiro已经替我们实现了
filterChainDefinitionMap.put("/logout", "logout");
filterChainDefinitionMap.put("/add", "perms[权限添加]");
// <!-- 过滤链定义,从上向下顺序执行,一般将 /**放在最为下边 -->:这是一个坑呢,一不小心代码就不好使了;
// <!-- authc:所有url都必须认证通过才可以访问; anon:所有url都都可以匿名访问-->
filterChainDefinitionMap.put("/**", "authc");
bean.setFilterChainDefinitionMap(filterChainDefinitionMap);
//System.out.println("Shiro拦截器工厂类注入成功");
return bean;
}
}
- ShiroSessionManager类,该类是为了减少redis的访问次数
import org.apache.log4j.LogManager;
import org.apache.log4j.Logger;
import org.apache.shiro.session.Session;
import org.apache.shiro.session.mgt.SessionKey;
import org.apache.shiro.web.session.mgt.DefaultWebSessionManager;
import org.apache.shiro.web.session.mgt.WebSessionKey;
import org.springframework.stereotype.Component;
import javax.servlet.ServletRequest;
import java.io.Serializable;
/**
* @Auther: LinJiaJia
* @Date: 2019/3/11 11:54
* @Description:自定义retrieveSession方法,把session放到request里面,这样就不用每次去redis里面去取了,这样大大提高了redis的性能。
*/
@Component
public class ShiroSessionManager extends DefaultWebSessionManager {
protected static final Logger logger = LogManager.getLogger(ShiroSessionManager.class);
public ShiroSessionManager() {
super();
}
//重写这个方法为了减少多次从redis中读取session(自定义redisSessionDao中的doReadSession方法)
@Override
protected Session retrieveSession(SessionKey sessionKey) {
// 获取sessionId
Serializable sessionId = getSessionId(sessionKey);
//logger.info("尝试获取Session:" + sessionId);
// 在 Web 下使用 shiro 时这个 sessionKey 是 WebSessionKey 类型的
// 若是在web下使用,则获取request
ServletRequest request = null;
if (sessionKey instanceof WebSessionKey) {
request = ((WebSessionKey) sessionKey).getServletRequest();
}
// 尝试从request中获取session
if (request != null && sessionId != null) {
Session session = (Session) request.getAttribute(sessionId.toString());
if (session != null) {
return session;
}
}
// 若从request中获取session失败,则从redis中获取session,并把获取到的session存储到request中方便下次获取
Session session = super.retrieveSession(sessionKey);
if (request != null && sessionId != null) {
//logger.info("存储新session到request中:" + sessionId);
request.setAttribute(sessionId.toString(), session);
}
return session;
}
}
- ShiroRedisConfig类
package com.syiti.aic.web.admin.config;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
/**
* @Auther: LinJiaJia
* @Date: 2019/3/7 16:44
* @Description:
*/
@Component
public class ShiroRedisConfig {
@Value("${spring.redis.host}")
public String redisHost;
@Value("${spring.redis.password}")
public String redisPwd;
@Value("${spring.redis.database}")
public int redisDatabase;
}
3.redis配置
- redis的properties配置
#Redis数据库索引(默认为0)
spring.redis.database=1
#Redis服务器地址
spring.redis.host=127.0.0.1
#Redis服务器连接端口
spring.redis.port=6379
#Redis服务器连接密码(默认为空)
spring.redis.password=123456
#连接池最大连接数(使用负值表示没有限制)
spring.redis.pool.max-active=8
#连接池最大阻塞等待时间(使用负值表示没有限制)
spring.redis.pool.max-wait=-1
#连接池中的最大空闲连接
spring.redis.pool.max-idle=8
#连接池中的最小空闲连接
spring.redis.pool.min-idle=0
#连接超时时间(毫秒)
spring.redis.timeout=1000
三、注意事项
-
配置完成后,访问项目,redis 中应该生成如下的缓存数据,没一个键值对代表一个登录的用户
- 如果发生配置都是对的,redis中也有写入值,但是就是无法共享redis。这时就要考虑,服务器之间是不是时间不对,设置一下服务器时间,要保证集群里面的机器时间都是一致的。