在Shiro中我们可以通过org.apache.shiro.session.mgt.eis.SessionDAO对象的getActiveSessions()方法方便的获取到当前所有有效的Session对象。通过这些Session对象,我们可以实现一些比较有趣的功能,比如查看当前系统的在线人数,查看这些在线用户的一些基本信息,强制让某个用户下线等。

为了达到这几个目标,我们在现有的Spring Boot Shiro项目基础上进行一些改造(缓存使用Ehcache)。

更改ShiroConfig

为了能够在Spring Boot中使用SessionDao,我们在ShiroConfig中配置该Bean:

  1. @Bean
  2. public SessionDAO sessionDAO() {
  3. MemorySessionDAO sessionDAO = new MemorySessionDAO();
  4. return sessionDAO;
  5. }

如果使用的是Redis作为缓存实现,那么SessionDAO则为RedisSessionDAO

  1. @Bean
  2. public RedisSessionDAO sessionDAO() {
  3. RedisSessionDAO redisSessionDAO = new RedisSessionDAO();
  4. redisSessionDAO.setRedisManager(redisManager());
  5. return redisSessionDAO;
  6. }

在Shiro中,SessionDao通过org.apache.shiro.session.mgt.SessionManager进行管理,所以继续在ShiroConfig中配置SessionManager

  1. @Bean
  2. public SessionManager sessionManager() {
  3. DefaultWebSessionManager sessionManager = new DefaultWebSessionManager();
  4. Collection<SessionListener> listeners = new ArrayList<SessionListener>();
  5. listeners.add(new ShiroSessionListener());
  6. sessionManager.setSessionListeners(listeners);
  7. sessionManager.setSessionDAO(sessionDAO());
  8. return sessionManager;
  9. }

其中ShiroSessionListenerorg.apache.shiro.session.SessionListener接口的手动实现,所以接下来定义一个该接口的实现:

  1. public class ShiroSessionListener implements SessionListener{
  2. private final AtomicInteger sessionCount = new AtomicInteger(0);
  3. @Override
  4. public void onStart(Session session) {
  5. sessionCount.incrementAndGet();
  6. }
  7. @Override
  8. public void onStop(Session session) {
  9. sessionCount.decrementAndGet();
  10. }
  11. @Override
  12. public void onExpiration(Session session) {
  13. sessionCount.decrementAndGet();
  14. }
  15. }

其维护着一个原子类型的Integer对象,用于统计在线Session的数量。

定义完SessionManager后,还需将其注入到SecurityManager中:

  1. @Bean
  2. public SecurityManager securityManager(){
  3. DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
  4. securityManager.setRealm(shiroRealm());
  5. ...
  6. securityManager.setSessionManager(sessionManager());
  7. return securityManager;
  8. }

UserOnline

配置完ShiroConfig后,我们可以创建一个UserOnline实体类,用于描述每个在线用户的基本信息:

  1. public class UserOnline implements Serializable{
  2. private static final long serialVersionUID = 3828664348416633856L;
  3. // session id
  4. private String id;
  5. // 用户id
  6. private String userId;
  7. // 用户名称
  8. private String username;
  9. // 用户主机地址
  10. private String host;
  11. // 用户登录时系统IP
  12. private String systemHost;
  13. // 状态
  14. private String status;
  15. // session创建时间
  16. private Date startTimestamp;
  17. // session最后访问时间
  18. private Date lastAccessTime;
  19. // 超时时间
  20. private Long timeout;
  21. // get set略
  22. }

Service

创建一个Service接口,包含查看所有在线用户和根据SessionId踢出用户抽象方法:

  1. public interface SessionService {
  2. List<UserOnline> list();
  3. boolean forceLogout(String sessionId);
  4. }

其具体实现:

  1. @Service("sessionService")
  2. public class SessionServiceImpl implements SessionService {
  3. @Autowired
  4. private SessionDAO sessionDAO;
  5. @Override
  6. public List<UserOnline> list() {
  7. List<UserOnline> list = new ArrayList<>();
  8. Collection<Session> sessions = sessionDAO.getActiveSessions();
  9. for (Session session : sessions) {
  10. UserOnline userOnline = new UserOnline();
  11. User user = new User();
  12. SimplePrincipalCollection principalCollection = new SimplePrincipalCollection();
  13. if (session.getAttribute(DefaultSubjectContext.PRINCIPALS_SESSION_KEY) == null) {
  14. continue;
  15. } else {
  16. principalCollection = (SimplePrincipalCollection) session
  17. .getAttribute(DefaultSubjectContext.PRINCIPALS_SESSION_KEY);
  18. user = (User) principalCollection.getPrimaryPrincipal();
  19. userOnline.setUsername(user.getUserName());
  20. userOnline.setUserId(user.getId().toString());
  21. }
  22. userOnline.setId((String) session.getId());
  23. userOnline.setHost(session.getHost());
  24. userOnline.setStartTimestamp(session.getStartTimestamp());
  25. userOnline.setLastAccessTime(session.getLastAccessTime());
  26. Long timeout = session.getTimeout();
  27. if (timeout == 0l) {
  28. userOnline.setStatus("离线");
  29. } else {
  30. userOnline.setStatus("在线");
  31. }
  32. userOnline.setTimeout(timeout);
  33. list.add(userOnline);
  34. }
  35. return list;
  36. }
  37. @Override
  38. public boolean forceLogout(String sessionId) {
  39. Session session = sessionDAO.readSession(sessionId);
  40. session.setTimeout(0);
  41. return true;
  42. }
  43. }

