java實(shí)現(xiàn)多設(shè)備同時(shí)登錄或強(qiáng)制下線
前言:你有沒(méi)有遇到過(guò)這樣的需求,產(chǎn)品要求實(shí)現(xiàn)同一個(gè)用戶根據(jù)后臺(tái)設(shè)置允許同時(shí)登錄,或者不準(zhǔn)同時(shí)登錄時(shí),需要強(qiáng)制踢下線前一個(gè)的場(chǎng)景。本文將帶領(lǐng)大家實(shí)現(xiàn)一個(gè)簡(jiǎn)單的這種場(chǎng)景需求。
先來(lái)看一下簡(jiǎn)單的時(shí)序圖,方便后續(xù)理解。

首先我們需要有一個(gè)后臺(tái)設(shè)置開(kāi)關(guān)來(lái)控制允不允許用戶多設(shè)備同時(shí)登錄的功能(沒(méi)有也無(wú)妨,假定允許),其次在登錄后,需要保存用戶的userId-token的關(guān)系緩存。再回頭看上面的時(shí)序圖,是不是已經(jīng)能理解實(shí)現(xiàn)的原理了。
如果你的架構(gòu)是微服務(wù),那么可以使用redis來(lái)存登錄關(guān)系緩存,單體架構(gòu)可以直接存session即可。本文是微服務(wù)架構(gòu),所以采用的是redis。
本文的前提都是基于同一個(gè)用戶的情況下,下文不再贅述。
1 構(gòu)造登錄緩存關(guān)系
如果要實(shí)現(xiàn)同一用戶多設(shè)備同時(shí)登錄,那必然需要在session(微服務(wù)中可以用redis做session共享)中能找到用戶的每一個(gè)登錄狀態(tài),如果只是簡(jiǎn)單的緩存用戶信息是實(shí)現(xiàn)不了的,登錄時(shí)那就必須要有一個(gè)唯一值token,這樣每次登錄token不一樣,但是指向的用戶是同一個(gè)。

