微服务改造
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