通过SessionDao的getActiveSessions()方法,我们可以获取所有有效的Session,通过该Session,我们还可以获取到当前用户的Principal信息。

值得说明的是,当某个用户被踢出后(Session Time置为0),该Session并不会立刻从ActiveSessions中剔除,所以我们可以通过其timeout信息来判断该用户在线与否。

如果使用的Redis作为缓存实现,那么,forceLogout()方法需要稍作修改:session.setTimeout(0);需要改成sessionDAO.delete(session)

  1. @Override
  2. public boolean forceLogout(String sessionId) {
  3. Session session = sessionDAO.readSession(sessionId);
  4. sessionDAO.delete(session);
  5. return true;
  6. }

Controller

定义一个SessionContoller,用于处理Session的相关操作:

  1. @Controller
  2. @RequestMapping("/online")
  3. public class SessionController {
  4. @Autowired
  5. SessionService sessionService;
  6. @RequestMapping("index")
  7. public String online() {
  8. return "online";
  9. }
  10. @ResponseBody
  11. @RequestMapping("list")
  12. public List<UserOnline> list() {
  13. return sessionService.list();
  14. }
  15. @ResponseBody
  16. @RequestMapping("forceLogout")
  17. public ResponseBo forceLogout(String id) {
  18. try {
  19. sessionService.forceLogout(id);
  20. return ResponseBo.ok();
  21. } catch (Exception e) {
  22. e.printStackTrace();
  23. return ResponseBo.error("踢出用户失败");
  24. }
  25. }
  26. }

页面

我们编写一个online.html页面,用于展示所有在线用户的信息:

  1. <!DOCTYPE html>
  2. <html xmlns:th="http://www.thymeleaf.org">
  3. <head>
  4. <meta charset="UTF-8">
  5. <title>在线用户管理</title>
  6. <script th:src="@{/js/jquery-1.11.1.min.js}"></script>
  7. <script th:src="@{/js/dateFormat.js}"></script>
  8. </head>
  9. <body>
  10. <h3>在线用户数:<span id="onlineCount"></span></h3>
  11. <table>
  12. <tr>
  13. <th>序号</th>
  14. <th>用户名称</th>
  15. <th>登录时间</th>
  16. <th>最后访问时间</th>
  17. <th>主机</th>
  18. <th>状态</th>
  19. <th>操作</th>
  20. </tr>
  21. </table>
  22. <a th:href="@{/index}">返回</a>
  23. </body>
  24. <script th:inline="javascript">
  25. var ctx = [[@{/}]];
  26. $.get(ctx + "online/list", {}, function(r){
  27. var length = r.length;
  28. $("#onlineCount").text(length);
  29. var html = "";
  30. for(var i = 0; i < length; i++){
  31. html += "<tr>"
  32. + "<td>" + (i+1) + "</td>"
  33. + "<td>" + r[i].username + "</td>"
  34. + "<td>" + new Date(r[i].startTimestamp).Format("yyyy-MM-dd hh:mm:ss") + "</td>"
  35. + "<td>" + new Date(r[i].lastAccessTime).Format("yyyy-MM-dd hh:mm:ss") + "</td>"
  36. + "<td>" + r[i].host + "</td>"
  37. + "<td>" + r[i].status + "</td>"
  38. + "<td><a href='#' onclick='offline(\"" + r[i].id + "\",\"" + r[i].status +"\")'>下线</a></td>"
  39. + "</tr>";
  40. }
  41. $("table").append(html);
  42. },"json");
  43. function offline(id,status){
  44. if(status == "离线"){
  45. alert("该用户已是离线状态!!");
  46. return;
  47. }
  48. $.get(ctx + "online/forceLogout", {"id": id}, function(r){
  49. if (r.code == 0) {
  50. alert('该用户已强制下线!');
  51. location.href = ctx + 'online/index';
  52. } else {
  53. alert(r.msg);
  54. }
  55. },"json");
  56. }
  57. </script>
  58. </html>

在index.html中加入该页面的入口:

  1. ...
  2. <body>
  3. <p>你好![[${user.userName}]]</p>
  4. <p shiro:hasRole="admin">你的角色为超级管理员</p>
  5. <p shiro:hasRole="test">你的角色为测试账户</p>
  6. <div>
  7. <a shiro:hasPermission="user:user" th:href="@{/user/list}">获取用户信息</a>
  8. <a shiro:hasPermission="user:add" th:href="@{/user/add}">新增用户</a>
  9. <a shiro:hasPermission="user:delete" th:href="@{/user/delete}">删除用户</a>
  10. </div>
  11. <a shiro:hasRole="admin" th:href="@{/online/index}">在线用户管理</a>
  12. <a th:href="@{/logout}">注销</a>
  13. </body>
  14. ...

测试

启动项目,在Opera浏览器中使用mrbird账户访问:

Spring Boot Shiro在线会话管理 - 图1

在FireFox浏览器中使用tester账户访问:

Spring Boot Shiro在线会话管理 - 图2

然后在mrbird主界面点击“在线用户管理”:

Spring Boot Shiro在线会话管理 - 图3

显示的信息符合我们的预期,点击tester的下线按钮,强制将其踢出:

Spring Boot Shiro在线会话管理 - 图4

回到tester用户的主界面,点击“查看用户信息”,会发现页面已经被重定向到login页面,因为其Session已经失效!

再次刷新mrbird的online页面,显示如下:

Spring Boot Shiro在线会话管理 - 图5

Ehcache版源码:https://github.com/wuyouzhuguli/Spring-Boot-Demos/tree/master/17.Spring-Boot-Shiro-Session