在Shiro中我们可以通过org.apache.shiro.session.mgt.eis.SessionDAO对象的getActiveSessions()方法方便的获取到当前所有有效的Session对象。通过这些Session对象,我们可以实现一些比较有趣的功能,比如查看当前系统的在线人数,查看这些在线用户的一些基本信息,强制让某个用户下线等。
为了达到这几个目标,我们在现有的Spring Boot Shiro项目基础上进行一些改造(缓存使用Ehcache)。
更改ShiroConfig
为了能够在Spring Boot中使用SessionDao,我们在ShiroConfig中配置该Bean:
@Beanpublic SessionDAO sessionDAO() {MemorySessionDAO sessionDAO = new MemorySessionDAO();return sessionDAO;}
如果使用的是Redis作为缓存实现,那么SessionDAO则为RedisSessionDAO:
@Beanpublic RedisSessionDAO sessionDAO() {RedisSessionDAO redisSessionDAO = new RedisSessionDAO();redisSessionDAO.setRedisManager(redisManager());return redisSessionDAO;}
在Shiro中,SessionDao通过org.apache.shiro.session.mgt.SessionManager进行管理,所以继续在ShiroConfig中配置SessionManager:
@Beanpublic 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接口的手动实现,所以接下来定义一个该接口的实现:
public class ShiroSessionListener implements SessionListener{private final AtomicInteger sessionCount = new AtomicInteger(0);@Overridepublic void onStart(Session session) {sessionCount.incrementAndGet();}@Overridepublic void onStop(Session session) {sessionCount.decrementAndGet();}@Overridepublic void onExpiration(Session session) {sessionCount.decrementAndGet();}}
其维护着一个原子类型的Integer对象,用于统计在线Session的数量。
定义完SessionManager后,还需将其注入到SecurityManager中:
@Beanpublic SecurityManager securityManager(){DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();securityManager.setRealm(shiroRealm());...securityManager.setSessionManager(sessionManager());return securityManager;}
UserOnline
配置完ShiroConfig后,我们可以创建一个UserOnline实体类,用于描述每个在线用户的基本信息:
public class UserOnline implements Serializable{private static final long serialVersionUID = 3828664348416633856L;// session idprivate String id;// 用户idprivate String userId;// 用户名称private String username;// 用户主机地址private String host;// 用户登录时系统IPprivate String systemHost;// 状态private String status;// session创建时间private Date startTimestamp;// session最后访问时间private Date lastAccessTime;// 超时时间private Long timeout;// get set略}
Service
创建一个Service接口,包含查看所有在线用户和根据SessionId踢出用户抽象方法:
public interface SessionService {List<UserOnline> list();boolean forceLogout(String sessionId);}
其具体实现:
@Service("sessionService")public class SessionServiceImpl implements SessionService {@Autowiredprivate SessionDAO sessionDAO;@Overridepublic 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;}@Overridepublic 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()方法需要稍作修改:session.setTimeout(0);需要改成sessionDAO.delete(session)
@Overridepublic boolean forceLogout(String sessionId) {Session session = sessionDAO.readSession(sessionId);sessionDAO.delete(session);return true;}
Controller
定义一个SessionContoller,用于处理Session的相关操作:
@Controller@RequestMapping("/online")public class SessionController {@AutowiredSessionService 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页面,用于展示所有在线用户的信息:
<!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中加入该页面的入口:
...<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>...
测试
启动项目,在Opera浏览器中使用mrbird账户访问:

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

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

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

回到tester用户的主界面,点击“查看用户信息”,会发现页面已经被重定向到login页面,因为其Session已经失效!
再次刷新mrbird的online页面,显示如下:

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