记得上下班打卡 | git大法好,push需谨慎

Commit 4fb161ca authored by 姜秀龙's avatar 姜秀龙

feat(adam): 多端登录 JTI 会话,取消单设备互踢

JWT 写入 jti,Redis 维护 adam:session:{jti} 与按 uid 的 jti 集合;登录/刷新/登出与会话校验统一改造,业务服务经 ath/check 或拦截器校验会话是否存在。
Co-authored-by: 's avatarCursor <cursoragent@cursor.com>
parent 125b0e00
......@@ -12,12 +12,17 @@ import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import java.util.Date;
import java.util.Map;
import java.util.UUID;
@Data
@Component("jwtValidator")
@ConfigurationProperties(prefix = "jwt")
public class JwtValidator {
private String ssoRedisKey = "adam:identity:sso:";
/** 用户会话:adam:session:{jti} = uid */
private String sessionRedisKey = "adam:session:";
/** 用户下活跃 jti 集合:adam:session:user:{uid} = {jti,...},用于按人踢线 */
private String userSessionsRedisKey = "adam:session:user:";
private String msoRedisKey = "adam:identity:mso:";
private String secret;
// 分钟
......@@ -45,15 +50,32 @@ public class JwtValidator {
* @return token字符串
*/
public String create(Map<String, Object> claimsMap) {
return create(claimsMap, generateJti());
}
public String create(Map<String, Object> claimsMap, String jti) {
long nowMillis = System.currentTimeMillis();
long expMillis = System.currentTimeMillis() + expireTtl * 60000;
long expMillis = nowMillis + expireTtl * 60000;
JwtBuilder builder = Jwts.builder()
return Jwts.builder()
.setClaims(claimsMap)
.setId(jti)
.setIssuedAt(new Date(nowMillis))
.setExpiration(new Date(expMillis))
.signWith(SignatureAlgorithm.HS256, this.initSecretKey(this.secret));
return builder.compact();
.signWith(SignatureAlgorithm.HS256, this.initSecretKey(this.secret))
.compact();
}
public String generateJti() {
return UUID.randomUUID().toString().replace("-", "");
}
public String sessionKey(String jti) {
return sessionRedisKey.concat(jti);
}
public String userSessionsKey(String uid) {
return userSessionsRedisKey.concat(uid);
}
/**
......
......@@ -18,6 +18,7 @@ public class CurrentUtil {
public static final String TOKEN_NICKNAME = "nickname";
public static final String TOKEN_TYPE = "type";
public static final String TOKEN_UCREATED = "c_at";
public static final String TOKEN_JTI = "jti";
public static final String TOKEN_TYPE_VAL_USER = "user";
public static final String TOKEN_TYPE_VAL_STATION = "station";
......
......@@ -249,13 +249,14 @@ public class GlobalAuthorityInterceptor extends HandlerInterceptorAdapter {
Integer online = null == val ? null : Integer.valueOf(val);
return null != online && online == 1 || this.responseHandlerRefuse(response, TOKEN_INVALID);
case CurrentUtil.TOKEN_TYPE_VAL_USER:// adam:identity:sso:${uid}=MD5(${token})
String ssoKey = jwtValidator.getSsoRedisKey().concat(currentUid);
String md5Token = this.getAccessToken(ssoKey);
return StringUtils.isEmpty(md5Token) ? this.responseHandlerRefuse(response, TOKEN_INVALID)
: (md5Token.equals(DigestUtils.md5DigestAsHex(token.getBytes(StandardCharsets.UTF_8))) || this.responseHandlerRefuse(response, TOKEN_KICK));
case CurrentUtil.TOKEN_TYPE_VAL_USER:// adam:session:{jti}=uid
String jti = claims.getId();
if (StringUtils.isBlank(jti)) {
return this.responseHandlerRefuse(response, TOKEN_INVALID);
}
String sessionUid = this.getAccessToken(jwtValidator.sessionKey(jti));
return StringUtils.isNotBlank(sessionUid) && sessionUid.equals(currentUid)
|| this.responseHandlerRefuse(response, TOKEN_INVALID);
default:
log.warn("Authority failed:{} (Unknown token type).uri:[{}],token:{}", TOKEN_ILLEGAL, uri, token);
return this.responseHandlerRefuse(response, TOKEN_ILLEGAL);
......
......@@ -19,6 +19,8 @@ import com.liquidnet.service.adam.dto.vo.AdamUserInfoVo;
import com.liquidnet.service.adam.service.AdamRdmService;
import com.liquidnet.service.adam.service.AdamWechatService;
import com.liquidnet.service.adam.service.IAdamUserService;
import com.liquidnet.service.adam.support.AdamUserSessionSupport;
import io.jsonwebtoken.Claims;
import com.liquidnet.service.base.ErrorMapping;
import com.liquidnet.service.base.ResponseDto;
import com.liquidnet.service.base.UserPathDto;
......@@ -72,6 +74,8 @@ public class AdamLoginController {
SmsProcessor smsProcessor;
@Autowired
AdamWechatService adamWechatService;
@Autowired
AdamUserSessionSupport adamUserSessionSupport;
@Value("${liquidnet.reviewer.app-login.mobile}")
private String reviewMobile;
......@@ -373,7 +377,10 @@ public class AdamLoginController {
public void logout() {
log.info("###logout_uid:{}", CurrentUtil.getCurrentUid());
redisUtil.del(jwtValidator.getSsoRedisKey().concat(CurrentUtil.getCurrentUid()));
String jti = resolveJtiFromRequest();
if (StringUtils.isNotBlank(jti)) {
adamUserSessionSupport.unbindSession(jti, CurrentUtil.getCurrentUid());
}
}
@ApiOperationSupport(order = 8)
......@@ -488,16 +495,32 @@ public class AdamLoginController {
log.debug("Gentoken:{}", claimsMap);
String token = jwtValidator.create(claimsMap);
redisUtil.set(
jwtValidator.getSsoRedisKey().concat(userInfoVo.getUid()),
DigestUtils.md5DigestAsHex(token.getBytes(StandardCharsets.UTF_8)),
jwtValidator.getExpireTtl() * 60
);
String jti = jwtValidator.generateJti();
String token = jwtValidator.create(claimsMap, jti);
adamUserSessionSupport.bindSession(jti, userInfoVo.getUid());
return token;
}
private String resolveJtiFromRequest() {
Map<?, ?> claims = CurrentUtil.getTokenClaims();
if (claims != null) {
Object jti = claims.get(CurrentUtil.TOKEN_JTI);
if (jti != null && StringUtils.isNotBlank(jti.toString())) {
return jti.toString();
}
}
String token = CurrentUtil.getToken();
if (StringUtils.isNotBlank(token)) {
try {
Claims parsed = jwtValidator.parse(token);
return parsed.getId();
} catch (Exception e) {
log.warn("logout resolve jti failed", e);
}
}
return null;
}
private ResponseDto<AdamLoginInfoVo> loginVoResponseProcessing(AdamLoginInfoVo loginInfoVo) {
AdamUserInfoVo userInfo = loginInfoVo.getUserInfo();
adamRdmService.ratingProvince(userInfo);
......
......@@ -12,6 +12,7 @@ import com.liquidnet.service.adam.dto.vo.AdamTagVo;
import com.liquidnet.service.adam.dto.vo.AdamUserInfoVo;
import com.liquidnet.service.adam.service.AdamRdmService;
import com.liquidnet.service.adam.service.IAdamUserInfoService;
import com.liquidnet.service.adam.support.AdamUserSessionSupport;
import com.liquidnet.service.adam.util.QueueUtils;
import com.liquidnet.service.base.ErrorMapping;
import com.liquidnet.service.base.ResponseDto;
......@@ -26,9 +27,6 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service;
import org.springframework.util.DigestUtils;
import java.nio.charset.StandardCharsets;
import java.time.LocalDateTime;
import java.util.*;
import java.util.regex.Pattern;
......@@ -53,6 +51,8 @@ public class AdamUserInfoServiceImpl implements IAdamUserInfoService {
@Autowired
JwtValidator jwtValidator;
@Autowired
AdamUserSessionSupport adamUserSessionSupport;
@Autowired
private EasemobUtil easemobUtil;
@Autowired
......@@ -308,13 +308,9 @@ public class AdamUserInfoServiceImpl implements IAdamUserInfoService {
claimsMap.put(CurrentUtil.TOKEN_TYPE, CurrentUtil.TOKEN_TYPE_VAL_USER);
claimsMap.put(CurrentUtil.TOKEN_UCREATED, DateUtil.Formatter.yyyyMMddHHmmssTrim.format(userInfoVo.getCreateAt()));
String token = jwtValidator.create(claimsMap);
redisUtil.set(
jwtValidator.getSsoRedisKey().concat(userInfoVo.getUid()),
DigestUtils.md5DigestAsHex(token.getBytes(StandardCharsets.UTF_8)),
jwtValidator.getExpireTtl() * 60
);
String jti = jwtValidator.generateJti();
String token = jwtValidator.create(claimsMap, jti);
adamUserSessionSupport.bindSession(jti, userInfoVo.getUid());
return token;
}
}
package com.liquidnet.service.adam.support;
import com.liquidnet.common.cache.redis.util.RedisUtil;
import com.liquidnet.commons.lang.core.JwtValidator;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
/**
* 用户 JTI 会话:adam:session:{jti}=uid,adam:session:user:{uid} 记录活跃 jti。
*/
@Component
public class AdamUserSessionSupport {
@Autowired
private RedisUtil redisUtil;
@Autowired
private JwtValidator jwtValidator;
public void bindSession(String jti, String uid) {
long sessionTtlSec = jwtValidator.getExpireTtl() * 60;
redisUtil.set(jwtValidator.sessionKey(jti), uid, sessionTtlSec);
redisUtil.sSetAndTime(jwtValidator.userSessionsKey(uid), sessionTtlSec, jti);
}
public void unbindSession(String jti, String uid) {
if (StringUtils.isBlank(jti)) {
return;
}
redisUtil.del(jwtValidator.sessionKey(jti));
if (StringUtils.isNotBlank(uid)) {
redisUtil.setRemove(jwtValidator.userSessionsKey(uid), jti);
}
}
}
......@@ -14,8 +14,6 @@ import io.jsonwebtoken.ExpiredJwtException;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.util.DigestUtils;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.nio.charset.StandardCharsets;
......@@ -128,23 +126,22 @@ public class GlobalAuthFilter extends ZuulFilter {
ctx.setSendZuulResponse(true);
// }
} else {
// adam:identity:sso:${uid}=MD5(${token})
String ssoKey = jwtValidator.getSsoRedisKey().concat(uid);
String md5Token = (String) redisUtil.get(ssoKey);
if (StringUtils.isEmpty(md5Token)) {
// 已离线
this.respHandler(ctx, TOKEN_INVALID);
} else {
// 与在线TOKEN比对
if (md5Token.equals(DigestUtils.md5DigestAsHex(uToken.getBytes(StandardCharsets.UTF_8)))) {
// 一致则放行
ctx.setSendZuulResponse(true);
// adam:session:{jti}=uid
try {
Claims claims = jwtValidator.parse(uToken);
String jti = claims.getId();
if (StringUtils.isBlank(jti)) {
this.respHandler(ctx, TOKEN_INVALID);
} else {
// 不一致则被踢下线
this.respHandler(ctx, TOKEN_KICK);
String sessionUid = (String) redisUtil.get(jwtValidator.sessionKey(jti));
if (StringUtils.isNotBlank(sessionUid) && sessionUid.equals(uid)) {
ctx.setSendZuulResponse(true);
} else {
this.respHandler(ctx, TOKEN_INVALID);
}
}
} catch (Exception e) {
this.respHandler(ctx, TOKEN_INVALID);
}
}
return null;
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment