微服务改造

This commit is contained in:
litianxiang
2026-04-22 11:16:03 +08:00
parent 92e7dbf258
commit d0b8b8d674
5 changed files with 368 additions and 0 deletions

View File

@@ -0,0 +1,31 @@
package com.ai.da.common.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.web.SecurityFilterChain;
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.csrf(AbstractHttpConfigurer::disable)
.authorizeHttpRequests(auth -> auth
.requestMatchers(
"/doc.html",
"/swagger-ui/**",
"/swagger-resources/**",
"/v2/api-docs/**",
"/v3/api-docs/**",
"/webjars/**"
).permitAll()
.anyRequest().permitAll() // 先全部允许,后续根据业务需要收紧
);
return http.build();
}
}

View File

@@ -0,0 +1,51 @@
package com.ai.da.common.interceptor;
import com.ai.da.common.context.UserContext;
import com.ai.da.model.vo.AuthPrincipalVo;
import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpHeaders;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
/**
* 从 Gateway 转发的请求头中读取已鉴权的用户身份,写入 UserContext。
* <p>
* Gateway 验证 JWT 后将 X-User-Id 和 X-User-Info 写入请求头,
* 此拦截器负责将信息填充到 ThreadLocal供业务代码使用。
* 不需要 Gateway 鉴权路径(如 login、静态资源不会有这两个头
* 此时 UserContext 保持为空,业务代码应自行处理。
*/
@Slf4j
@Component
@RequiredArgsConstructor
public class UserContextInterceptor implements HandlerInterceptor {
private static final String USER_ID_HEADER = "X-User-Id";
private static final String USER_INFO_HEADER = "X-User-Info";
private final ObjectMapper objectMapper;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
String userInfoJson = request.getHeader(USER_INFO_HEADER);
if (userInfoJson != null && !userInfoJson.isBlank()) {
try {
AuthPrincipalVo principal = objectMapper.readValue(userInfoJson, AuthPrincipalVo.class);
UserContext.setUserHolder(principal);
} catch (Exception e) {
log.warn("Failed to parse X-User-Info header: {}", e.getMessage());
}
}
return true;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response,
Object handler, Exception ex) {
UserContext.delete();
}
}

View File

@@ -0,0 +1,131 @@
package com.ai.da.common.utils;
import cn.hutool.core.util.StrUtil;
import cn.hutool.crypto.digest.DigestUtil;
import com.ai.da.common.constant.CommonConstant;
import com.ai.da.model.vo.AuthPrincipalVo;
import com.alibaba.fastjson.JSONObject;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.security.Keys;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import javax.crypto.SecretKey;
import java.nio.charset.StandardCharsets;
import java.util.Date;
/**
* Token 生成工具类(仅负责生成,不负责鉴权)。
* 鉴权逻辑已迁移至 GatewayGlobalAuthWebFilter
*/
@Slf4j
@Component
public class TokenGenerateUtils {
private static final String ISSUER = "DWJ";
@Value("${spring.security.jwtSecret:JWTSECRET}")
private String jwtSecret;
@Value("${spring.security.jwtExpiration:8640000000}")
private long jwtExpiration;
@Value("${spring.security.jwtTokenPrefix:Bearer-}")
private String jwtTokenPrefix;
/**
* 生成 JWT Token。
* @param principal 用户信息
* @return 完整的 token含 prefix
*/
public String createToken(AuthPrincipalVo principal) {
SecretKey key = buildSigningKey();
String token = Jwts.builder()
.id(String.valueOf(principal.getId()))
.subject(JSONObject.toJSONString(principal))
.issuedAt(new Date())
.issuer(ISSUER)
.expiration(new Date(System.currentTimeMillis() + jwtExpiration))
.signWith(key)
.compact();
return jwtTokenPrefix + token;
}
/**
* 获取 Token 过期时间(毫秒)。
*/
public long getJwtExpiration() {
return jwtExpiration;
}
/**
* 生成用于邮箱变更的简化 Token。
* @param userId 用户 ID
* @param mailbox 新邮箱
* @return token不含 prefix
*/
public String createMailboxToken(Long userId, String mailbox) {
SecretKey key = buildSigningKey();
return Jwts.builder()
.id(String.valueOf(userId))
.subject(mailbox + "_" + userId)
.issuedAt(new Date())
.issuer(ISSUER)
.expiration(new Date(System.currentTimeMillis() + CommonConstant.CHANGE_MAILBOX_LINK_VALIDITY))
.signWith(key)
.compact();
}
/**
* 验证 Token 是否有效(签名和有效期)。
*/
public boolean validateToken(String token) {
try {
Claims claims = parseTokenBody(token);
return claims != null;
} catch (Exception e) {
return false;
}
}
/**
* 从 Token 中解析用户信息。
*/
public AuthPrincipalVo parserToUser(String token) {
try {
String subject = parseTokenBody(token).getSubject();
if (StrUtil.isNotEmpty(subject)) {
return JSONObject.parseObject(subject, AuthPrincipalVo.class);
}
} catch (Exception e) {
log.error("JWT解析用户信息失败: {}", e.getMessage());
}
return null;
}
/**
* 解析邮箱变更 Token返回 "email_id" 格式字符串。
*/
public String parseMailboxToken(String token) {
return parseTokenBody(token).getSubject();
}
private Claims parseTokenBody(String token) {
SecretKey key = buildSigningKey();
return Jwts.parser()
.verifyWith(key)
.build()
.parseSignedClaims(token)
.getPayload();
}
private SecretKey buildSigningKey() {
byte[] raw = jwtSecret.getBytes(StandardCharsets.UTF_8);
if (raw.length < 32) {
raw = DigestUtil.sha256(raw);
}
return Keys.hmacShaKeyFor(raw);
}
}

