微服务改造
This commit is contained in:
31
src/main/java/com/ai/da/common/config/SecurityConfig.java
Normal file
31
src/main/java/com/ai/da/common/config/SecurityConfig.java
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
131
src/main/java/com/ai/da/common/utils/TokenGenerateUtils.java
Normal file
131
src/main/java/com/ai/da/common/utils/TokenGenerateUtils.java
Normal 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 生成工具类(仅负责生成,不负责鉴权)。
|
||||||
|
* 鉴权逻辑已迁移至 Gateway(GlobalAuthWebFilter)。
|
||||||
|
*/
|
||||||
|
@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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
134
src/main/resources/application.yml
Normal file
134
src/main/resources/application.yml
Normal 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
|
||||||
Reference in New Issue
Block a user