|
@@ -17,21 +17,16 @@ import org.springframework.beans.factory.annotation.Autowired;
|
|
|
import org.springframework.boot.context.properties.ConfigurationProperties;
|
|
import org.springframework.boot.context.properties.ConfigurationProperties;
|
|
|
import org.springframework.http.HttpMethod;
|
|
import org.springframework.http.HttpMethod;
|
|
|
import org.springframework.stereotype.Component;
|
|
import org.springframework.stereotype.Component;
|
|
|
-import org.springframework.util.StreamUtils;
|
|
|
|
|
import org.springframework.web.method.HandlerMethod;
|
|
import org.springframework.web.method.HandlerMethod;
|
|
|
import org.springframework.web.servlet.HandlerInterceptor;
|
|
import org.springframework.web.servlet.HandlerInterceptor;
|
|
|
|
|
|
|
|
-import javax.crypto.Mac;
|
|
|
|
|
-import javax.crypto.spec.SecretKeySpec;
|
|
|
|
|
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.security.MessageDigest;
|
|
|
|
|
-import java.util.Base64;
|
|
|
|
|
import java.util.List;
|
|
import java.util.List;
|
|
|
import java.util.Objects;
|
|
import java.util.Objects;
|
|
|
import java.util.Set;
|
|
import java.util.Set;
|
|
|
import java.util.concurrent.LinkedBlockingQueue;
|
|
import java.util.concurrent.LinkedBlockingQueue;
|
|
|
|
|
+import java.util.concurrent.RejectedExecutionException;
|
|
|
import java.util.concurrent.ThreadPoolExecutor;
|
|
import java.util.concurrent.ThreadPoolExecutor;
|
|
|
import java.util.concurrent.TimeUnit;
|
|
import java.util.concurrent.TimeUnit;
|
|
|
|
|
|
|
@@ -46,6 +41,9 @@ public class JwtInterceptor implements HandlerInterceptor {
|
|
|
|
|
|
|
|
private static final String SLASH = "/";
|
|
private static final String SLASH = "/";
|
|
|
public static String TOKEN_PREFIX = "login.{token}";
|
|
public static String TOKEN_PREFIX = "login.{token}";
|
|
|
|
|
+ private static final String TOKEN_TEL_PREFIX = "login.token.tel.";
|
|
|
|
|
+ private static final String TOKEN_LIST_PREFIX = "login.account.token.list.";
|
|
|
|
|
+ private static final long TOKEN_INDEX_EXPIRE_TIME = RedisUtils.DEFAULT_EXPIRE_TIME + 24 * 60 * 60L;
|
|
|
private Set<String> excludePaths;
|
|
private Set<String> excludePaths;
|
|
|
private Set<String> excludeUris;
|
|
private Set<String> excludeUris;
|
|
|
|
|
|
|
@@ -100,7 +98,7 @@ public class JwtInterceptor implements HandlerInterceptor {
|
|
|
|
|
|
|
|
validToken(authHeader);
|
|
validToken(authHeader);
|
|
|
|
|
|
|
|
- // 请求签名校验(防重放),标记了 @RequireSign 的接口需要验签
|
|
|
|
|
|
|
+ // 标记了 @RequireSign 的接口需要进行防重放校验
|
|
|
if (handler instanceof HandlerMethod) {
|
|
if (handler instanceof HandlerMethod) {
|
|
|
HandlerMethod handlerMethod = (HandlerMethod) handler;
|
|
HandlerMethod handlerMethod = (HandlerMethod) handler;
|
|
|
RequireSign requireSign = handlerMethod.getMethodAnnotation(RequireSign.class);
|
|
RequireSign requireSign = handlerMethod.getMethodAnnotation(RequireSign.class);
|
|
@@ -125,8 +123,10 @@ public class JwtInterceptor implements HandlerInterceptor {
|
|
|
* 校验Token
|
|
* 校验Token
|
|
|
*/
|
|
*/
|
|
|
private void validToken(String authHeader) {
|
|
private void validToken(String authHeader) {
|
|
|
- String loginUserString = redisUtils.getString(TOKEN_PREFIX.replace("{token}", authHeader));
|
|
|
|
|
|
|
+ String key = TOKEN_PREFIX.replace("{token}", authHeader);
|
|
|
|
|
+ String loginUserString = redisUtils.getString(key);
|
|
|
if (StringUtils.isBlank(loginUserString)) {
|
|
if (StringUtils.isBlank(loginUserString)) {
|
|
|
|
|
+ cleanExpiredTokenAsync(authHeader);
|
|
|
throw new CommonException(ExceptionEnum.Not_LOGIN);
|
|
throw new CommonException(ExceptionEnum.Not_LOGIN);
|
|
|
}
|
|
}
|
|
|
ContentPlatformAccount account;
|
|
ContentPlatformAccount account;
|
|
@@ -135,6 +135,9 @@ public class JwtInterceptor implements HandlerInterceptor {
|
|
|
} catch (Exception e) {
|
|
} catch (Exception e) {
|
|
|
throw new CommonException(ExceptionEnum.Not_LOGIN);
|
|
throw new CommonException(ExceptionEnum.Not_LOGIN);
|
|
|
}
|
|
}
|
|
|
|
|
+ if (Objects.isNull(account)) {
|
|
|
|
|
+ throw new CommonException(ExceptionEnum.Not_LOGIN);
|
|
|
|
|
+ }
|
|
|
// token验证完成后获取对应信息,存放在本地线程中
|
|
// token验证完成后获取对应信息,存放在本地线程中
|
|
|
LoginUserContext.setLoginUser(account);
|
|
LoginUserContext.setLoginUser(account);
|
|
|
// 异步处理续期
|
|
// 异步处理续期
|
|
@@ -182,9 +185,7 @@ public class JwtInterceptor implements HandlerInterceptor {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
/**
|
|
|
- * 请求签名校验:HMAC-SHA256(method:uri:timestamp:nonce:body, signKey)
|
|
|
|
|
- * - timestamp 必须在 ±5 分钟内
|
|
|
|
|
- * - nonce 全局唯一(Redis setIfAbsent,10 分钟过期)
|
|
|
|
|
|
|
+ * 请求防重放校验:timestamp 必须在 ±5 分钟内,同一 token 的 nonce 只能使用一次。
|
|
|
*/
|
|
*/
|
|
|
private void validateSign(HttpServletRequest request, String token) {
|
|
private void validateSign(HttpServletRequest request, String token) {
|
|
|
String timestamp = request.getHeader("X-Timestamp");
|
|
String timestamp = request.getHeader("X-Timestamp");
|
|
@@ -202,7 +203,8 @@ public class JwtInterceptor implements HandlerInterceptor {
|
|
|
throw new CommonException(ExceptionEnum.PARAM_ERROR.getCode(), "时间戳格式错误");
|
|
throw new CommonException(ExceptionEnum.PARAM_ERROR.getCode(), "时间戳格式错误");
|
|
|
}
|
|
}
|
|
|
long now = System.currentTimeMillis();
|
|
long now = System.currentTimeMillis();
|
|
|
- if (Math.abs(now - requestTime) > 5 * 60 * 1000) {
|
|
|
|
|
|
|
+ long allowedSkew = 5 * 60 * 1000L;
|
|
|
|
|
+ if (requestTime < now - allowedSkew || requestTime > now + allowedSkew) {
|
|
|
throw new CommonException(ExceptionEnum.PARAM_ERROR.getCode(), "请求已过期");
|
|
throw new CommonException(ExceptionEnum.PARAM_ERROR.getCode(), "请求已过期");
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -216,30 +218,8 @@ public class JwtInterceptor implements HandlerInterceptor {
|
|
|
|
|
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- private String getRequestBody(HttpServletRequest request) {
|
|
|
|
|
- try {
|
|
|
|
|
- byte[] body = StreamUtils.copyToByteArray(request.getInputStream());
|
|
|
|
|
- return new String(body, StandardCharsets.UTF_8);
|
|
|
|
|
- } catch (Exception e) {
|
|
|
|
|
- log.warn("Failed to read request body for sign validation", e);
|
|
|
|
|
- return "";
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- private String hmacSha256(String data, String key) {
|
|
|
|
|
- try {
|
|
|
|
|
- Mac mac = Mac.getInstance("HmacSHA256");
|
|
|
|
|
- SecretKeySpec keySpec = new SecretKeySpec(key.getBytes(StandardCharsets.UTF_8), "HmacSHA256");
|
|
|
|
|
- mac.init(keySpec);
|
|
|
|
|
- byte[] hash = mac.doFinal(data.getBytes(StandardCharsets.UTF_8));
|
|
|
|
|
- return Base64.getEncoder().encodeToString(hash);
|
|
|
|
|
- } catch (Exception e) {
|
|
|
|
|
- throw new RuntimeException("HMAC-SHA256 computation failed", e);
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
private void renewalToken(String token) {
|
|
private void renewalToken(String token) {
|
|
|
- executor.execute(() -> {
|
|
|
|
|
|
|
+ executeAsync(() -> {
|
|
|
try {
|
|
try {
|
|
|
String key = TOKEN_PREFIX.replace("{token}", token);
|
|
String key = TOKEN_PREFIX.replace("{token}", token);
|
|
|
String loginUserString = redisUtils.getString(key);
|
|
String loginUserString = redisUtils.getString(key);
|
|
@@ -255,7 +235,12 @@ public class JwtInterceptor implements HandlerInterceptor {
|
|
|
if (Objects.nonNull(account) && Objects.nonNull(account.getId())) {
|
|
if (Objects.nonNull(account) && Objects.nonNull(account.getId())) {
|
|
|
ContentPlatformAccount latestAccount = accountService.getAccountById(account.getId());
|
|
ContentPlatformAccount latestAccount = accountService.getAccountById(account.getId());
|
|
|
if (Objects.nonNull(latestAccount)) {
|
|
if (Objects.nonNull(latestAccount)) {
|
|
|
- redisUtils.setValueWithExpire(key, JSON.toJSONString(latestAccount), RedisUtils.DEFAULT_EXPIRE_TIME);
|
|
|
|
|
|
|
+ boolean renewed = redisUtils.setValueWithExpireIfPresent(key,
|
|
|
|
|
+ JSON.toJSONString(latestAccount), RedisUtils.DEFAULT_EXPIRE_TIME);
|
|
|
|
|
+ if (renewed && StringUtils.isNotBlank(latestAccount.getTelNum())) {
|
|
|
|
|
+ redisUtils.setValueWithExpire(TOKEN_TEL_PREFIX + token,
|
|
|
|
|
+ latestAccount.getTelNum(), TOKEN_INDEX_EXPIRE_TIME);
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
@@ -263,7 +248,20 @@ public class JwtInterceptor implements HandlerInterceptor {
|
|
|
// 续期失败不影响正常请求,仅记录日志
|
|
// 续期失败不影响正常请求,仅记录日志
|
|
|
log.error("renewalToken error, token={}", token, e);
|
|
log.error("renewalToken error, token={}", token, e);
|
|
|
}
|
|
}
|
|
|
- });
|
|
|
|
|
|
|
+ }, "renewalToken", token);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private void cleanExpiredTokenAsync(String token) {
|
|
|
|
|
+ executeAsync(() -> cleanExpiredToken(token), "cleanExpiredToken", token);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private void executeAsync(Runnable task, String taskName, String token) {
|
|
|
|
|
+ try {
|
|
|
|
|
+ executor.execute(task);
|
|
|
|
|
+ } catch (RejectedExecutionException e) {
|
|
|
|
|
+ // 异步维护失败不能改变本次请求的鉴权结果
|
|
|
|
|
+ log.error("{} rejected, token={}", taskName, token, e);
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
/**
|
|
@@ -271,13 +269,13 @@ public class JwtInterceptor implements HandlerInterceptor {
|
|
|
*/
|
|
*/
|
|
|
private void cleanExpiredToken(String token) {
|
|
private void cleanExpiredToken(String token) {
|
|
|
try {
|
|
try {
|
|
|
- String telKey = "login.token.tel." + token;
|
|
|
|
|
|
|
+ String telKey = TOKEN_TEL_PREFIX + token;
|
|
|
String telNum = redisUtils.getString(telKey);
|
|
String telNum = redisUtils.getString(telKey);
|
|
|
if (StringUtils.isBlank(telNum)) {
|
|
if (StringUtils.isBlank(telNum)) {
|
|
|
redisUtils.del(telKey);
|
|
redisUtils.del(telKey);
|
|
|
return;
|
|
return;
|
|
|
}
|
|
}
|
|
|
- String listKey = "login.account.token.list." + telNum;
|
|
|
|
|
|
|
+ String listKey = TOKEN_LIST_PREFIX + telNum;
|
|
|
List<String> tokens = redisUtils.listRange(listKey);
|
|
List<String> tokens = redisUtils.listRange(listKey);
|
|
|
if (tokens == null || tokens.isEmpty()) {
|
|
if (tokens == null || tokens.isEmpty()) {
|
|
|
redisUtils.del(telKey);
|
|
redisUtils.del(telKey);
|
|
@@ -291,7 +289,7 @@ public class JwtInterceptor implements HandlerInterceptor {
|
|
|
if (StringUtils.isBlank(redisUtils.getString(loginKey))) {
|
|
if (StringUtils.isBlank(redisUtils.getString(loginKey))) {
|
|
|
// token 已过期,从列表和映射中移除
|
|
// token 已过期,从列表和映射中移除
|
|
|
redisUtils.listRemove(listKey, t);
|
|
redisUtils.listRemove(listKey, t);
|
|
|
- redisUtils.del("login.token.tel." + t);
|
|
|
|
|
|
|
+ redisUtils.del(TOKEN_TEL_PREFIX + t);
|
|
|
redisUtils.del("sign_key." + t);
|
|
redisUtils.del("sign_key." + t);
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
@@ -305,4 +303,4 @@ public class JwtInterceptor implements HandlerInterceptor {
|
|
|
ex) throws Exception {
|
|
ex) throws Exception {
|
|
|
LoginUserContext.remove();
|
|
LoginUserContext.remove();
|
|
|
}
|
|
}
|
|
|
-}
|
|
|
|
|
|
|
+}
|