其实关于Shiro的一些学习笔记很早就该写了,因为懒癌和拖延症晚期一直没有落实,直到今天公司的一个项目碰到了在集群环境的单点登录频繁掉线的问题,为了解决这个问题,Shiro相关的文档和教程没少翻。最后问题解决了,但我觉得我也是时候来做一波Shiro学习笔记了。
本篇是Shiro系列第二篇,使用Shiro基于Redis实现分布式环境下的Session共享。在讲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和Cookie,我们使得无状态的HTTP协议间接的变成了有状态的了,可以实现保持登录,存储用户信息,购物车等等功能。但是随着服务访问人数的增多,单台服务器已经不足以应付所有的请求了,必须部署集群环境。但是随着集群环境的出现,追踪用户状态的问题又开始出现问题,之前用户在A服务器登录,A服务器保存了用户信息,但是下一次请求发送到B服务器去了,这时候B服务器是不知道用户在A服务器登录的事情的,它虽然也能拿到用户请求Cookie中的SessionId,但是在B服务根据这个SessionId找不到对应的Session,B服务器就会认为用户没有登录,需要用户重新登录,这对用户来说是没办法接受的。
这时候常见的有两种方式解决这个问题,第一种是让这个用户所有的请求都发送到A服务器,比如根据IP地址做一些列算法将所有用户分配到不同的服务器上去,让每个用户只访问其中的一台服务器。这种做法可行,但是后续也会产生其它问题,更好的做法是第二种,将所有的服务器上的Session都做成共享的,A服务能拿到B服务器上的所有Session,同理B服务器也能获取A服务器所有的Session,这样上面的问题就不存在了。
上一篇已经通过Shiro实现了用户登录和权限管理,Shiro的登录也是基于Session的,默认情况下Session是保存在内存中。在集群环境下,我们仅仅需要继承AbstractSessionDAO实现一下几个Session管理的方法就可以很方便的实现Session共享。具体的Session管理方式(共享)由什么方式实现由我们的代码决定,这里我们使用高性能的Redis来管理Session。
继承AbstractSessionDAO实现五个Session管理的方法,就是Session的增删改查和列表方法,代码很简单:
@Component public class RedisSessionDao extends AbstractSessionDAO { @Value("${session.redis.expireTime}") private long expireTime; @Autowired private RedisTemplate redisTemplate; @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); } @Override public void update(Session session) throws UnknownSessionException { if (session != null && session.getId() != null) { session.setTimeout(expireTime * 1000); redisTemplate.opsForValue().set(session.getId(), session, expireTime, TimeUnit.SECONDS); } } @Override public void delete(Session session) { if (session != null && session.getId() != null) { redisTemplate.opsForValue().getOperations().delete(session.getId()); } } @Override public Collection<Session> getActiveSessions() { return redisTemplate.keys("*"); } }
配置文件中添加上面用到的配置
###redis连接配置 spring.redis.host=localhost spring.redis.port=6379 spring.redis.password=foobared ### Session过期时间(秒) session.redis.expireTime=3600
向SessionManager中注入我们自定义的RedisSessionDao并设置过期时间等相关参数
@Bean public DefaultWebSessionManager defaultWebSessionManager(RedisSessionDao redisSessionDao) { DefaultWebSessionManager sessionManager = new DefaultWebSessionManager(); sessionManager.setGlobalSessionTimeout(expireTime * 1000); sessionManager.setDeleteInvalidSessions(true); sessionManager.setSessionDAO(redisSessionDao); sessionManager.setSessionValidationSchedulerEnabled(true); sessionManager.setDeleteInvalidSessions(true); /** * 修改Cookie中的SessionId的key,默认为JSESSIONID,自定义名称 */ sessionManager.setSessionIdCookie(new SimpleCookie("JSESSIONID")); return sessionManager; }
将SessionManager注入Shiro的安全管理器SecurityManager
@Bean public SecurityManager securityManager(UserAuthorizingRealm userRealm, RedisSessionDao redisSessionDao) { DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager(); securityManager.setRealm(userRealm); // 取消Cookie中的RememberMe参数 securityManager.setRememberMeManager(null); securityManager.setSessionManager(defaultWebSessionManager(redisSessionDao)); return securityManager; }
OK,基于Redis实现的Session共享就完成了,是不是简单的不可思议。
注:基于网络传输的对象请实现Serializable序列化接口,比如User类。
使用这套代码使用不同的端口跑两套服务,访问两台服务器获取用户信息的接口,未登录状态毫无疑问都会调到登录页去:
在任意一台服务器登录:
再次分别访问两台服务器获取用户信息的接口:
最后继续放上项目代码,代码是很早之前的,部分代码经过整理后上传。
Gitee: https://gitee.com/guitu18/ShiroDemo
GitHub: https://github.com/guitu18/ShiroDemo
本篇结束,简直不要太简单是不是,其实这主要是因为大部分工作Shiro都帮我们做了,细节的东西都被Shiro隐藏起来,我们仅仅需要添加一些简单的配置就可以实现强大的功能,这就是框架的好处。
但是 作为一个程序员,仅仅调用一个方法或者添加一个注解就实现了一个很强大的功能,而我们却看不到一个if判断和for循环的时候心里应该是非常不踏实的 。所以,我们不仅要学会使用框架,更要去深入理解框架,至少要知道为什么他能帮我们实现一大堆功能,这样才能让我们感觉脚踏实地。下一篇,深入Shiro源码看看,可能需要酝酿一下想想笔记怎么写。