一、前言
在Shiro中我们可以通过org.apache.shiro.session.mgt.eis.SessionDAO对象的getActiveSessions()方法方便的获取到当前所有有效的Session对象。通过这些Session对象,我们可以实现一些比较有趣的功能,比如查看当前系统的在线人数,查看这些在线用户的一些基本信息,强制让某个用户下线等。
为了达到这几个目标,我们在现有的Spring Boot Shiro项目基础上进行一些改造(缓存使用Ehcache)。
二、更改ShiroConfig
为了能够在Spring Boot中使用SessionDao,我们在ShiroConfig中配置该Bean:
1 2 3 4 5
| @Bean public SessionDAO sessionDAO() { MemorySessionDAO sessionDAO = new MemorySessionDAO(); return sessionDAO; }
|
如果使用的是Redis作为缓存实现,那么SessionDAO则为RedisSessionDAO:
1 2 3 4 5 6
| @Bean public RedisSessionDAO sessionDAO() { RedisSessionDAO redisSessionDAO = new RedisSessionDAO(); redisSessionDAO.setRedisManager(redisManager()); return redisSessionDAO; }
|
在Shiro中,SessionDao通过org.apache.shiro.session.mgt.SessionManager进行管理,所以继续在ShiroConfig中配置SessionManager:
1 2 3 4 5 6 7 8 9
| @Bean public SessionManager sessionManager() { DefaultWebSessionManager sessionManager = new DefaultWebSessionManager(); Collection<SessionListener> listeners = new ArrayList<SessionListener>(); listeners.add(new ShiroSessionListener()); sessionManager.setSessionListeners(listeners); sessionManager.setSessionDAO(sessionDAO()); return sessionManager; }
|
其中ShiroSessionListener为org.apache.shiro.session.SessionListener接口的手动实现,所以接下来定义一个该接口的实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| public class ShiroSessionListener implements SessionListener{ private final AtomicInteger sessionCount = new AtomicInteger(0);
@Override public void onStart(Session session) { sessionCount.incrementAndGet(); }
@Override public void onStop(Session session) { sessionCount.decrementAndGet(); }
@Override public void onExpiration(Session session) { sessionCount.decrementAndGet(); } }
|
其维护着一个原子类型的Integer对象,用于统计在线Session的数量。
定义完SessionManager后,还需将其注入到SecurityManager中:
1 2 3 4 5 6 7 8 9 10
| @Bean public SecurityManager securityManager() { // 配置SecurityManager,并注入shiroRealm DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager(); securityManager.setRealm(shiroRealm()); securityManager.setRememberMeManager(rememberMeManager()); securityManager.setCacheManager(getEhCacheManager()); securityManager.setSessionManager(sessionManager()); return securityManager; }
|
三、UserOnline
配置完ShiroConfig后,我们可以创建一个UserOnline实体类,用于描述每个在线用户的基本信息:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| @Getter @Setter public class UserOnline implements Serializable { private static final long serialVersionUID = -5440372534300871948L; // session id private String id; // 用户id private String userId; // 用户名称 private String username; // 用户主机地址 private String host; // 用户登录时系统IP private String systemHost; // 状态 private String status; // session创建时间 private Date startTimestamp; // session最后访问时间 private Date lastAccessTime; // 超时时间 private Long timeout; }
|
四、Service
创建一个Service接口,包含查看所有在线用户和根据SessionId踢出用户抽象方法:
1 2 3 4
| public interface SessionService { List<UserOnline> list(); boolean forceLogout(String sessionId); }
|
其具体实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45
| @Service("sessionService") public class SessionServiceImpl implements SessionService { @Autowired private SessionDAO sessionDAO;
@Override public List<UserOnline> list() { List<UserOnline> list = new ArrayList<>(); Collection<Session> sessions = sessionDAO.getActiveSessions(); for (Session session : sessions) { UserOnline userOnline = new UserOnline(); User user = new User(); SimplePrincipalCollection principalCollection = new SimplePrincipalCollection(); if (session.getAttribute(DefaultSubjectContext.PRINCIPALS_SESSION_KEY) == null) { continue; } else { principalCollection = (SimplePrincipalCollection) session .getAttribute(DefaultSubjectContext.PRINCIPALS_SESSION_KEY); user = (User) principalCollection.getPrimaryPrincipal(); userOnline.setUsername(user.getUserName()); userOnline.setUserId(user.getId().toString()); } userOnline.setId((String) session.getId()); userOnline.setHost(session.getHost()); userOnline.setStartTimestamp(session.getStartTimestamp()); userOnline.setLastAccessTime(session.getLastAccessTime()); Long timeout = session.getTimeout(); if (timeout == 0l) { userOnline.setStatus("离线"); } else { userOnline.setStatus("在线"); } userOnline.setTimeout(timeout); list.add(userOnline); } return list; }
@Override public boolean forceLogout(String sessionId) { Session session = sessionDAO.readSession(sessionId); session.setTimeout(0); return true; } }
|
通过SessionDao的getActiveSessions()方法,我们可以获取所有有效的Session,通过该Session,我们还可以获取到当前用户的Principal信息。
值得说明的是,当某个用户被踢出后(Session Time置为0),该Session并不会立刻从ActiveSessions中剔除,所以我们可以通过其timeout信息来判断该用户在线与否。
如果使用的Redis作为缓存实现,那么,forceLogout()方法需要稍作修改:
1 2 3 4 5 6
| @Override public boolean forceLogout(String sessionId) { Session session = sessionDAO.readSession(sessionId); sessionDAO.delete(session); return true; }
|
五、Controller
定义一个SessionContoller,用于处理Session的相关操作:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| @Controller @RequestMapping("/online") public class SessionController { @Autowired SessionService sessionService;
@RequestMapping("index") public String online() { return "online"; }
@ResponseBody @RequestMapping("list") public List<UserOnline> list() { return sessionService.list(); }
@ResponseBody @RequestMapping("forceLogout") public ResponseBo forceLogout(String id) { try { sessionService.forceLogout(id); return ResponseBo.ok(); } catch (Exception e) { e.printStackTrace(); return ResponseBo.error("踢出用户失败"); } } }
|
六、页面
我们编写一个online.html页面,用于展示所有在线用户的信息:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59
| <!DOCTYPE html> <html xmlns:th="http://www.thymeleaf.org"> <head> <meta charset="UTF-8"> <title>在线用户管理</title> <script th:src="@{/js/jquery-1.11.1.min.js}"></script> <script th:src="@{/js/dateFormat.js}"></script> </head> <body> <h3>在线用户数:<span id="onlineCount"></span></h3> <table> <tr> <th>序号</th> <th>用户名称</th> <th>登录时间</th> <th>最后访问时间</th> <th>主机</th> <th>状态</th> <th>操作</th> </tr> </table> <a th:href="@{/index}">返回</a> </body> <script th:inline="javascript"> var ctx = [[@{/}]]; $.get(ctx + "online/list", {}, function(r){ var length = r.length; $("#onlineCount").text(length); var html = ""; for(var i = 0; i < length; i++){ html += "<tr>" + "<td>" + (i+1) + "</td>" + "<td>" + r[i].username + "</td>" + "<td>" + new Date(r[i].startTimestamp).Format("yyyy-MM-dd hh:mm:ss") + "</td>" + "<td>" + new Date(r[i].lastAccessTime).Format("yyyy-MM-dd hh:mm:ss") + "</td>" + "<td>" + r[i].host + "</td>" + "<td>" + r[i].status + "</td>" + "<td><a href='#' onclick='offline(\"" + r[i].id + "\",\"" + r[i].status +"\")'>下线</a></td>" + "</tr>"; } $("table").append(html); },"json");
function offline(id,status){ if(status == "离线"){ alert("该用户已是离线状态!!"); return; } $.get(ctx + "online/forceLogout", {"id": id}, function(r){ if (r.code == 0) { alert('该用户已强制下线!'); location.href = ctx + 'online/index'; } else { alert(r.msg); } },"json"); } </script> </html>
|
在index.html中加入该页面的入口:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| <!DOCTYPE html> <html xmlns:th="http://www.thymeleaf.org" xmlns:shiro="http://www.pollix.at/thymeleaf/shiro" > <head> <meta charset="UTF-8"> <title>首页</title> </head> <style> div { border: 1px dashed #ddd; padding: 10px; margin: 10px 10px 10px 0px; } </style> <body> <p>你好![[${user.userName}]]</p> <p shiro:hasRole="admin">你的角色为超级管理员</p> <p shiro:hasRole="test">你的角色为测试账户</p> <div> <a shiro:hasPermission="user:user" th:href="@{/user/list}">获取用户信息</a> <a shiro:hasPermission="user:add" th:href="@{/user/add}">新增用户</a> <a shiro:hasPermission="user:delete" th:href="@{/user/delete}">删除用户</a> </div> <a shiro:hasRole="admin" th:href="@{/online/index}">在线用户管理</a> <a th:href="@{/logout}">注销</a> </body> </html>
|
七、测试
启动项目,在FireFox浏览器中使用test账户访问:
在QQ浏览器中使用test账户访问:
然后在wno704主界面点击“在线用户管理”:
显示的信息符合我们的预期,点击test的下线按钮,强制将其踢出:
回到test用户的主界面,点击“查看用户信息”,会发现页面已经被重定向到login页面,因为其Session已经失效!
再次刷新wno704的online页面,显示如下: