整理自己的代碼片段的時候, 想起來之前有一個需求: 實現(xiàn)同一個用戶同時只能在一個地方登陸, 如果該用戶在其他地方登陸, 踢出前一個登陸狀態(tài)十绑。
項目中使用的是shiro做為權限控制框架, 對此需求進行了一些實現(xiàn), 思路如下: shiro是利用一個個filter進行過濾請求的權限, 那么我就可以自定義一個filter在用戶登陸之后, 判斷當前的用戶是否存在已經(jīng)登陸的情況, 如果已經(jīng)存在, 那么就踢出。
代碼分析
- 存儲用戶當前登陸的狀態(tài), 這里其實可以用redis, 但是因為項目比較小, 我直接使用了內(nèi)存Map來實現(xiàn);
/**
* 當前登錄用戶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
, 其中有兩個方法需要我們復寫實現(xiàn), 一個是isAccessAllowed, 這個方法返回值是Boolean, 主要是判斷邏輯是否能通過, 如果不通過, 就會調(diào)用第二個方法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();
// 判斷是否已經(jīng)踢出
Boolean kickout = SessionCacheHolder.sessionStatusMap.get(sessionId);
if (null != kickout && kickout) {
// 移除該session數(shù)據(jù)
SessionCacheHolder.sessionStatusMap.remove(sessionId);
return Boolean.FALSE;
}
// 獲取mobileId
User user = (User) subject.getPrincipal();
String mobileId = user.getMobile();
if (SessionCacheHolder.loginSessionCache.containsKey(mobileId)) {
// 如果已經(jīng)包含當前Session,并且是同一個用戶悠瞬,跳過嘁灯。
if (SessionCacheHolder.loginSessionCache.containsValue(sessionId)) {
return Boolean.TRUE;
}
/*
* 如果用戶Id相同, Session值不相同
* 1.獲取到原來的session对碌,并且標記為踢出帝璧。
* 2.繼續(xù)走
*/
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配置模式。 需要注意的是, 因為這里只是想要實現(xiàn)判斷當前用戶是否已經(jīng)在其他地方登陸, 所以我們需要把filter放到過濾鏈中執(zhí)行真正進行鑒權的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>
效果展示
踢出用戶
本文只是記錄自己實現(xiàn)Shiro用戶踢出的一種方式, 歡迎各位來指正和提出建議拌阴。