環境:centos7,Java1.8+,一個Nginx,兩個Tomcat,一個Redis。
關於共享session的問題大家都應該知道了,傳統的部署項目,兩個相同的項目部署到不同的服務器上,Nginx負載均衡后會導致用戶在A上登陸了,經過負載均衡后,在B上要重新登錄,因為A上有相關session信息,而B沒有。這種情況也稱為“有狀態”服務。而“無狀態”服務則是:在一個公共的地方存儲session,每次訪問都會統一到這個地方來拿。
為了方便我只用了一台linux虛擬機。
如果你使用了Shiro權限管理呢,他的好處之一就是會話管理,由Shiro管理Session,而不是Tomcat。聽說都在用Spring-Session,實際上它和shiro一樣,請看web.xml里面的配置
<!-- spring-session Filter <filter> <filter-name>springSessionRepositoryFilter</filter-name> <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class> </filter> <filter-mapping> <filter-name>springSessionRepositoryFilter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping> --> <!-- Shiro Filter is defined in the spring application context: --> <filter> <filter-name>shiroFilter</filter-name> <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class> <init-param> <param-name>targetFilterLifecycle</param-name> <param-value>true</param-value> </init-param> </filter> <filter-mapping> <filter-name>shiroFilter</filter-name> <url-pattern>/*</url-pattern> <dispatcher>REQUEST</dispatcher> <dispatcher>FORWARD</dispatcher> <dispatcher>INCLUDE</dispatcher> <dispatcher>ERROR</dispatcher> </filter-mapping>
不難看出,這兩個都是基於:org.springframework.web.filter.DelegatingFilterProxy
下面開始正題。
使用Shiro需要繼承AbstractSessionDAO
package com.internetsaying.auth.session; import java.io.Serializable; import java.util.Collection; import java.util.List; import org.apache.shiro.session.Session; import org.apache.shiro.session.UnknownSessionException; import org.apache.shiro.session.mgt.eis.AbstractSessionDAO; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import com.internetsaying.auth.redis.RedisManager; @Component public class MySessionDao extends AbstractSessionDAO { @Autowired private RedisManager redisManager; @Override public void delete(Session session) { if(session == null || session.getId() == null){ System.out.println("Session is null"); return; } redisManager.hdelete(session.getId().toString()); } @Override public Collection<Session> getActiveSessions() { List<Session> list = redisManager.hmget(); return list; } @Override public void update(Session session) throws UnknownSessionException { if(session == null || session.getId() == null){ System.out.println("Session is null"); return; } Serializable sessionId = session.getId(); redisManager.hadd(sessionId.toString(), session); } @Override protected Serializable doCreate(Session session) { Serializable sessionId = generateSessionId(session); assignSessionId(session, sessionId); //添加進redis redisManager.hadd(sessionId.toString(), session); return sessionId; } @Override protected Session doReadSession(Serializable sessionId) { return redisManager.hget(sessionId.toString()); } }
下面是RedisManager的代碼
package com.internetsaying.auth.redis; import java.util.ArrayList; import java.util.List; import org.apache.shiro.session.Session; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.stereotype.Component; @Component public class RedisManager { @Autowired private RedisTemplate<String, Session> redisTemplate; private static final String KEY = "shareSessionMap"; public void hadd(String sessionId, byte[] bytes){ redisTemplate.boundHashOps(KEY).put(sessionId, bytes); } public void hadd(String sessionId, Session session){ redisTemplate.boundHashOps(KEY).put(sessionId, session); } public void hdelete(String sessionId){ redisTemplate.boundHashOps(KEY).delete(sessionId); } public Session hget(String sessionId){ return (Session) redisTemplate.boundHashOps(KEY).get(sessionId); } public List<Session> hmget(){ List<Session> list = new ArrayList<>(); List<Object> values = redisTemplate.boundHashOps(KEY).values(); for (Object object : values) { list.add((Session) object); } return list; } }
配置文件:shiro和redis部分(只是與共享session相關的代碼)
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context" xmlns:core="http://activemq.apache.org/schema/core" xmlns:redis="http://www.springframework.org/schema/redis" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.3.xsd http://activemq.apache.org/schema/core http://activemq.apache.org/schema/core http://www.springframework.org/schema/redis http://www.springframework.org/schema/redis/spring-redis-1.0.xsd"> <!-- SecurityManager --> <bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager"> <!-- session管理 --> <property name="sessionManager" ref="sessionManager"></property> </bean> <!-- redis操作 --> <bean id="redisManager" class="com.internetsaying.auth.redis.RedisManager"></bean> <!-- Session ID 生成器 --> <bean id="sessionIdGenerator" class="org.apache.shiro.session.mgt.eis.JavaUuidSessionIdGenerator"></bean> <!-- SessionDao實現類 --> <bean id="sessionDAO" class="com.internetsaying.auth.session.MySessionDao"> <property name="sessionIdGenerator" ref="sessionIdGenerator"></property> </bean> <!-- session管理 --> <bean id="sessionManager" class="org.apache.shiro.web.session.mgt.DefaultWebSessionManager"> <property name="globalSessionTimeout" value="1800000"></property> <property name="deleteInvalidSessions" value="true"></property> <property name="sessionDAO" ref="sessionDAO"></property> <!-- sessionIdCookie的實現,用於重寫覆蓋容器默認的JSESSIONID --> <property name="sessionIdCookie" ref="sharesession" /> </bean> <!-- sessionIdCookie的實現,用於重寫覆蓋容器默認的JSESSIONID --> <bean id="sharesession" class="org.apache.shiro.web.servlet.SimpleCookie"> <!-- cookie的name,對應的默認是 JSESSIONID --> <constructor-arg name="name" value="SHAREJSESSIONID" /> <!-- jsessionId的path為 / 用於多個系統共享jsessionId --> <property name="path" value="/" /> <property name="httpOnly" value="true"/> </bean> <!-- 以下是Redis --> <context:property-placeholder location="classpath:redis.properties"/> <bean id="jedisPoolConfig" class="redis.clients.jedis.JedisPoolConfig"> <property name="maxIdle" value="${redis.maxIdle}"></property> <property name="maxTotal" value="${redis.maxTotal}"></property> <property name="maxWaitMillis" value="${redis.maxWaitMillis}"></property> <property name="testOnBorrow" value="${redis.testOnBorrow}"></property> </bean> <bean id="jedisConnectionFactory" class="org.springframework.data.redis.connection.jedis.JedisConnectionFactory"> <property name="usePool" value="${redis.pooled}"></property> <property name="hostName" value="${redis.host}"></property> <property name="port" value="${redis.port}"></property> <property name="poolConfig" ref="jedisPoolConfig"></property> </bean> <bean id="redisTemplate" class="org.springframework.data.redis.core.RedisTemplate"> <property name="connectionFactory" ref="jedisConnectionFactory"></property> <property name="keySerializer"> <bean class="org.springframework.data.redis.serializer.StringRedisSerializer"></bean> </property> <!-- 解決讀取int類型value值報錯的問題 --> <property name="valueSerializer"> <bean class="org.springframework.data.redis.serializer.StringRedisSerializer"></bean> </property> <property name="hashKeySerializer"> <bean class="org.springframework.data.redis.serializer.StringRedisSerializer"></bean> </property> </bean> </beans>
配置文件:redis.properties
#Redis settings
redis.host=127.0.0.1
redis.port=6379
#redis.pass=
redis.maxIdle=30
redis.maxTotal=100
redis.maxWaitMillis=1000
redis.testOnBorrow=true
redis.pooled=true
執行流程
說明:由於做的是分布式項目,不便貼上全部代碼,如遇問題可聯系我:3110320051.
我有個完整的小栗子
部署
關於Nginx做負載均衡可參考我上一篇文章。
這里將項目打包后,分別上傳到兩個Tomcat,啟動。當訪問時。【頁面過於簡單,僅供測試】
點擊登錄既完成Shiro的認證授權,跳轉到可訪問頁面。
刷新
注意cookie的Value
現在我們看一下redis存儲的:
[root@localhost local]# ./bin/redis-cli 127.0.0.1:6379> keys * 1) "shareSessionMap" 127.0.0.1:6379> HKEYS shareSessionMap 1) "a511c5c4-9f61-4c24-aad2-b551f8053ebf"
所以:每次服務器都是去redis里面去拿的session,登錄A后,切換到B不需要重新登錄。