SpringMVC + Shiro实现用户踢出功能
2020-03-01 本文已影响0人
视频怪物
整理自己的代码片段的时候, 想起来之前有一个需求: 实现同一个用户同时只能在一个地方登陆, 如果该用户在其他地方登陆, 踢出前一个登陆状态。
项目中使用的是shiro做为权限控制框架, 对此需求进行了一些实现, 思路如下: shiro是利用一个个filter进行过滤请求的权限, 那么我就可以自定义一个filter在用户登陆之后, 判断当前的用户是否存在已经登陆的情况, 如果已经存在, 那么就踢出。
代码分析
- 存储用户当前登陆的状态, 这里其实可以用redis, 但是因为项目比较小, 我直接使用了内存Map来实现;
/**
* 当前登录用户session的信息
*
* @author videomonster
*/
public class SessionCacheHolder {
/**
* 用户account, SessionId
*/
public static Map<String, Serializable> loginSessionCache = Maps.newConcurrentMap();
/**
* session map
* true: 踢出
* false: 未踢出
*/
public static Map<Serializable, Boolean> sessionStatusMap = Maps.newConcurrentMap();
}
- 定义一个
KickoutSessionFilter
, 这个filter继承Shiro提供的AccessControlFilter
, 其中有两个方法需要我们复写实现, 一个是isAccessAllowed, 这个方法返回值是Boolean, 主要是判断逻辑是否能通过, 如果不通过, 就会调用第二个方法onAccessDenied来处理, 并且AccessControlFilter
提供了getSubject方法, 可以通过当前的request和response获取当前主体(当前主体也可以通过Shiro提供的工具类SecurityUtils.getSubject()来获取);
/**
* 踢出重复登录用户
*
* @author videomonster
*/
@Slf4j
@Component
public class KickoutSessionFilter extends AccessControlFilter {
@Override
protected boolean isAccessAllowed(ServletRequest request,
ServletResponse response, Object mappedValue) {
Subject subject = getSubject(request, response);
//如果是相关目录或者是如果没有登录, 就直接return true
if ((!subject.isAuthenticated() && !subject.isRemembered())) {
return Boolean.TRUE;
}
// 获取当前登录的用户的相关信息
Session session = subject.getSession();
Serializable sessionId = session.getId();
// 判断是否已经踢出
Boolean kickout = SessionCacheHolder.sessionStatusMap.get(sessionId);
if (null != kickout && kickout) {
// 移除该session数据
SessionCacheHolder.sessionStatusMap.remove(sessionId);
return Boolean.FALSE;
}
// 获取mobileId
User user = (User) subject.getPrincipal();
String mobileId = user.getMobile();
if (SessionCacheHolder.loginSessionCache.containsKey(mobileId)) {
// 如果已经包含当前Session,并且是同一个用户,跳过。
if (SessionCacheHolder.loginSessionCache.containsValue(sessionId)) {
return Boolean.TRUE;
}
/*
* 如果用户Id相同, Session值不相同
* 1.获取到原来的session,并且标记为踢出。
* 2.继续走
*/
Serializable oldSessionId = SessionCacheHolder.loginSessionCache.get(mobileId);
SessionCacheHolder.sessionStatusMap.put(oldSessionId, Boolean.TRUE);
log.info("用户手机号: {}, 姓名: {} 登陆, 当前sessionId: {}, 踢出 session id: {}",
mobileId, user.getRealname(), sessionId, oldSessionId);
}
SessionCacheHolder.loginSessionCache.put(mobileId, sessionId);
// 当前session标记为未被踢出
SessionCacheHolder.sessionStatusMap.put(sessionId, Boolean.FALSE);
return Boolean.TRUE;
}
@Override
protected boolean onAccessDenied(ServletRequest request,
ServletResponse response) throws Exception {
Subject subject = getSubject(request, response);
// 先退出该用户
subject.logout();
if (isAjaxRequest((HttpServletRequest) request)) {
// 如果是ajax请求
HttpServletResponse httpServletResponse = (HttpServletResponse) response;
httpServletResponse.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE);
Map<String, Object> rtn = Maps.newHashMap();
rtn.put("code", 510);
rtn.put("msg", "当前用户已在其他地方登陆, 请重新登录!");
httpServletResponse.getWriter().write(JsonUtils.toJson(rtn));
} else {
// 重定向到指定位置
WebUtils.issueRedirect(request, response, "/login.html");
}
return false;
}
private boolean isAjaxRequest(HttpServletRequest request) {
String header = request.getHeader("X-Requested-With");
return "XMLHttpRequest".equals(header);
}
}
- 在shiro的配置文件中加入
KickoutSessionFilter
, 这里采用的是SpringMVC的XML配置模式。 需要注意的是, 因为这里只是想要实现判断当前用户是否已经在其他地方登陆, 所以我们需要把filter放到过滤链中执行真正进行鉴权的filter之前;
<bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
<!-- Shiro的核心安全接口,这个属性是必须的 -->
<property name="securityManager" ref="securityManager"/>
<property name="loginUrl" value="/login.html"/>
<!-- 登录成功后要跳转的连接 -->
<property name="successUrl" value="/index.html"/>
<property name="unauthorizedUrl" value="/404.html"/>
<property name="filters">
<util:map>
<!-- 注册kickoutSessionFilter -->
<entry key="kickout" value-ref="kickoutSessionFilter"/>
</util:map>
</property>
<property name="filterChainDefinitions">
<value>
/resources/**=anon
/favicon.ico=anon
/login.html=anon
/logout.html=anon
/captcha.jpg=anon
/login=anon
/=anon
<!-- 注册kickoutSessionFilter 到过滤链中, 并且放在真正鉴权处理的filter之前 -->
/**=kickout,authc
</value>
</property>
</bean>
效果展示

本文只是记录自己实现Shiro用户踢出的一种方式, 欢迎各位来指正和提出建议。