记得上下班打卡 | 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; ...@@ -12,12 +12,17 @@ import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec; import javax.crypto.spec.SecretKeySpec;
import java.util.Date; import java.util.Date;
import java.util.Map; import java.util.Map;
import java.util.UUID;
@Data @Data
@Component("jwtValidator") @Component("jwtValidator")
@ConfigurationProperties(prefix = "jwt") @ConfigurationProperties(prefix = "jwt")
public class JwtValidator { public class JwtValidator {
private String ssoRedisKey = "adam:identity:sso:"; 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 msoRedisKey = "adam:identity:mso:";
private String secret; private String secret;
// 分钟 // 分钟
...@@ -45,15 +50,32 @@ public class JwtValidator { ...@@ -45,15 +50,32 @@ public class JwtValidator {
* @return token字符串 * @return token字符串
*/ */
public String create(Map<String, Object> claimsMap) { 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 nowMillis = System.currentTimeMillis();
long expMillis = System.currentTimeMillis() + expireTtl * 60000; long expMillis = nowMillis + expireTtl * 60000;
JwtBuilder builder = Jwts.builder() return Jwts.builder()
.setClaims(claimsMap) .setClaims(claimsMap)
.setId(jti)
.setIssuedAt(new Date(nowMillis)) .setIssuedAt(new Date(nowMillis))
.setExpiration(new Date(expMillis)) .setExpiration(new Date(expMillis))
.signWith(SignatureAlgorithm.HS256, this.initSecretKey(this.secret)); .signWith(SignatureAlgorithm.HS256, this.initSecretKey(this.secret))
return builder.compact(); .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 { ...@@ -18,6 +18,7 @@ public class CurrentUtil {
public static final String TOKEN_NICKNAME = "nickname"; public static final String TOKEN_NICKNAME = "nickname";
public static final String TOKEN_TYPE = "type"; public static final String TOKEN_TYPE = "type";
public static final String TOKEN_UCREATED = "c_at"; 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_USER = "user";
public static final String TOKEN_TYPE_VAL_STATION = "station"; public static final String TOKEN_TYPE_VAL_STATION = "station";
......
...@@ -249,13 +249,14 @@ public class GlobalAuthorityInterceptor extends HandlerInterceptorAdapter { ...@@ -249,13 +249,14 @@ public class GlobalAuthorityInterceptor extends HandlerInterceptorAdapter {
Integer online = null == val ? null : Integer.valueOf(val); Integer online = null == val ? null : Integer.valueOf(val);
return null != online && online == 1 || this.responseHandlerRefuse(response, TOKEN_INVALID); return null != online && online == 1 || this.responseHandlerRefuse(response, TOKEN_INVALID);
case CurrentUtil.TOKEN_TYPE_VAL_USER:// adam:identity:sso:${uid}=MD5(${token}) case CurrentUtil.TOKEN_TYPE_VAL_USER:// adam:session:{jti}=uid
String ssoKey = jwtValidator.getSsoRedisKey().concat(currentUid); String jti = claims.getId();
if (StringUtils.isBlank(jti)) {
String md5Token = this.getAccessToken(ssoKey); return this.responseHandlerRefuse(response, TOKEN_INVALID);
}
return StringUtils.isEmpty(md5Token) ? this.responseHandlerRefuse(response, TOKEN_INVALID) String sessionUid = this.getAccessToken(jwtValidator.sessionKey(jti));
: (md5Token.equals(DigestUtils.md5DigestAsHex(token.getBytes(StandardCharsets.UTF_8))) || this.responseHandlerRefuse(response, TOKEN_KICK)); return StringUtils.isNotBlank(sessionUid) && sessionUid.equals(currentUid)
|| this.responseHandlerRefuse(response, TOKEN_INVALID);
default: default:
log.warn("Authority failed:{} (Unknown token type).uri:[{}],token:{}", TOKEN_ILLEGAL, uri, token); log.warn("Authority failed:{} (Unknown token type).uri:[{}],token:{}", TOKEN_ILLEGAL, uri, token);
return this.responseHandlerRefuse(response, TOKEN_ILLEGAL); return this.responseHandlerRefuse(response, TOKEN_ILLEGAL);
......
...@@ -19,6 +19,8 @@ import com.liquidnet.service.adam.dto.vo.AdamUserInfoVo; ...@@ -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.AdamRdmService;
import com.liquidnet.service.adam.service.AdamWechatService; import com.liquidnet.service.adam.service.AdamWechatService;
import com.liquidnet.service.adam.service.IAdamUserService; 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.ErrorMapping;
import com.liquidnet.service.base.ResponseDto; import com.liquidnet.service.base.ResponseDto;
import com.liquidnet.service.base.UserPathDto; import com.liquidnet.service.base.UserPathDto;
...@@ -72,6 +74,8 @@ public class AdamLoginController { ...@@ -72,6 +74,8 @@ public class AdamLoginController {
SmsProcessor smsProcessor; SmsProcessor smsProcessor;
@Autowired @Autowired
AdamWechatService adamWechatService; AdamWechatService adamWechatService;
@Autowired
AdamUserSessionSupport adamUserSessionSupport;
@Value("${liquidnet.reviewer.app-login.mobile}") @Value("${liquidnet.reviewer.app-login.mobile}")
private String reviewMobile; private String reviewMobile;
...@@ -373,7 +377,10 @@ public class AdamLoginController { ...@@ -373,7 +377,10 @@ public class AdamLoginController {
public void logout() { public void logout() {
log.info("###logout_uid:{}", CurrentUtil.getCurrentUid()); 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) @ApiOperationSupport(order = 8)
...@@ -488,16 +495,32 @@ public class AdamLoginController { ...@@ -488,16 +495,32 @@ public class AdamLoginController {
log.debug("Gentoken:{}", claimsMap); log.debug("Gentoken:{}", claimsMap);
String token = jwtValidator.create(claimsMap); String jti = jwtValidator.generateJti();
String token = jwtValidator.create(claimsMap, jti);
redisUtil.set( adamUserSessionSupport.bindSession(jti, userInfoVo.getUid());
jwtValidator.getSsoRedisKey().concat(userInfoVo.getUid()),
DigestUtils.md5DigestAsHex(token.getBytes(StandardCharsets.UTF_8)),
jwtValidator.getExpireTtl() * 60
);
return token; 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) { private ResponseDto<AdamLoginInfoVo> loginVoResponseProcessing(AdamLoginInfoVo loginInfoVo) {
AdamUserInfoVo userInfo = loginInfoVo.getUserInfo(); AdamUserInfoVo userInfo = loginInfoVo.getUserInfo();
adamRdmService.ratingProvince(userInfo); adamRdmService.ratingProvince(userInfo);
......
...@@ -12,6 +12,7 @@ import com.liquidnet.service.adam.dto.vo.AdamTagVo; ...@@ -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.dto.vo.AdamUserInfoVo;
import com.liquidnet.service.adam.service.AdamRdmService; import com.liquidnet.service.adam.service.AdamRdmService;
import com.liquidnet.service.adam.service.IAdamUserInfoService; 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.adam.util.QueueUtils;
import com.liquidnet.service.base.ErrorMapping; import com.liquidnet.service.base.ErrorMapping;
import com.liquidnet.service.base.ResponseDto; import com.liquidnet.service.base.ResponseDto;
...@@ -26,9 +27,6 @@ import org.springframework.beans.factory.annotation.Autowired; ...@@ -26,9 +27,6 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Lazy; import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.util.DigestUtils;
import java.nio.charset.StandardCharsets;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.util.*; import java.util.*;
import java.util.regex.Pattern; import java.util.regex.Pattern;
...@@ -53,6 +51,8 @@ public class AdamUserInfoServiceImpl implements IAdamUserInfoService { ...@@ -53,6 +51,8 @@ public class AdamUserInfoServiceImpl implements IAdamUserInfoService {
@Autowired @Autowired
JwtValidator jwtValidator; JwtValidator jwtValidator;
@Autowired @Autowired
AdamUserSessionSupport adamUserSessionSupport;
@Autowired
private EasemobUtil easemobUtil; private EasemobUtil easemobUtil;
@Autowired @Autowired
...@@ -308,13 +308,9 @@ public class AdamUserInfoServiceImpl implements IAdamUserInfoService { ...@@ -308,13 +308,9 @@ public class AdamUserInfoServiceImpl implements IAdamUserInfoService {
claimsMap.put(CurrentUtil.TOKEN_TYPE, CurrentUtil.TOKEN_TYPE_VAL_USER); claimsMap.put(CurrentUtil.TOKEN_TYPE, CurrentUtil.TOKEN_TYPE_VAL_USER);
claimsMap.put(CurrentUtil.TOKEN_UCREATED, DateUtil.Formatter.yyyyMMddHHmmssTrim.format(userInfoVo.getCreateAt())); claimsMap.put(CurrentUtil.TOKEN_UCREATED, DateUtil.Formatter.yyyyMMddHHmmssTrim.format(userInfoVo.getCreateAt()));
String token = jwtValidator.create(claimsMap); String jti = jwtValidator.generateJti();
String token = jwtValidator.create(claimsMap, jti);
redisUtil.set( adamUserSessionSupport.bindSession(jti, userInfoVo.getUid());
jwtValidator.getSsoRedisKey().concat(userInfoVo.getUid()),
DigestUtils.md5DigestAsHex(token.getBytes(StandardCharsets.UTF_8)),
jwtValidator.getExpireTtl() * 60
);
return token; 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; ...@@ -14,8 +14,6 @@ import io.jsonwebtoken.ExpiredJwtException;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.util.DigestUtils;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpServletResponse;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
...@@ -128,23 +126,22 @@ public class GlobalAuthFilter extends ZuulFilter { ...@@ -128,23 +126,22 @@ public class GlobalAuthFilter extends ZuulFilter {
ctx.setSendZuulResponse(true); ctx.setSendZuulResponse(true);
// } // }
} else { } else {
// adam:identity:sso:${uid}=MD5(${token}) // adam:session:{jti}=uid
String ssoKey = jwtValidator.getSsoRedisKey().concat(uid); try {
Claims claims = jwtValidator.parse(uToken);
String md5Token = (String) redisUtil.get(ssoKey); String jti = claims.getId();
if (StringUtils.isBlank(jti)) {
if (StringUtils.isEmpty(md5Token)) {
// 已离线
this.respHandler(ctx, TOKEN_INVALID); this.respHandler(ctx, TOKEN_INVALID);
} else { } else {
// 与在线TOKEN比对 String sessionUid = (String) redisUtil.get(jwtValidator.sessionKey(jti));
if (md5Token.equals(DigestUtils.md5DigestAsHex(uToken.getBytes(StandardCharsets.UTF_8)))) { if (StringUtils.isNotBlank(sessionUid) && sessionUid.equals(uid)) {
// 一致则放行
ctx.setSendZuulResponse(true); ctx.setSendZuulResponse(true);
} else { } else {
// 不一致则被踢下线 this.respHandler(ctx, TOKEN_INVALID);
this.respHandler(ctx, TOKEN_KICK); }
} }
} catch (Exception e) {
this.respHandler(ctx, TOKEN_INVALID);
} }
} }
return null; 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