Redis+Shiro+Spring-data-redis,共享Session
环境: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不需要重新登录。
小LUA
面对敌人的严刑逼供,我一个字也没说,而是一五一十写了下来。