首发地址: https://www.guitu18.com/post/2019/07/28/44.html
本篇是 Shiro 系列第二篇, 使用 Shiro 基于 Redis 实现分布式环境下的 Session 共享. 在讲 Session 共享之前先说一下为什么要做 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 共享
通过 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 中. Redis 大法好! YES~
自定义 RedisSessionDAO
理解了原理之后就很容易办事了, 继承 AbstractSessionDAO 后实现 Session 增删改查的几个方法, 然后再分布式系统中所有的项目再需要存储或获取 Session 时都会走 Redis 操作, 这样就做到了集群环境的 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
注入 RedisSessionDao
上面只是我们自己实现的管理 Session 的方式, 现在需要将其注入 SessionManager 中, 并设置过期时间等相关参数.
- @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 中, 前面说过, 我们围绕安全相关的所有操作, 都需要与 SecurityManager 打交道, 这位才是 Shiro 中真正的老大哥.
- @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 类.
测试
将这套代码用不同的端口跑两套服务(理论上跑多少套都可以只要你的配置够用), 访问两台服务器获取用户信息的接口, 未登录状态毫无疑问都会跳到登录页去:
在任意一台服务器上调用登录接口登录:
登录成功后再次分别访问两台服务器获取用户信息的接口:
如此, 分布式环境 Session 共享完美实现. 最后继续放上项目代码, 代码还是很早之前的, 部分代码为了配合此篇笔记经过修改整理后上传.
- Gitee:https://gitee.com/guitu18/ShiroDemo
- GitHub:https://github.com/guitu18/ShiroDemo
本篇结束, 简直不要太简单是不是, 其实这主要是因为大部分工作 Shiro 都帮我们做了, 细节的东西都被 Shiro 隐藏起来, 我们仅仅需要添加一些简单的配置就可以实现强大的功能, 这就是框架的好处.
但是作为一个程序员, 仅仅调用一个方法或者添加一个注解就实现了一套很强大的功能, 而我们却看不到一个 if 判断和 for 循环的时候心里应该是非常不踏实的. 我们不仅要学会使用框架, 更要去深入理解框架, 至少要知道为什么我们就加了一个注解框架就能帮我们实现一大堆功能, 只有这样才能让我们感到脚踏实地. 下一篇, 深入 Shiro 源码看看, 可能需要酝酿一下想想笔记怎么写.
posted on 2019-07-29 09:30 夜月归途 阅读(...) 评论(...) 编辑 收藏
来源: https://www.cnblogs.com/guitu18/p/11262106.html