微服务改造
This commit is contained in:
@@ -1,21 +0,0 @@
|
||||
package com.aida.seller.common.security.config;
|
||||
|
||||
import lombok.Data;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
@Data
|
||||
@Configuration
|
||||
@ConfigurationProperties(prefix = "spring.security")
|
||||
public class SecurityProperties {
|
||||
|
||||
private String jwtSecret;
|
||||
|
||||
private String jwtTokenHeader = "Authorization";
|
||||
|
||||
private String jwtTokenPrefix = "Bearer-";
|
||||
|
||||
private long jwtExpiration;
|
||||
|
||||
private String[] ignorePaths;
|
||||
}
|
||||
@@ -1,134 +0,0 @@
|
||||
package com.aida.seller.common.security.filter;
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.aida.seller.common.context.UserContext;
|
||||
import com.aida.seller.common.security.config.SecurityProperties;
|
||||
import com.aida.seller.common.security.jwt.SellerJwtHelper;
|
||||
import com.aida.seller.common.security.utils.SellerRedisUtil;
|
||||
import com.aida.seller.model.vo.AuthPrincipalVo;
|
||||
import jakarta.servlet.FilterChain;
|
||||
import jakarta.servlet.ServletException;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.lang.NonNull;
|
||||
import org.springframework.stereotype.Component;
|
||||
import com.aida.seller.common.security.utils.SellerLocalCacheUtils;
|
||||
import org.springframework.web.filter.OncePerRequestFilter;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
@Slf4j
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class SellerAuthenticationFilter extends OncePerRequestFilter {
|
||||
|
||||
private final SellerJwtHelper jwtHelper;
|
||||
private final SecurityProperties securityProperties;
|
||||
private final SellerRedisUtil redisUtil;
|
||||
|
||||
private static final List<String> FILTER_URL = Arrays.asList(
|
||||
"/favicon.ico", "/doc.html", "/swagger-ui.html",
|
||||
"/swagger-resources", "/swagger-resources/", "/swagger-resources/configuration/ui",
|
||||
"/swagger-resources/configuration/security", "/webjars/", "/v2/api-docs",
|
||||
"/v3/api-docs", "/v3/api-docs/swagger-config",
|
||||
"/api/account/login", "/api/account/preLogin",
|
||||
"/api/designer/check"
|
||||
);
|
||||
|
||||
@Override
|
||||
protected void doFilterInternal(
|
||||
@NonNull HttpServletRequest request,
|
||||
@NonNull HttpServletResponse response,
|
||||
@NonNull FilterChain filterChain) throws ServletException, IOException {
|
||||
|
||||
String requestURI = request.getRequestURI();
|
||||
|
||||
if (isIgnoredPath(requestURI)) {
|
||||
filterChain.doFilter(request, response);
|
||||
return;
|
||||
}
|
||||
|
||||
String jwtToken = request.getHeader(securityProperties.getJwtTokenHeader());
|
||||
if (StrUtil.isBlank(jwtToken)) {
|
||||
writeUnauthorized(response, "请传入token!");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// 1. JWT 签名验证
|
||||
if (!jwtHelper.validateToken(jwtToken)) {
|
||||
writeUnauthorized(response, "Token无效!");
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. 解析用户信息
|
||||
AuthPrincipalVo principal = jwtHelper.parserToUser(jwtToken);
|
||||
if (principal == null || principal.getId() == null) {
|
||||
writeUnauthorized(response, "Token解析失败!");
|
||||
return;
|
||||
}
|
||||
|
||||
// 3. 本地缓存比对
|
||||
String cacheToken = SellerLocalCacheUtils.getTokenCache(principal.getId());
|
||||
if (StrUtil.isNotBlank(cacheToken)) {
|
||||
// 本地缓存有,直接比对
|
||||
if (!cacheToken.equals(jwtToken)) {
|
||||
writeUnauthorized(response, "Token已被顶替,请重新登录!");
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
// 本地缓存没有,查 Redis
|
||||
String redisToken = redisUtil.getLoginToken(principal.getId());
|
||||
if (!StrUtil.isNotBlank(redisToken)) {
|
||||
// Redis 也没有,说明真的失效了
|
||||
writeUnauthorized(response, "Token已过期,请重新登录!");
|
||||
return;
|
||||
}
|
||||
if (!redisToken.equals(jwtToken)) {
|
||||
writeUnauthorized(response, "Token已被顶替,请重新登录!");
|
||||
return;
|
||||
}
|
||||
// Redis 有, 回填到本地缓存,减少后续 Redis 访问
|
||||
SellerLocalCacheUtils.setTokenCache(principal.getId(), jwtToken);
|
||||
}
|
||||
|
||||
// 4. 设置用户上下文
|
||||
UserContext.delete();
|
||||
UserContext.setUserHolder(principal);
|
||||
filterChain.doFilter(request, response);
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("JWT验证失败: {}", e.getMessage());
|
||||
writeUnauthorized(response, "Token已过期,请重新登录!");
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isIgnoredPath(String requestURI) {
|
||||
// 检查配置文件中的白名单
|
||||
if (securityProperties.getIgnorePaths() != null) {
|
||||
for (String path : securityProperties.getIgnorePaths()) {
|
||||
String pattern = path.replace("/**", "").replace("*", "");
|
||||
if (requestURI.contains(pattern)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
// 检查硬编码的白名单
|
||||
for (String url : FILTER_URL) {
|
||||
if (requestURI.contains(url)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private void writeUnauthorized(HttpServletResponse response, String message) throws IOException {
|
||||
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
|
||||
response.setContentType("application/json");
|
||||
response.getWriter().write("{\"code\":401,\"message\":\"" + message + "\"}");
|
||||
}
|
||||
}
|
||||
@@ -1,67 +0,0 @@
|
||||
package com.aida.seller.common.security.jwt;
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.hutool.crypto.digest.DigestUtil;
|
||||
import com.aida.seller.common.security.config.SecurityProperties;
|
||||
import com.aida.seller.model.vo.AuthPrincipalVo;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import io.jsonwebtoken.Claims;
|
||||
import io.jsonwebtoken.Jwts;
|
||||
import io.jsonwebtoken.security.Keys;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import javax.crypto.SecretKey;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
@Slf4j
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class SellerJwtHelper {
|
||||
|
||||
private final SecurityProperties securityProperties;
|
||||
private final ObjectMapper objectMapper;
|
||||
|
||||
private static final String ISSUER = "DWJ";
|
||||
|
||||
public boolean validateToken(String token) {
|
||||
try {
|
||||
Claims claims = parser(token);
|
||||
return claims != null && StrUtil.isNotEmpty(claims.getSubject());
|
||||
} catch (Exception e) {
|
||||
log.error("JWT签名验证失败: {}", e.getMessage());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public AuthPrincipalVo parserToUser(String token) {
|
||||
try {
|
||||
String subject = parser(token).getSubject();
|
||||
if (StrUtil.isNotEmpty(subject)) {
|
||||
return objectMapper.readValue(subject, AuthPrincipalVo.class);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("JWT解析用户信息失败: {}", e.getMessage());
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public Claims parser(String token) {
|
||||
token = token.replaceAll(securityProperties.getJwtTokenPrefix(), "");
|
||||
SecretKey key = buildSigningKey();
|
||||
return Jwts.parser()
|
||||
.verifyWith(key)
|
||||
.build()
|
||||
.parseSignedClaims(token)
|
||||
.getPayload();
|
||||
}
|
||||
|
||||
private SecretKey buildSigningKey() {
|
||||
byte[] raw = securityProperties.getJwtSecret().getBytes(StandardCharsets.UTF_8);
|
||||
if (raw.length < 32) {
|
||||
raw = DigestUtil.sha256(raw);
|
||||
}
|
||||
return Keys.hmacShaKeyFor(raw);
|
||||
}
|
||||
}
|
||||
@@ -1,57 +0,0 @@
|
||||
package com.aida.seller.common.security.utils;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
@Slf4j
|
||||
public class SellerLocalCacheUtils {
|
||||
|
||||
private static final ConcurrentHashMap<Long, CacheEntry> TOKEN_CACHE = new ConcurrentHashMap<>();
|
||||
|
||||
private static final long EXPIRE_HOURS = 24 * 7 - 1;
|
||||
|
||||
private static class CacheEntry {
|
||||
private final String token;
|
||||
private final long expireTime;
|
||||
|
||||
CacheEntry(String token, long expireTime) {
|
||||
this.token = token;
|
||||
this.expireTime = expireTime;
|
||||
}
|
||||
|
||||
String getToken() {
|
||||
return token;
|
||||
}
|
||||
|
||||
boolean isExpired() {
|
||||
return System.currentTimeMillis() > expireTime;
|
||||
}
|
||||
}
|
||||
|
||||
public static void setTokenCache(Long userId, String token) {
|
||||
long expireTime = System.currentTimeMillis() + TimeUnit.HOURS.toMillis(EXPIRE_HOURS);
|
||||
TOKEN_CACHE.put(userId, new CacheEntry(token, expireTime));
|
||||
}
|
||||
|
||||
public static String getTokenCache(Long userId) {
|
||||
CacheEntry entry = TOKEN_CACHE.get(userId);
|
||||
if (entry == null) {
|
||||
return null;
|
||||
}
|
||||
if (entry.isExpired()) {
|
||||
TOKEN_CACHE.remove(userId);
|
||||
return null;
|
||||
}
|
||||
return entry.getToken();
|
||||
}
|
||||
|
||||
public static void delTokenCache(Long userId) {
|
||||
TOKEN_CACHE.remove(userId);
|
||||
}
|
||||
|
||||
public static void clearAll() {
|
||||
TOKEN_CACHE.clear();
|
||||
}
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
package com.aida.seller.common.security.utils;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.data.redis.core.RedisTemplate;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
@Slf4j
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class SellerRedisUtil {
|
||||
|
||||
private final RedisTemplate<String, Object> redisTemplate;
|
||||
|
||||
public static final String LOGIN_TOKEN_KEY = "login:token:";
|
||||
|
||||
public String getLoginToken(Long userId) {
|
||||
try {
|
||||
Object value = redisTemplate.opsForValue().get(LOGIN_TOKEN_KEY + userId);
|
||||
return value != null ? value.toString() : null;
|
||||
} catch (Exception e) {
|
||||
log.error("Redis getLoginToken error, userId: {}, error: {}", userId, e.getMessage());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public void setLoginToken(Long userId, String token, long expireMillis) {
|
||||
try {
|
||||
long expireSeconds = expireMillis / 1000;
|
||||
if (expireSeconds <= 0) {
|
||||
expireSeconds = 1;
|
||||
}
|
||||
redisTemplate.opsForValue().set(LOGIN_TOKEN_KEY + userId, token, expireSeconds, TimeUnit.SECONDS);
|
||||
} catch (Exception e) {
|
||||
log.error("Redis setLoginToken error, userId: {}, error: {}", userId, e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user