微服务改造
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);
|
||||
}
|
||||
Reference in New Issue
Block a user