usertoken中維護(hù)的是前綴:用戶id,這里不需要維護(hù)多個(gè),因?yàn)橛玫膔eids的hash數(shù)據(jù)類(lèi)型,多個(gè)登錄時(shí),添加新行即可;user部分,這里維護(hù)的是多個(gè),即登錄一次就有一條記錄;因?yàn)楦鶕?jù)業(yè)務(wù)需要,后續(xù)需要從緩存中獲取用戶其他信息。
- 允許多設(shè)備同時(shí)登錄:
usertoken只有1條,user可能會(huì)有多條 - 不允許多設(shè)備同時(shí)登錄(有則強(qiáng)制下線):
usertoken只有1條,user只有1條
/**
* 登錄成功后緩存用戶信息
*
* @param req
* @return
*/
public void cacheUserInfo(CacheUserInfoReqDTO req) {
// 1、緩存用戶信息
cacheUser(req);
cacheAuth(req.getUid(), req.getRoles(), req.getPermissions());
// 2、更新token與userId關(guān)系
String userTokenRelationKey = RedisKeyHelper.getUserTokenRelationKey(req.getEntId() + SymbolConstant.COLON + req.getUid());
redisAdapter.set(userTokenRelationKey, req.getToken(), RedisTtl.USER_LOGIN_SUCCESS);
}2 過(guò)濾器配置
登錄鑒權(quán)部分和用戶登錄狀態(tài)上下文不在本文范圍內(nèi),此處忽略
登錄成功后,每一個(gè)請(qǐng)求到達(dá)過(guò)濾器時(shí),通過(guò)請(qǐng)求header中的token來(lái)獲取登錄信息;因?yàn)槲覀兇娴木彺鎘ey前綴都包含userId,所以要想得到用戶信息,需要使用到redis的scan命令來(lái)獲取。(scan最好配置count來(lái)限制,保證性能)
@Override
protected Mono<Void> innerFilter(ServerWebExchange exchange, WebFilterChain chain) {
String token = filterContext.getToken();
if (StringUtils.isBlank(token)) {
throw new DataValidateException(GatewayReturnCodes.TOKEN_MISSING);
}
// scan獲取user的key
String userKey = "";
Set<String> scan = redisAdapter.scan(GatewayRedisKeyPrefix.USER_KEY.getKey() + "*" + token);
if (scan.isEmpty()) {
throw new DataValidateException(GatewayReturnCodes.TOKEN_EXPIRED_LOGIN_SUCCESS);
}
userKey = scan.iterator().next();
MyUser myUser = (MyUser) redisAdapter.get(userKey);
if (myUser == null) {
throw new BusinessException(GatewayReturnCodes.TOKEN_EXPIRED_LOGIN_SUCCESS);
}
// 將用戶信息塞入http header
// do something...
return chain.filter(exchange.mutate().request(newServerHttpRequest).build());
}這樣保證即使有多設(shè)備同時(shí)登錄,也能獲取到登錄信息和上下文。
3 如何做強(qiáng)制下線呢?
其實(shí)也很簡(jiǎn)單,在登錄前可以通過(guò)AOP方式做校驗(yàn),如果已登錄了,那么這里就清除session或用戶緩存,再繼續(xù)進(jìn)行正常登錄即可。再簡(jiǎn)單一點(diǎn)可以直接在登錄service中添加校驗(yàn)
核心邏輯
String userTokenRelationKey = RedisKeyHelper.getUserTokenRelationKey(req.getEntId() + SymbolConstant.COLON + userEntList.get(0).getUserId());
String redisToken = (String) redisAdapter.get(userTokenRelationKey);
if (StringUtils.isNotEmpty(redisToken) && !redisToken.equals(token)) {
throw new BusinessException(UserReturnCodes.MULTI_DEVICE_LOGIN);
}這里用于判斷是否已有登錄,并返回給前端提示。用于前端其他業(yè)務(wù)處理如果不需要給前端提示,不用返回前端,直接進(jìn)行清除session或用戶緩存邏輯。
String userTokenRelationKey = RedisKeyHelper.getUserTokenRelationKey(req.getEntId() + SymbolConstant.COLON + userEntity.getId());
// 獲取當(dāng)前已用戶的登錄token
String redisToken = (String) redisAdapter.get(userTokenRelationKey);
// 踢下線之前全部登錄
Response<Void> exitLoginResponse = gatewayRpc.allExit(ExitLoginReqDTO.builder().token(redisToken).userId(userEntity.getId()).build());4 演示
演示強(qiáng)制下線
這里我用用戶id為4做演示
先正常第一次登錄,提示成功,并且redis中有1條user記錄


再次登錄,我這里是返回給前端處理了,所以會(huì)有提示信息。

前端效果

最后,擴(kuò)展一下,如果要實(shí)現(xiàn)登錄后強(qiáng)制修改默認(rèn)密碼、登錄時(shí)間段限制等場(chǎng)景,你會(huì)怎么實(shí)現(xiàn)呢?
到此這篇關(guān)于java實(shí)現(xiàn)多設(shè)備同時(shí)登錄或強(qiáng)制下線的文章就介紹到這了,更多相關(guān)java 多設(shè)備同時(shí)登錄或強(qiáng)制下線內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
在Mac OS上安裝Java以及配置環(huán)境變量的基本方法
這篇文章主要介紹了在Mac OS上安裝Java以及配置環(huán)境變量的基本方法,包括查看所安裝Java版本的方法,需要的朋友可以參考下2015-10-10
Java應(yīng)用層協(xié)議WebSocket實(shí)現(xiàn)消息推送
后端向前端推送消息就需要長(zhǎng)連接,首先想到的就是websocket,下面這篇文章主要給大家介紹了關(guān)于java后端+前端使用WebSocket實(shí)現(xiàn)消息推送的詳細(xì)流程,需要的朋友可以參考下2023-02-02
springBoot Junit測(cè)試用例出現(xiàn)@Autowired不生效的解決
這篇文章主要介紹了springBoot Junit測(cè)試用例出現(xiàn)@Autowired不生效的解決,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-09-09
Java Http請(qǐng)求傳json數(shù)據(jù)亂碼問(wèn)題的解決
這篇文章主要介紹了Java Http請(qǐng)求傳json數(shù)據(jù)亂碼問(wèn)題的解決,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2019-09-09
Spring標(biāo)準(zhǔn)的xml文件頭實(shí)例分析
這篇文章主要介紹了Spring標(biāo)準(zhǔn)的xml文件頭實(shí)例分析,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2019-11-11

