目录
1、为什么要使用redis管理session
很多公司会使用分布式来部署项目,使用反向代理功能来分发请求,但是如果依旧采用session的方式来对登录信息等进行管理,就会限制反向代理服务器的配置(根据ip,hash服务器地址),为了解决这个问题,使用独立的session服务器可以完美解决这个问题,如下图所示
每个服务器都会从相同的redis里读取用户登录信息,如果用户登录认证是在服务器1上面完成的,服务器1会把相应的会话数据保存到redis中,当用户访问其他链接时,请求被分配到了服务器3,由于服务器是无状态的,服务器3会到redis中查找该用户状态,查询到已登录后,处理数据,成功返回!
2、session的工作原理
在使用之前,我们先了解一下session的原理,session是一段会话,由于http是无状态的,为了保存会话状态,才有了session。session在服务器端是存放在内存中的,如果登录用户多会占用很多内存资源,而session在客户端时保存在cookie中的一段文本JSESSIONID,而这个id正式链接客户端和服务器内存的凭证。
下图模拟用户登录后修改密码流程
3、session的生命周期
- session在用户首次访问系统时创建(访问静态资源不会创建)
- 生命周期可以由服务器手动配置,tomcat默认为30分钟
- 由于session对应的信息保存在cookie中,关闭浏览器并不会使session失效(除非改写session)
- 服务器端可以使用 invalidate() 手动失效session
4、 shiro的session
4.1、官方说明
默认 SecurityManager 实现默认使用 DefaultSessionManager 开箱即用。该 DefaultSessionManager 实现提供了应用程序所需的所有企业级会话管理功能,例如会话验证,孤立清理等。可在任何应用程序中使用。像所管理的所有其他组件一样 SecurityManager , SessionManager 可以通过 Shiro 的所有默认 SecurityManager 实现( getSessionManager()/ setSessionManager() )上的JavaBeans风格的getter / setter方法来获取或设置它们。每当创建或更新会话时,其数据都需要保留到存储位置,以便应用程序稍后可以访问它。类似地,当会话无效且已被更长时间使用时,需要将其从存储中删除,因此会话数据存储空间不会耗尽。这些SessionManager实现将这些创建/读取/更新/删除(CRUD)操作委托给一个内部组件,该组件SessionDAO反映了数据访问对象(DAO)设计模式。
SessionDAO的功能是您可以实现此接口以与所需的任何数据存储进行通信。这意味着您的会话数据可以驻留在内存中,文件系统上,关系数据库或NoSQL数据存储区中,或您需要的任何其他位置。您可以控制持久性行为。
您可以将任何SessionDAO实现配置为默认SessionManager实例上的属性。
我们可以根据下图查看到shiro的session具体结构。
4.2、shiro默认session的实现
- 在之前的配置中,我们知道了shiro的核心配置项是 SecurityManager 类,发现这个类是提供 setSessionManager() 方法让我们自定义session管理器的。
/**
* 注入 securityManager
*/
@Bean
public SecurityManager securityManager() {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(myShiroRealm());
securityManager.setRememberMeManager(rememberMeManager());
securityManager.setCacheManager(myEhCacheManager());// 将缓存管理交给ehCache
securityManager.setSessionManager(****);//设置session管理器
return securityManager;
}
- 所有的配置都会注入我们使用的 DefaultWebSecurityManager 中,查看该类发现在构建实例的时候,系统默认设置了 ServletContainerSessionManager() 为session管理器
class DefaultWebSecurityManager
......
//无参构造
public DefaultWebSecurityManager() {
super();
((DefaultSubjectDAO) this.subjectDAO).setSessionStorageEvaluator(new DefaultWebSessionStorageEvaluator());
this.sessionMode = HTTP_SESSION_MODE;
setSubjectFactory(new DefaultWebSubjectFactory());
setRememberMeManager(new CookieRememberMeManager());
setSessionManager(new ServletContainerSessionManager());
}
- 继续查看 ServletContainerSessionManager 类,在new的时候,会依次执行父类的构造,在 SessionsSecurityManager 中,我们发现了如下代码,设置sessionManager为 DefaultSessionManager ,
/**
* Default no-arg constructor, internally creates a suitable default {@link SessionManager SessionManager} delegate
* instance.
*/
public SessionsSecurityManager() {
super();
this.sessionManager = new DefaultSessionManager();
applyCacheManagerToSessionManager();
}
- 而在 DefaultSessionManager 的构造中,我们找到了默认提供的sessionDao的实现 MemorySessionDAO 类
class DefaultSessionManager
......
public DefaultSessionManager() {
this.deleteInvalidSessions = true;
this.sessionFactory = new SimpleSessionFactory();
this.sessionDAO = new MemorySessionDAO();
}
- MemorySessionDAO 关系图,可以看到,他继承了AbstractSessionDAO抽象类,而顶级接口则是SessionDao层,另外以外的发现了shiro为我们提供的一个企业级session缓存的实现类 EnterpriseCacheSessionDAO
6.查看了一下 EnterpriseCacheSessionDAO 类发现,这个缓存是利用了shiro提供的 Cache 接口 下的 MapCache 实现类实现的,ConcurrentHashMap作为value来保证线程安全!
public class EnterpriseCacheSessionDAO extends CachingSessionDAO {
public EnterpriseCacheSessionDAO() {
setCacheManager(new AbstractCacheManager() {
@Override
protected Cache<Serializable, Session> createCache(String name) throws CacheException {
return new MapCache<Serializable, Session>(name, new ConcurrentHashMap<Serializable, Session>());
}
});
}
......
- shiro 提供的Cache接口另外实现类
8.回到 MemorySessionDAO 我们查看这个类的相关代码
public class MemorySessionDAO extends AbstractSessionDAO {
private static final Logger log = LoggerFactory.getLogger(MemorySessionDAO.class);
//保存session的容器
private ConcurrentMap<Serializable, Session> sessions;
//构造
public MemorySessionDAO() {
this.sessions = new ConcurrentHashMap<Serializable, Session>();
}
//创建session
protected Serializable doCreate(Session session) {
//sessionid生成器
Serializable sessionId = generateSessionId(session);
assignSessionId(session, sessionId);
storeSession(sessionId, session);
return sessionId;
}
//存储会话
protected Session storeSession(Serializable id, Session session) {
if (id == null) {
throw new NullPointerException("id argument cannot be null.");
}
//只有在key不存在或者key为null的时候,value值才会被覆盖 jdk 1.8新特性
return sessions.putIfAbsent(id, session);
/**putIfAbsent 源码展示
default V putIfAbsent(K key, V value) {
V v = get(key);
if (v == null) {
v = put(key, value);
}
return v;
}
*/
}
//根据sessionid获取session对象
protected Session doReadSession(Serializable sessionId) {
return sessions.get(sessionId);
}
//根据sessionid更新session对象
public void update(Session session) throws UnknownSessionException {
storeSession(session.getId(), session);
}
//删除session
public void delete(Session session) {
if (session == null) {
throw new NullPointerException("session argument cannot be null.");
}
Serializable id = session.getId();
if (id != null) {
sessions.remove(id);
}
}
//获取存活的session对象
public Collection<Session> getActiveSessions() {
Collection<Session> values = sessions.values();
if (CollectionUtils.isEmpty(values)) {
return Collections.emptySet();
} else {
return Collections.unmodifiableCollection(values);
}
}
}
可以看到,在默认情况下,session的操作全是有 MemorySessionDAO 类实现的,我们猜想,新增redis操作类,继承 AbstractSessionDAO ,重写其中的重要方法,是不是就可以实现redis管理session了?
5、使用redis管理session(配置)
redis的安装就不介绍了,只讲解如何在shiro中使用redis管理session!
5.1 、引入jar包
上面虽然已经发现如何替换session操作类来实现session的redis管理,但是已经有的轮子直接拿来即可,首先我们要引入redis的jar
<!-- shiro-redis -->
<dependency>
<groupId>org.crazycake</groupId>
<artifactId>shiro-redis</artifactId>
<version>3.1.0</version>
</dependency>
我在引入上面的包后会报错,引入这个解决,未报错不用引入,报错原因未深究
err:Missing artifact com.sun:tools:jar:1.8.0
<!-- tools -->
<dependency>
<groupId>com.sun</groupId>
<artifactId>tools</artifactId>
<version>1.8.0</version>
<scope>system</scope>
<systemPath>${env.JAVA_HOME}/lib/tools.jar</systemPath>
<optional>true</optional>
</dependency>
5.2、新增redis配置
在boot配置文件中新增如下文件,我采用的是yml文件,其他格式需要自行转换
application.yml
#redis
redis:
#redis机器ip
host: 127.0.0.1
#redis端口
port: 6379
#redis密码
password: 123456
#默认数据库
database: 10
#redis超时时间(毫秒),如果不设置,取默认值2000
timeout: 10000
5.3、ShiroConfig配置
- 新增读取配置项属性及redis实例和redisdao实例
@Value("${redis.host}")
private String host;
@Value("${redis.port}")
private int port;
@Value("${redis.password}")
private String password;
@Value("${redis.database}")
private int database;
@Value("${redis.timeout}")
private int timeout;
@Bean
public RedisManager redisManager() {
RedisManager redisManager = new RedisManager();
redisManager.setHost(host);// 主机地址
redisManager.setPort(port);// 端口
redisManager.setPassword(password);// 访问密码
redisManager.setDatabase(database);// 默认数据库
redisManager.setTimeout(timeout);// 过期时间
return redisManager;
}
/**
* SessionDAO的作用是为Session提供CRUD并进行持久化的一个shiro组件 MemorySessionDAO 直接在内存中进行会话维护
* EnterpriseCacheSessionDAO
* 提供了缓存功能的会话维护,默认情况下使用MapCache实现,内部使用ConcurrentHashMap保存缓存的会话。
*
* @return
*/
@Bean
public RedisSessionDAO redisSessionDAO() {
RedisSessionDAO redisSessionDao = new RedisSessionDAO();
redisSessionDao.setKeyPrefix("shiro-session");//配置session前缀
redisSessionDao.setSessionIdGenerator(sessionIdGenerator());
redisSessionDao.setRedisManager(redisManager());
// session在redis中的保存时间,最好大于session会话超时时间
redisSessionDao.setExpire(timeout);
return redisSessionDao;
}
- 配置自定义sessionid生成器
/**
* 配置会话ID生成器
*
* @return
*/
@Bean
public SessionIdGenerator sessionIdGenerator() {
return new JavaUuidSessionIdGenerator();
}
- 配置session监听器
/**
* 配置session监听
*
* @return
*/
@Bean
public MySessionListener sessionListener() {
MySessionListener sessionListener = new MySessionListener();
return sessionListener;
}
MySessionListener 类
import org.apache.shiro.session.Session;
import org.apache.shiro.session.SessionListener;
/**
*
* @ClassName: MySessionListener
* @Description 统计session数量
* @version
* @author JH
* @date 2019年9月2日 上午11:15:38
*/
public class MySessionListener implements SessionListener {
private final AtomicInteger sessionCount = new AtomicInteger(0);
/**
* 登录
*/
@Override
public void onStart(Session session) {
sessionCount.incrementAndGet();
System.out.println("登录,有效session数量:"+sessionCount.get());
}
/**
* 登出
*/
@Override
public void onStop(Session session) {
sessionCount.decrementAndGet();
System.out.println("登出,有效session数量:"+sessionCount.get());
}
/**
* session过期
*/
@Override
public void onExpiration(Session session) {
sessionCount.decrementAndGet();
System.out.println("session过期,有效session数量:"+sessionCount.get());
}
}
- 配置自定义session的cookie,替换JSESSIONID
/**
* 配置保存sessionId的cookie 注意:这里的cookie 不是上面的记住我 cookie 记住我需要一个cookie session管理
* 也需要自己的cookie 默认为: JSESSIONID 问题: 与SERVLET容器名冲突,重新定义为sid
*
* @return
*/
@Bean("sessionIdCookie")
public SimpleCookie sessionIdCookie() {
// 这个参数是cookie的名称
SimpleCookie simpleCookie = new SimpleCookie("REDIS-SESSION");
// setcookie的httponly属性如果设为true的话,会增加对xss防护的安全系数。它有以下特点:
// setcookie()的第七个参数
// 设为true后,只能通过http访问,javascript无法访问
// 防止xss读取cookie
simpleCookie.setHttpOnly(true);
simpleCookie.setPath("/");
// maxAge=-1表示浏览器关闭时失效此Cookie
simpleCookie.setMaxAge(-1);
return simpleCookie;
}
- 配置会话管理器
/**
* 配置会话管理器,设定会话超时及保存
*
* @return
*/
@Bean
public SessionManager sessionManager() {
DefaultWebSessionManager sessionManager = new DefaultWebSessionManager();
Collection<SessionListener> listeners = new ArrayList<SessionListener>();
// 配置监听
listeners.add(sessionListener());
sessionManager.setSessionListeners(listeners);
sessionManager.setSessionIdCookie(sessionIdCookie());
sessionManager.setSessionDAO(redisSessionDAO());
// 全局会话超时时间(单位毫秒),默认30分钟 暂时设置为10秒钟 用来测试
sessionManager.setGlobalSessionTimeout(1800000);//单位毫秒
// 是否开启删除无效的session对象 默认为true
sessionManager.setDeleteInvalidSessions(true);
// 是否开启定时调度器进行检测过期session 默认为true
sessionManager.setSessionValidationSchedulerEnabled(true);
// 设置session失效的扫描时间, 清理用户直接关闭浏览器造成的孤立会话 默认为 1个小时
// 设置该属性 就不需要设置 ExecutorServiceSessionValidationScheduler
// 底层也是默认自动调用ExecutorServiceSessionValidationScheduler
sessionManager.setSessionValidationInterval(3600000);//单位毫秒
// 取消url 后面的 JSESSIONID,设置为false为取消
sessionManager.setSessionIdUrlRewritingEnabled(false);
return sessionManager;
}
- 将会话管理器交给 securityManager 管理
/**
* 注入 securityManager
*/
@Bean
public SecurityManager securityManager() {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(myShiroRealm());
securityManager.setRememberMeManager(rememberMeManager());
securityManager.setCacheManager(myEhCacheManager());// 将缓存管理交给ehCache
securityManager.setSessionManager(sessionManager());//将session管理交给reids
return securityManager;
}
至此,shiro的session交给redis管理已经配置完成了,很简单啊!
6、验证猜想
- 6.1、在之前的源码跟踪中,我们猜想重写一个类继承 AbstractSessionDAO* ,替换 MemorySessionDAO ,在引入 shiro-redis jar包后,查看是用的redisSessionDAO关系图,验证猜想!
- 6.2、部分 RedisSessionDAO 源码
@Override
protected Serializable doCreate(Session session) {
if (session == null) {
logger.error("session is null");
throw new UnknownSessionException("session is null");
}
Serializable sessionId = this.generateSessionId(session);
this.assignSessionId(session, sessionId);
this.saveSession(session);
return sessionId;
}
@Override
protected Session doReadSession(Serializable sessionId) {
if (sessionId == null) {
logger.warn("session id is null");
return null;
}
Session s = getSessionFromThreadLocal(sessionId);
if (s != null) {
return s;
}
logger.debug("read session from redis");
try {
s = (Session) valueSerializer.deserialize(redisManager.get(keySerializer.serialize(getRedisSessionKey(sessionId))));
setSessionToThreadLocal(sessionId, s);
} catch (SerializationException e) {
logger.error("read session error. settionId=" + sessionId);
}
return s;
}
private void setSessionToThreadLocal(Serializable sessionId, Session s) {
Map<Serializable, SessionInMemory> sessionMap = (Map<Serializable, SessionInMemory>) sessionsInThread.get();
if (sessionMap == null) {
sessionMap = new HashMap<Serializable, SessionInMemory>();
sessionsInThread.set(sessionMap);
}
SessionInMemory sessionInMemory = new SessionInMemory();
sessionInMemory.setCreateTime(new Date());
sessionInMemory.setSession(s);
sessionMap.put(sessionId, sessionInMemory);
}
- 6.3 redis集群
在使用shiro-redis工具包的时候,惊讶的发现了它已经为我们提供了redis集群的支持,后续会进行测试。
7、测试
- 打开登录页面,查看cookie是否被修改(已修改为我们设置的名称)
- session监听器控制台打印人数
3.redis可视化工具(成功将session写入redis中)
8、源码
- 需要源码可以点击这里获取!