View File

@@ -0,0 +1,21 @@
package com.ai.da.feign.gateway;
import com.ai.da.common.response.Response;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
/**
* 调用 Gateway 黑名单接口,将指定用户的 token 加入黑名单。
* 替代原来的 SellerFeignClient.clearTokenCache。
*/
@FeignClient(name = "aida-gateway", path = "/internal")
public interface GatewayFeignClient {
/**
* 将用户 token 加入黑名单。
* 后续 Gateway 会拒绝携带该用户 token 的请求。
*/
@PostMapping("/logout")
Response<Void> logout(@RequestParam("userId") Long userId);
}

View File

@@ -0,0 +1,134 @@
# ============================================================
# aida-back - 本地配置(不区分环境)
# 公共配置DB、Redis、RabbitMQ、MinIO、API Keys 等)由 Nacos 统一管理
# 此文件仅包含 back 服务私有的业务配置
# ============================================================
server:
port: 5567
spring:
application:
name: aida-back
# ---------- 副数据源back 私有,由 Nacos 统一管理) ----------
# ---------- Token 生成参数(由 TokenGenerateUtils 使用) ----------
security:
jwtSecret: ${JWT_SECRET:JWTSECRET}
jwtTokenHeader: Authorization
jwtTokenPrefix: Bearer-
jwtExpiration: ${JWT_EXPIRATION:8640000000}
# ---------- Python 服务 ----------
access:
python:
ip: ${PYTHON_HOST:http://18.167.251.121}
port: 9994
generate_sr_port: 9994
address: http://18.167.251.121:9994
# ---------- MinIO Buckets ----------
minio:
bucketName:
clothing: aida-clothing
mannequins: aida-mannequins
results: aida-results
sysImage: aida-sys-image
users: aida-users
collectionElement: aida-collection-element
gradient: aida-gradient
modifiedSketch: aida-modified-sketch
slogan: aida-slogan
partialDesign: aida-partial-design
globalAward: global-award
# ---------- Redis Keys ----------
redis:
key:
orderForGenerate: OrderForGenerate
generateCancelSet: GenerateCancelSet
generateExceptionMap: Generate:Exception
resultMap: ResultMap
orderForSR: OrderForSR
SRCancelSet: SRCancelSet
SRExceptionMap: SRExceptionMap
taskList: TaskList
credits:
pre-deduction: Credits:PreDeduction
generateResult: Generate:Result
toProductImageResultKey: ToProductImage:Result
relightResultKey: Relight:Result
newPosted: LastViewNewPostedTime
maximumUserId: CodeCreate:MaximumUserId
# ---------- RabbitMQ 队列 ----------
rabbitmq:
queues:
generate: generate-queue
sr: SR-queue
srResult: SuperResolution
generateResult: GenerateImage
toProductImageResult: ToProductImage
relightResult: Relight
poseTransform: PoseTransform
designBatch: DesignBatch
relightBatch: BatchRelight
toProductImageBatch: BatchToProductImage
poseTransformBatch: BatchPoseTransform
emailRetry: emailRetry-business
exchange:
generate: generate-exchange
dead-letter:
exchange: dlx.email-retry
queue: dlx.email-retry.queue
routing-key: dlx.email-retry.key
# ---------- 第三方服务 ----------
orderList:
link: ${ORDER_LINK:https://develop.aida.com.hk/home/homePage?order=}
stripe:
webhook:
fail:
reminder: 0
paymentMethodConfiguration: pmc_1QIKyq02n1TEydyNKVEYvhW7
google:
client:
id: ${GOOGLE_CLIENT_ID:157095842121-kdd1fdf8m8nudvj9sprstb2k2prnf9e4.apps.googleusercontent.com}
secret: ${GOOGLE_CLIENT_SECRET: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}
redirect:
uri: ${GOOGLE_REDIRECT_URI:https://develop.api.aida.com.hk/api/third/party/auth/google_callback}
design:
callback:
url: ${DESIGN_CALLBACK_URL:https://develop.api.aida.com.hk/api/third/party/receiveDesignResults}
redirect:
url: ${REDIRECT_URL:http://18.167.251.121:7788}
global:
award:
link: https://aida-global-design-awards.com.hk/contestants?id=
# ---------- 文件上传 ----------
file:
upload:
temp:
dir: temp/uploads
chunk:
size:
pdf: 1048576
video: 2097152
max:
size:
pdf: 20971520
video: 104857600
task:
expiry:
hours: 24
logging:
level:
com.aida: debug