微服务改造

This commit is contained in:
litianxiang
2026-04-22 11:15:36 +08:00
parent 67cb760f5f
commit a03cc14749
11 changed files with 39 additions and 504 deletions

View File

@@ -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;
}

View File

@@ -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 + "\"}");
}
}

View File

@@ -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);
}
}

View File

@@ -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();
}
}

View File

@@ -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());
}
}
}

View File

@@ -1,20 +0,0 @@
package com.aida.seller.config;
import com.aida.seller.common.security.filter.SellerAuthenticationFilter;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class FilterConfig {
@Bean
public FilterRegistrationBean<SellerAuthenticationFilter> authFilterRegistration(
SellerAuthenticationFilter sellerAuthenticationFilter) {
FilterRegistrationBean<SellerAuthenticationFilter> registration = new FilterRegistrationBean<>();
registration.setFilter(sellerAuthenticationFilter);
registration.addUrlPatterns("/*");
registration.setOrder(1);
return registration;
}
}

View File

@@ -1,13 +1,21 @@
package com.aida.seller.config;
import com.aida.seller.common.interceptor.UserContextInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import jakarta.annotation.Resource;
@Configuration
public class WebConfig {
public class WebConfig implements WebMvcConfigurer {
@Resource
private UserContextInterceptor userContextInterceptor;
@Bean
public CorsFilter corsFilter() {
@@ -22,4 +30,10 @@ public class WebConfig {
source.registerCorsConfiguration("/**", config);
return new CorsFilter(source);
}
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(userContextInterceptor)
.addPathPatterns("/**");
}
}

View File

@@ -61,12 +61,4 @@ public class DesignerController {
designerService.audit(dto);
return Response.success();
}
@Operation(summary = "清理用户登录缓存", description = "供 aida-back 登出时远程调用,清除 seller 本地 token 缓存")
@PostMapping("/cache/clear")
public Response<Void> clearCache(
@Parameter(description = "用户ID") @RequestParam Long userId) {
com.aida.seller.common.security.utils.SellerLocalCacheUtils.delTokenCache(userId);
return Response.success();
}
}

View File

@@ -1,99 +0,0 @@
package com.aida.seller.util;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.util.StrUtil;
import com.aida.seller.config.JwtConfig;
import io.jsonwebtoken.*;
import io.jsonwebtoken.security.Keys;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;
import javax.crypto.SecretKey;
import java.nio.charset.StandardCharsets;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
@Component
@RequiredArgsConstructor
public class JwtUtil {
private final JwtConfig jwtConfig;
private SecretKey getSecretKey() {
return Keys.hmacShaKeyFor(jwtConfig.getSecret().getBytes(StandardCharsets.UTF_8));
}
public String generateToken(Long userId, String username) {
Map<String, Object> claims = new HashMap<>();
claims.put("userId", userId);
claims.put("username", username);
return createToken(claims, username);
}
public String generateToken(Long userId, String username, Map<String, Object> additionalClaims) {
Map<String, Object> claims = new HashMap<>();
claims.put("userId", userId);
claims.put("username", username);
if (additionalClaims != null) {
claims.putAll(additionalClaims);
}
return createToken(claims, username);
}
private String createToken(Map<String, Object> claims, String subject) {
return Jwts.builder()
.claims(claims)
.subject(subject)
.issuedAt(new Date())
.expiration(new Date(System.currentTimeMillis() + jwtConfig.getExpiration()))
.signWith(getSecretKey())
.compact();
}
public Claims parseToken(String token) {
return Jwts.parser()
.verifyWith(getSecretKey())
.build()
.parseSignedClaims(token)
.getPayload();
}
public String getUsernameFromToken(String token) {
Claims claims = parseToken(token);
return claims.getSubject();
}
public Long getUserIdFromToken(String token) {
Claims claims = parseToken(token);
return claims.get("userId", Long.class);
}
public boolean isTokenExpired(String token) {
try {
Claims claims = parseToken(token);
return claims.getExpiration().before(new Date());
} catch (ExpiredJwtException e) {
return true;
}
}
public boolean validateToken(String token) {
if (StrUtil.isBlank(token)) {
return false;
}
try {
parseToken(token);
return true;
} catch (JwtException e) {
return false;
}
}
public String refreshToken(String token) {
Claims claims = parseToken(token);
String username = claims.getSubject();
Long userId = claims.get("userId", Long.class);
return generateToken(userId, username);
}
}