一、背景

Session 共享有多种方案,之前写过《Spring Session 实现 Tomcat 集群的 Session 共享》 文章,功能实现起来非常简单和方便。

最近在学习 Shiro 框架,Shiro 也提供了会话管理的功能。如果项目中选用 Shiro 作为权限控制的方案,同时项目又需要集群,那么可以自定义 sessionDAO 来实现 Session 共享。

二、实现

JDK:1.8
容器:Tomcat 8
Session 存储容器:Redis 3.2.0

测试环境与测试 Spring Session 时的一样,将项目部署到同一台虚拟机上的 2 个 tomcat 中,使用 8080 和 8081 端口启动。

下边列出主要配置,Shiro 所依赖的 jar 配置和运行配置忽略,具体代码可以下载由下文提供的源码进行查看。

# 2.1 applicationContext-shiro.xml

  1. <?xml version="1.0" encoding="UTF-8"?>
  2. <beans xmlns="http://www.springframework.org/schema/beans"
  3. xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  4. xmlns:context="http://www.springframework.org/schema/context"
  5. xmlns:tx="http://www.springframework.org/schema/tx"
  6. xmlns:aop="http://www.springframework.org/schema/aop"
  7. xsi:schemaLocation="http://www.springframework.org/schema/beans
  8. http://www.springframework.org/schema/beans/spring-beans-4.0.xsd
  9. http://www.springframework.org/schema/context
  10. http://www.springframework.org/schema/context/spring-context-4.0.xsd
  11. http://www.springframework.org/schema/tx
  12. http://www.springframework.org/schema/tx/spring-tx-4.0.xsd
  13. http://www.springframework.org/schema/aop
  14. http://www.springframework.org/schema/aop/spring-aop-4.0.xsd">
  15. <context:component-scan base-package="com.light.dao.*"/>
  16. <!-- redis 连接池 -->
  17. <bean id="jedisPoolConfig" class="redis.clients.jedis.JedisPoolConfig">
  18. <property name="maxTotal" value="20"></property>
  19. <property name="maxIdle" value="1"></property>
  20. </bean>
  21. <!-- redis 连接工厂 -->
  22. <bean id="jedisConnectionFactory" class="org.springframework.data.redis.connection.jedis.JedisConnectionFactory"
  23. destroy-method="destroy">
  24. <property name="hostName" value="192.168.2.11"/>
  25. <property name="port" value="6379"/>
  26. <property name="timeout" value="5000"/>
  27. <property name="password" value=""/>
  28. <property name="usePool" value="true"/>
  29. <property name="poolConfig" ref="jedisPoolConfig"/>
  30. </bean>
  31. <!-- redis 模板 -->
  32. <bean id="redisTemplate" class="org.springframework.data.redis.core.RedisTemplate" >
  33. <property name="connectionFactory" ref="jedisConnectionFactory" />
  34. </bean >
  35. <!-- Shiro 的Web过滤器 -->
  36. <bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
  37. <property name="securityManager" ref="securityManager" />
  38. <property name="loginUrl" value="/index.jsp" />
  39. <!-- 过虑器链定义,从上向下顺序执行,一般将/**放在最下边 -->
  40. <property name="filterChainDefinitions">
  41. <value>
  42. /resources/**=anon
  43. /login=anon
  44. </value>
  45. </property>
  46. </bean>
  47. <!-- 安全管理器 -->
  48. <bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
  49. <property name="sessionManager" ref="sessionManager" />
  50. </bean>
  51. <!-- 会话管理器 -->
  52. <bean id="sessionManager" class="org.apache.shiro.web.session.mgt.DefaultWebSessionManager">
  53. <property name="sessionDAO" ref="sessionDAO"></property>
  54. </bean>
  55. <!-- 自定义 sessionDAO -->
  56. <bean id="sessionDAO" class="com.light.dao.CustomSessionDAO"></bean>
  57. </beans>

# 2.2 自定义 sessionDAO

自定义 sessionDAO 需要继承 AbstractSessionDAO 类来重写 session 的 CRUD。

  1. public class CustomSessionDAO extends AbstractSessionDAO {
  2. private static final int EXPIRE_TIME = 600;
  3. @Resource(name="redisTemplate")
  4. private RedisTemplate<String,Object> redisTemplate;
  5. public void update(Session session) throws UnknownSessionException {
  6. this.redisTemplate.opsForValue().set(
  7. session.getId().toString(),
  8. session,
  9. EXPIRE_TIME,
  10. TimeUnit.SECONDS);
  11. }
  12. public void delete(Session session) {
  13. this.redisTemplate.delete(session.getId().toString());
  14. }
  15. public Collection<Session> getActiveSessions() {
  16. // TODO
  17. return null;
  18. }
  19. @Override
  20. protected Serializable doCreate(Session session) {
  21. // 生成 sessionId
  22. Serializable sessionId = this.generateSessionId(session);
  23. // session 绑定 sessionId
  24. this.assignSessionId(session, sessionId);
  25. this.redisTemplate.opsForValue().set(
  26. session.getId().toString(),
  27. session,
  28. EXPIRE_TIME,
  29. TimeUnit.SECONDS);
  30. return sessionId;
  31. }
  32. @Override
  33. protected Session doReadSession(Serializable sessionId) {
  34. Session session = (Session) this.redisTemplate.opsForValue().get(sessionId.toString());
  35. if (session != null) {
  36. this.redisTemplate.opsForValue().set(
  37. session.getId().toString(),
  38. session,
  39. EXPIRE_TIME,
  40. TimeUnit.SECONDS);
  41. }
  42. return session;
  43. }
  44. }

CustomSessionDAO 类是实现 session 共享的核心。

# 3.3 后端代码

  1. @Controller
  2. public class LoginController {
  3. @Autowired
  4. private SecurityManager sm;
  5. @RequestMapping("login")
  6. public String login(String userName, String password,HttpServletRequest request) {
  7. // 首次登录
  8. if ("admin".equals(userName) && "admin".equals(password)) {
  9. SecurityUtils.setSecurityManager(sm);
  10. Subject subject = SecurityUtils.getSubject();
  11. // 使用 shiro 的 session 保存数据
  12. Session session = subject.getSession();
  13. session.setAttribute("userName", userName);
  14. return "manageUI";
  15. }
  16. // 如果已经登录过,从另一个 tomcat 访问该方法,跳转到 manageUI 页面可以查看 session 信息
  17. if ("".equals(userName) && "".equals(password)) {
  18. return "manageUI";
  19. }
  20. return "redirect:/index.jsp";
  21. }
  22. @RequestMapping("logout")
  23. public String logout(HttpSession session) {
  24. session.removeAttribute("userName");
  25. session.removeAttribute("url");
  26. return "redirect:/index.jsp";
  27. }
  28. }

注意:后端代码使用的是 Shiro 提供的 session API 进行保存数据。

# 3.4 前端代码

index.jsp 页面:

  1. <%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
  2. <!DOCTYPE html>
  3. <html lang="zh">
  4. <head>
  5. <meta charset="utf-8">
  6. <meta http-equiv="X-UA-Compatible" content="IE=edge">
  7. <meta name="viewport" content="width=device-width, initial-scale=1">
  8. <meta name="description" content="">
  9. <meta name="author" content="">
  10. <title>登陆界面</title>
  11. <link href="/resources/css/bootstrap.min.css" rel="stylesheet">
  12. <style>
  13. html {
  14. background: url("/resources/images/bg.png") no-repeat center center;
  15. }
  16. label {
  17. color: #fff;
  18. }
  19. .container {
  20. position:absolute;
  21. top:50%;
  22. left:50%;
  23. margin-top: -115px;
  24. margin-left: -250px;
  25. width: 500px;
  26. height:230px;
  27. padding:50px;
  28. border: 2px solid #eee;
  29. border-radius: 5px;
  30. box-shadow:5px 5px 16px #000;
  31. }
  32. </style>
  33. </head>
  34. <body>
  35. <div class="container">
  36. <form class="form-horizontal" role="form" action="/login" method="post">
  37. <div class="form-group">
  38. <label for="inputEmail3" class="col-sm-2 control-label">用户名</label>
  39. <div class="col-sm-10">
  40. <input type="text" class="form-control" name="userName" placeholder="用户名">
  41. </div>
  42. </div>
  43. <div class="form-group">
  44. <label for="inputPassword3" class="col-sm-2 control-label">密码</label>
  45. <div class="col-sm-10">
  46. <input type="password" class="form-control" name="password" placeholder="密码">
  47. </div>
  48. </div>
  49. <div class="form-group">
  50. <div class="col-sm-offset-2 col-sm-10">
  51. <button type="submit" class="btn btn-primary" style="width: 100%">登陆</button>
  52. </div>
  53. </div>
  54. </form>
  55. </div>
  56. </body>
  57. </html>

manageUI.jsp 页面:

  1. <%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
  2. <!DOCTYPE html>
  3. <html lang="zh">
  4. <head>
  5. <meta charset="utf-8">
  6. <meta http-equiv="X-UA-Compatible" content="IE=edge">
  7. <meta name="viewport" content="width=device-width, initial-scale=1">
  8. <meta name="description" content="">
  9. <meta name="author" content="">
  10. <title>管理界面</title>
  11. <link href="/resources/css/bootstrap.min.css" rel="stylesheet">
  12. </head>
  13. <body>
  14. <div class="container">
  15. <div class="jumbotron">
  16. <h3>测试 Shiro 实现 session 共享</h3>
  17. <h3>端口为 8080 的页面</h3>
  18. <h3>用户名:${sessionScope.userName}(session 域数据)</h3>
  19. <p><a class="btn btn-lg btn-success" href="/logout" role="button">注销</a></p>
  20. </div>
  21. </div>
  22. </body>
  23. </html>

注意:8081 项目的页面需要改成 “端口为 8081 的页面”。

三、演示

测试步骤同样与测试 Spring Session 时的一致。

预期效果:

  1. 首先访问 8080 端口的项目并进行登陆操作,跳转到管理界面并显示保存的信息。

  2. 在同个浏览器中访问 8081 端口项目的页面,不需要输入账号密码直接点击登陆按钮,会直接跳转到管理界面。如果 session 实现了共享,那么在管理界面就可以查看由 8080 端口项目保存在 session 的信息。否则反之。

演示图如下:

总体来说,功能实现不算困难,但是比使用 Spring session 方案要麻烦一些,因为需要开发者自己实现 session 的 CRUD。正因为需要手动实现,从另方面考虑使用 Shiro 方案管理 session 会比较灵活。

四、源码下载

session-share