为什么要做Session共享
什么是session
我们都知道HTTP协议(1.1)是无状态的,所以服务器在需要识别用户访问的时候,就要做相应的记录用于跟踪用户操作,这个实现机制就是Session。当一个用户第一次访问服务器的时候,服务器就会为用户创建一个Session,每个Session都有一个唯一的SessionId(应用级别)用于标识用户。
Session通常不会单独出现,因为请求是无状态的,那么我们必须让用户在下次请求时带上服务器为其生成的Session的ID,通常的做法时使用Cookie实现(当然你要非要在请求参数中带上SessionId那也不是不行)。请求返回时会向浏览器的Cookie中写入SessionID,通常使用的键是JSESSIONID,这样下次用户再请求这台服务器时,服务器就能从Cookie中取出SessionId识别出该次请求的用户是谁。
例如:
左边红框部分是Cookie列表,当前服务器是:localhost:28080。右边红框部分从左到右依次是Cookie的键、值、主机、路径和过期时间。路径为/时表示全站有效,最后一个过期时间未设置的话是默认值为Session,表示浏览器关闭时该Cookie失效。我们也可以为Cookie指定过期时间,以做到会话保持。
什么是session共享
通过Session和Cookie,我们使得无状态的HTTP协议间接的变成了有状态的了,可以实现保持登录,存储用户信息,购物车等等功能。但是随着服务访问人数的增多,单台服务器已经不足以应付所有的请求了,必须部署集群环境。但是随着集群环境的出现,追踪用户状态的问题又开始出现问题,之前用户在A服务器登录,A服务器保存了用户信息,但是下一次请求发送到B服务器去了,这时候B服务器是不知道用户在A服务器登录的事情的,它虽然也能拿到用户请求Cookie中的SessionId,但是在B服务根据这个SessionId找不到对应的Session,B服务器就会认为用户没有登录,需要用户重新登录,这对用户来说是没办法接受的。
这时候常见的有两种方式解决这个问题,第一种是让这个用户所有的请求都发送到A服务器,比如根据IP地址做一些列算法将所有用户分配到不同的服务器上去,让每个用户只访问其中的一台服务器。这种做法可行,但是后续也会产生其它问题,更好的做法是第二种,将所有的服务器上的Session都做成共享的,A服务能拿到B服务器上的所有Session,同理B服务器也能获取A服务器所有的Session,这样上面的问题就不存在了。
Shiro结合Redis实现Session共享
上一篇已经通过Shiro实现了用户登录和权限管理,Shiro的登录也是基于Session的,默认情况下Session是保存在内存中。既然要做Session共享,那么肯定是将Session抽取出来,放到一个多个服务器都能访问到的地方。
在集群环境下,我们仅仅需要继承AbstractSessionDAO,实现一下Session的增删改查等几个方法就可以很方便的实现Session共享,Shiro已经将完整的流程都做好了。这里涉及到的设计模式是模板方法模式,我们仅需要参与部分业务就可以完善整个流程了,当然我们不参与这部分流程的话,Shiro也有默认的实现方式,那就是将Session管理在当前应用的内存中。
具体的Session管理(共享)怎么实现由我们自己决定,可以存放在数据库,也可以通过网络传输,甚至可以通过IO流写入文件都行,但就性能来讲,我们一般都将Session放入Redis中。
自定义RedisSessionDAO
import java.io.Serializable;
import java.util.Collection;
import java.util.Date;
import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.TimeUnit;
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.data.redis.core.RedisTemplate;
/**
* @author somebody
*/
public class RedisSessionDao extends AbstractSessionDAO {
/**
* Session超时时间(秒)
*/
private long expireTime;
public RedisSessionDao(long expireTime) {
this.expireTime = expireTime;
}
@SuppressWarnings("rawtypes")
@Autowired
private RedisTemplate redisTemplate;
// private int redisTemplateCount = 0;
@SuppressWarnings("unchecked")
@Override
protected Serializable doCreate(Session session) {
Serializable sessionId = this.generateSessionId(session);
this.assignSessionId(session, sessionId);
redisTemplate.opsForValue().set(session.getId(), session, expireTime, TimeUnit.SECONDS);
return sessionId;
}
@Override
protected Session doReadSession(Serializable sessionId) {
return sessionId == null ? null : (Session) redisTemplate.opsForValue().get(sessionId);
}
@SuppressWarnings("unchecked")
@Override
public void update(Session session) throws UnknownSessionException {
redisTemplate.opsForValue().set(session.getId(), session, expireTime, TimeUnit.SECONDS);
}
@SuppressWarnings("unchecked")
@Override
public void delete(Session session) {
if (session != null && session.getId() != null) {
redisTemplate.opsForValue().getOperations().delete(session.getId());
}
}
@SuppressWarnings("unchecked")
@Override
public Collection<Session> getActiveSessions() {
return redisTemplate.keys("*");
}
}
以上代码需要引入相关依赖,这里的版本为2.3.12.RELEASE
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
并添加redis服务相关配置信息
###redis连接配置
# dev env
spring.redis.host=192.168.8.225
spring.redis.port=6379
spring.redis.password=redis的密码
# Redis连接池最大连接数
# spring.redis.jedis.pool.max-active=1000
# Redis连接池最大空闲连接数
# spring.redis.jedis.pool.max-idle=100
# 连接超时时间
spring.redis.timeout=15000
# 连接池最大连接数(使用负值表示没有限制)
spring.redis.lettuce.pool.max-active=3
# 连接池中的最小空闲连接
spring.redis.lettuce.pool.min-idle=2
# 连接池中的最大空闲连接
spring.redis.lettuce.pool.max-idle=3
# 连接池最大阻塞等待时间(使用负值表示没有限制)
spring.redis.lettuce.pool.max-wait=-1
#在关闭客户端连接之前等待任务处理完成的最长时间,在这之后,无论任务是否执行完成,都会被执行器关闭,默认100ms
spring.redis.lettuce.shutdown-timeout=100
注入RedisSessionDao
上面只是我们自己实现的管理Session的方式,现在需要将其注入SessionManager中,并设置过期时间等相关参数。
这里有两种方式将RedisSessionDao注入进SessionManager:
直接注入RedisSessionDao
优点:不会存在session的设置的值更新不同步问题
缺点:系统的访问速度又很慢
将RedisSessionDao封装进shiro的缓存sessionDAO : CachingSessionDAO, 这样做的目的是大大提供系统的访问速度
优点:系统访问速度快
缺点:session的设置的值更新不同步
- 上述第2点中的CachingSessionDAO是继承自AbstractSessionDAO的。
这里采用第2中方式,这种方式能保证系统能有较快的访问速度, 至于“session的设置的值更新不同步”
的问题待后续解决
CachingSessionDAO封装RedisSessionDao
import java.io.Serializable;
import org.apache.shiro.session.Session;
import org.apache.shiro.session.mgt.eis.CachingSessionDAO;
import org.springframework.beans.factory.annotation.Autowired;
public class LocalRedisSessionDAO extends CachingSessionDAO {
@Autowired
private RedisSessionDao redisSessionDao;
@Override
protected void doUpdate(Session session) {
redisSessionDao.update(session);
cache(session,session.getId());
}
@Override
protected void doDelete(Session session) {
redisSessionDao.delete(session);
uncache(session);
}
@Override
protected Session doReadSession(Serializable sessionId) {
// return redisSessionDao.readSession(sessionId);
Session session = getCachedSession(sessionId);
if (session == null) {
session = redisSessionDao.readSession(sessionId);
if (session != null) {
cache(session, session.getId());
}
}
return session;
}
@Override
protected Serializable doCreate(Session session) {
Serializable sessionId = generateSessionId(session);
assignSessionId(session,sessionId);
cache(session, sessionId);
return sessionId;
}
}
在ShiroConfig.java中配置RedisSessionDao 和LocalRedisSessionDAO
@Bean
public RedisSessionDao redisSessionDao() {
return new RedisSessionDao(expireTime);
}
/**
* John Abruzzi
* @return
*/
@Bean
public LocalRedisSessionDAO localRedisSessionDAO() {
LocalRedisSessionDAO localRedisSessionDAO = new LocalRedisSessionDAO();
localRedisSessionDAO.setCacheManager(ehCacheManager());
localRedisSessionDAO.setActiveSessionsCacheName("shiroSessionCache");
return localRedisSessionDAO;
}
将LocalRedisSessionDAO注入进SessionManager
@Bean
public MySessionManager mySessionManager() {
MySessionManager mySessionManager = new MySessionManager();
// 配置自定义SessionDao,直接使用redisSessionDao不会存在session的设置的值更新不同步问题,但是系统的访问速度又很慢
// mySessionManager.setSessionDAO(redisSessionDao());
// 将redisSessionDao改为本地缓存DAO: localRedisSessionDAO,使用本地缓存可以提高访问速度,但是存在session的设置的值更新不同步问题
mySessionManager.setSessionDAO(localRedisSessionDAO());
// mySessionManager.setGlobalSessionTimeout(expireTime * 1000);
mySessionManager.setGlobalSessionTimeout(5 * 1000);
return mySessionManager;
}
最后再将SessionManager注入Shiro的安全管理器SecurityManager中,前面说过,我们围绕安全相关的所有操作,都需要与SecurityManager打交道,这位才是Shiro中真正的老大哥。
@Bean(name = "SecurityManager")
public SecurityManager securityManager(){
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
//设置realm.
securityManager.setAuthenticator(modularRealmAuthenticator());
List<Realm> realms = new ArrayList<>();
//添加多个Realm
realms.add(realm1);
realms.add(realm2);
realms.add(realm3);
// 注入缓存管理器;
securityManager.setCacheManager(ehCacheManager());
securityManager.setRealms(realms);
// 取消Cookie中的RememberMe参数
securityManager.setRememberMeManager(null);
// 配置自定义Session管理器
securityManager.setSessionManager(mySessionManager());
return securityManager;
}
在springCloud分布式环境下,使用openfeign调用远程服务器的的微服务时,需要手动传递cookie
在feign的请求拦截器中,实现拦截并将 源请求的cookie传递给feign的请求
requestTemplate.header("Cookie", request.getHeader("Cookie")); // 新request
feign拦截器的完整代码:
import javax.servlet.http.HttpServletRequest;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import feign.RequestInterceptor;
import feign.RequestTemplate;
import io.micrometer.core.instrument.util.StringUtils;
import io.seata.core.context.RootContext;
@Configuration
public class ComonConfig {
@Bean
public RequestInterceptor requestInterceptor() {
return new RequestInterceptor() {
@Override
public void apply(RequestTemplate requestTemplate) {
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder
.getRequestAttributes();
HttpServletRequest request = attributes.getRequest(); // 老 request
// 从Seata获取XID,并将其添加到Feign请求标头中
String xid = RootContext.getXID();
if (xid != null) {
requestTemplate.header("TX_XID", xid);
}
requestTemplate.header("Cookie", request.getHeader("Cookie")); // 新request
String pageSizeDefault = "20";
String pageNumDefalut = "1";
String pageSize = request.getParameter("pageSize");
String pageNum = request.getParameter("pageNum");
if (StringUtils.isNotEmpty(pageSize)) {
pageSizeDefault = pageSize;
}
if (StringUtils.isNotEmpty(pageNum)) {
pageNumDefalut = pageNum;
}
requestTemplate.header("pageSize", pageSizeDefault);
requestTemplate.header("pageNum", pageNumDefalut);
// requestTemplate.header("Accept", "application/json;charset=UTF-8");
// requestTemplate.header("Content-Type", "application/json;charset=UTF-8");
}
};
}
}
验证
分别使用2种浏览器登录2个不同的用户,在redis中查看对应的存储的sessionId
由上图可知,浏览器的SessionId均被存入到了Redis中。因此,其他应用/服务便可以通过Redis获取到对应的Session,来完成身份的认证!!
结束!