公衆號原創文章
開發背景:項目中採用spring session + spring security 方式做登陸註冊 ,現在要求後臺用戶只能同時一個人登陸
苦難的經歷:spring security 框架比較重,難以 快速深入理解, 另外 網上 spring security 如何限制 用戶同時登陸 的 文章 又是 一大堆,只想抱着試試看的態度快速成功,可 在項目中 實踐 配置 好多種情況 都絲毫不起作用 ,加斷點調試 源碼 ,發現 配置 過濾器(filter)生效了,但是 過濾器中的具體策略(strategy)卻根本 都沒有執行 ,一度陷入了迷茫 。
苦海脫生 : 下班了都,沒辦法 ,只能 利用晚上時間體系去學習spring security 了,但估計時間 短不了 ,怎麼辦 ?找經典 博客快速突擊,我一直認爲 公衆號的文章都是最新最優秀的。哈哈哈 ,還真讓我幸運得碰到了 ,不出幾篇文章 ,就找到了 關於 spring security 的經典文章 。https://toutiao.io/posts/idtbcp
看完框架解讀第一篇,核心 類第二篇 還沒看完 ,因爲項目代碼已經成竹在胸了,我就明白 我一下午 慌亂採坑所在了 。
原來,我們接入spring security的流程並不標準, 沒有用原生的方式UsernamePasswordAuthenticationFilter 實現 登錄驗證,而是 代碼中簡單比對後 直接 儲存session,所以網上的ConcurrentSessionControlAuthenticationStrategy策略配置根本無法生效 ,所以 一下午的研究白費了 。
海高憑魚躍 :
經過上面的一番苦戰 ,思路就已經有了 ,第一種方法 是 按照 標準 方法 重新 接入 spring security ,那網上的文章 配置肯定生效啊(費時)
第二種 方法就是 嘗試直接 操作 spring session ,跳過 spring security 。
關鍵 是我在 spring session的 公衆號 還真看到了根據用戶名查找用戶歸屬的SESSION 的段落(從零開始的Spring Session)
@Autowired
FindByIndexNameSessionRepository<? extends ExpiringSession> sessionRepository;
@RequestMapping("/test/findByUsername")
@ResponseBody
public Map findByUsername (@RequestParam String username)
{
Map<String, ? extends ExpiringSession> usersSessions = sessionRepository
.findByIndexNameAndIndexValue(FindByIndexNameSessionRepository
.PRINCIPAL_NAME_INDEX_NAME, username);
return usersSessions;
}
那還說什麼 ,第二天去實踐唄
真槍實戰 :
/**
* 保存user到session中
* @param user
* @return
*/
public static String savePrincipal(Auditor user) {
AuthUserDetails authUserDetails = new AuthUserDetails(user);
return savePrincipal(authUserDetails);
}
/**
* 保存principal
* @param principal
* @return
*/
public static String savePrincipal(AuthUserDetails principal) {
String SESSION_USER_KEY = HttpSessionSecurityContextRepository.SPRING_SECURITY_CONTEXT_KEY;
SecurityContextImpl context = new SecurityContextImpl();
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(principal, "dummy credentials", Collections.emptyList());
authentication.setDetails(new WebAuthenticationDetails(getRequest()));
context.setAuthentication(authentication);
getSession().setAttribute(SESSION_USER_KEY, context);
return getSession().getId();
}
認真 攻讀 了保存session的代碼 ,找到了最終往redis保存的源代碼 RedisOperationsSessionRepository,還真有 上面所說的
FindByIndexNameSessionRepository 和 期待的delete 方法 ,那隻要研究一下 非標準接入 情況下FindByIndexNameSessionRepository 第二個參數 username該傳 什麼值就可以了
通過閱讀save 方法 和 FindByIndexNameSessionRepository 對比,
String getPrincipalKey(String principalName) {
return this.keyPrefix + "index:" + FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME + ":" + principalName;
}
找到了第二個參數 生成方法,打斷點知道了FindByIndexNameSessionRepository 應該傳什麼值,就是生成 UsernamePasswordAuthenticationToken的AuthUserDetails,基本大功告成 。
在登錄時 我們只需要把 上一個用戶踢掉就可以了,就可以實現功能了
最終登錄時的部分代碼
@Override
public void forcedOffLine(Long auditorId){
final Auditor auditor = this.findById(auditorId);
AuthUserDetails authUserDetails = new AuthUserDetails(auditor);
final Map<String, ? extends ExpiringSession> sessions = redisOperationsSessionRepository.findByIndexNameAndIndexValue(FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME, authUserDetails.toString());
log.info("刪掉{}用戶session爲{}",auditorId,sessions);
if(!org.springframework.util.CollectionUtils.isEmpty(sessions)){
for ( String key : sessions.keySet()){
redisOperationsSessionRepository.delete(key);
}
}
}
總結:
本文簡述瞭如何實現登錄功能的思想歷程,懂得了還是應該先整體瞭解後再深入實踐的道理
後記:關於 redis 總結的第三篇文章沒發出來,是因爲 瞭解redis單線程模型 去 學習了網絡編程,後續兩篇文章同時發出
如果喜歡,歡迎關注我的公衆號:喬志勇筆記