一、背景
- 一般情況下,我們?yōu)榱嗽黾泳W(wǎng)站的性能,會(huì)用Nginx去轉(zhuǎn)發(fā)請(qǐng)求負(fù)載到多臺(tái)Tomcat上,但是這就會(huì)出現(xiàn)一個(gè)問題。當(dāng)你在tomcatA上shiro登錄成功后session保存到了A服務(wù)器上,接下來你的請(qǐng)求可能被分發(fā)到tomcatB,這時(shí)A服務(wù)器中的session無法共享到B中,B會(huì)認(rèn)為你是沒有登錄的,就會(huì)跳轉(zhuǎn)到登錄頁。這就是我們要解決的,將Shiro的Session共享, 使得一處登錄,多處都能得到這個(gè)登錄態(tài)。此處我們使用redis來存儲(chǔ)這個(gè)Session。
二、配置代碼
- 此處關(guān)于redis 的配置我就不寫了。只寫關(guān)于shiro實(shí)現(xiàn)session的相關(guān)代碼
1.配置pom文件
- 需要引入shiro-redis這個(gè)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 過期時(shí)間24小時(shí)
private int shiroTimeout = 86400;
@Bean
public ShiroRedisConfig shiroRedisConfig(){
return new ShiroRedisConfig();
}
@Bean
public RedisManager redisManager(){
RedisManager redisManager = new RedisManager(); // crazycake 實(shí)現(xiàn)
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 實(shí)現(xiàn)
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,對(duì)應(yīng)的默認(rèn)是 JSESSIONID
cookie.setMaxAge(shiroTimeout);
cookie.setHttpOnly(true);
cookie.setPath("/"); // path為 / 用于多個(gè)系統(tǒng)共享JSESSIONID
return cookie;
}
@Bean(name="sessionManager")
public ShiroSessionManager shiroSessionManager() {
ShiroSessionManager sessionManager = new ShiroSessionManager();
sessionManager.setGlobalSessionTimeout(24*60*60*1000); // 設(shè)置session超時(shí),單位毫秒,此處設(shè)置24小時(shí)
sessionManager.setDeleteInvalidSessions(true); // 刪除無效session
sessionManager.setSessionIdCookie(cookie()); // 設(shè)置JSESSIONID
sessionManager.setSessionDAO(sessionDAO()); // 設(shè)置sessionDAO
return sessionManager;
}
// @Bean(name="sessionManager")
// public DefaultWebSessionManager defaultWebSessionManager() {
// DefaultWebSessionManager sessionManager = new DefaultWebSessionManager();
// sessionManager.setGlobalSessionTimeout(24*60*60*1000); // 設(shè)置session超時(shí)
// sessionManager.setDeleteInvalidSessions(true); // 刪除無效session
// sessionManager.setSessionIdCookie(cookie()); // 設(shè)置JSESSIONID
// sessionManager.setSessionDAO(sessionDAO()); // 設(shè)置sessionDAO
// return sessionManager;
// }
@Bean(name="securityManager")
public SecurityManager securityManager(@Qualifier("authRealm") CustomerShiroRealm authRealm) {
System.err.println("--------------shiro已經(jīng)加載----------------");
DefaultWebSecurityManager manager = new DefaultWebSecurityManager();
// 設(shè)置realm.
manager.setRealm(authRealm);
// 自定義session管理
manager.setSessionManager(shiroSessionManager());
return manager;
}
@Bean
public RedisCacheManager redisCacheManager(){
RedisCacheManager cacheManager = new RedisCacheManager(); // crazycake 實(shí)現(xiàn)
cacheManager.setRedisManager(redisManager());
cacheManager.setExpire(shiroTimeout);
return cacheManager;
}
/**
* 身份認(rèn)證realm; (這個(gè)需要自己寫,賬號(hào)密碼校驗(yàn);權(quán)限等)
*
* @return
*/
//配置自定義的權(quán)限登錄器
@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 處理攔截資源文件問題。
* 注意:單獨(dú)一個(gè)ShiroFilterFactoryBean配置是或報(bào)錯(cuò)的,以為在
* 初始化ShiroFilterFactoryBean的時(shí)候需要注入:SecurityManager
*
* Filter Chain定義說明 1、一個(gè)URL可以配置多個(gè)Filter,使用逗號(hào)分隔 2、當(dāng)設(shè)置多個(gè)過濾器時(shí),全部驗(yàn)證通過,才視為通過
* 3、部分過濾器可指定參數(shù),如perms,roles
*
*/
@Bean
public ShiroFilterFactoryBean shiroFilter(@Qualifier("securityManager") SecurityManager manager) {
ShiroFilterFactoryBean bean = new ShiroFilterFactoryBean();
// 必須設(shè)置 SecurityManager
bean.setSecurityManager(manager);
// 如果不設(shè)置默認(rèn)會(huì)自動(dòng)尋找Web工程根目錄下的"/login.jsp"頁面
bean.setLoginUrl("/admin/loginPage");
// 登錄成功后要跳轉(zhuǎn)的鏈接
bean.setSuccessUrl("/admin/successPage");
// 未授權(quán)界面;
bean.setUnauthorizedUrl("/403");
// 攔截器.
Map<String, String> filterChainDefinitionMap = new LinkedHashMap<String, String>();
// 配置不會(huì)被攔截的鏈接 順序判斷
filterChainDefinitionMap.put("/static/**", "anon");
filterChainDefinitionMap.put("/admin/login", "anon");
// 配置退出過濾器,其中的具體的退出代碼Shiro已經(jīng)替我們實(shí)現(xiàn)了
filterChainDefinitionMap.put("/logout", "logout");
filterChainDefinitionMap.put("/add", "perms[權(quán)限添加]");
// <!-- 過濾鏈定義,從上向下順序執(zhí)行,一般將 /**放在最為下邊 -->:這是一個(gè)坑呢,一不小心代碼就不好使了;
// <!-- authc:所有url都必須認(rèn)證通過才可以訪問; anon:所有url都都可以匿名訪問-->
filterChainDefinitionMap.put("/**", "authc");
bean.setFilterChainDefinitionMap(filterChainDefinitionMap);
//System.out.println("Shiro攔截器工廠類注入成功");
return bean;
}
}
- ShiroSessionManager類,該類是為了減少redis的訪問次數(shù)
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();
}
//重寫這個(gè)方法為了減少多次從redis中讀取session(自定義redisSessionDao中的doReadSession方法)
@Override
protected Session retrieveSession(SessionKey sessionKey) {
// 獲取sessionId
Serializable sessionId = getSessionId(sessionKey);
//logger.info("嘗試獲取Session:" + sessionId);
// 在 Web 下使用 shiro 時(shí)這個(gè) 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存儲(chǔ)到request中方便下次獲取
Session session = super.retrieveSession(sessionKey);
if (request != null && sessionId != null) {
//logger.info("存儲(chǔ)新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數(shù)據(jù)庫索引(默認(rèn)為0)
spring.redis.database=1
#Redis服務(wù)器地址
spring.redis.host=127.0.0.1
#Redis服務(wù)器連接端口
spring.redis.port=6379
#Redis服務(wù)器連接密碼(默認(rèn)為空)
spring.redis.password=123456
#連接池最大連接數(shù)(使用負(fù)值表示沒有限制)
spring.redis.pool.max-active=8
#連接池最大阻塞等待時(shí)間(使用負(fù)值表示沒有限制)
spring.redis.pool.max-wait=-1
#連接池中的最大空閑連接
spring.redis.pool.max-idle=8
#連接池中的最小空閑連接
spring.redis.pool.min-idle=0
#連接超時(shí)時(shí)間(毫秒)
spring.redis.timeout=1000
三、注意事項(xiàng)
-
配置完成后,訪問項(xiàng)目,redis 中應(yīng)該生成如下的緩存數(shù)據(jù),沒一個(gè)鍵值對(duì)代表一個(gè)登錄的用戶
image.png - 如果發(fā)生配置都是對(duì)的,redis中也有寫入值,但是就是無法共享redis。這時(shí)就要考慮,服務(wù)器之間是不是時(shí)間不對(duì),設(shè)置一下服務(wù)器時(shí)間,要保證集群里面的機(jī)器時(shí)間都是一致的。
