From f044c4e296c831a4f288d8c4cc1abf0664e519db Mon Sep 17 00:00:00 2001
From: panxuejie <15855548138@163.com>
Date: Wed, 7 Jan 2026 16:55:25 +0800
Subject: [PATCH] =?UTF-8?q?=E6=9C=8D=E5=8A=A1=E9=97=B4=E8=B0=83=E7=94=A8?=
=?UTF-8?q?=E4=BC=A0=E9=80=92jwt?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../java/com/tacit/common/utils/AesUtils.java | 7 +-
.../java/com/tacit/common/utils/JwtUtils.java | 5 +-
common/common-feign/pom.xml | 14 ++++
.../tacit/common/feign/AppApiFeignClient.java | 2 +-
.../common/feign/FeignAuthInterceptor.java | 65 +++++++++++++++
.../feign/config/FeignClientConfig.java | 21 +++++
.../tacit/admin/config/SecurityConfig.java | 45 ++++++++--
.../tacit/app/config/AppSecurityConfig.java | 82 +++++++++++++++++++
8 files changed, 230 insertions(+), 11 deletions(-)
create mode 100644 common/common-feign/src/main/java/com/tacit/common/feign/FeignAuthInterceptor.java
create mode 100644 common/common-feign/src/main/java/com/tacit/common/feign/config/FeignClientConfig.java
diff --git a/common/common-core/src/main/java/com/tacit/common/utils/AesUtils.java b/common/common-core/src/main/java/com/tacit/common/utils/AesUtils.java
index b173ae3..9a9a190 100644
--- a/common/common-core/src/main/java/com/tacit/common/utils/AesUtils.java
+++ b/common/common-core/src/main/java/com/tacit/common/utils/AesUtils.java
@@ -6,14 +6,19 @@ import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.security.SecureRandom;
import java.util.Base64;
+import java.util.Objects;
+/**
+ * AES加密工具类,使用AES-GCM算法进行可逆加密
+ */
public class AesUtils {
private static final String ALGORITHM = "AES/GCM/NoPadding";
private static final int GCM_IV_LENGTH = 12;
private static final int GCM_TAG_LENGTH = 128;
- private static final String SECRET_KEY = "1234567890123456";
+ // 密钥,优先从环境变量获取,其次使用默认密钥
+ private static final String SECRET_KEY = Objects.requireNonNullElse(System.getenv("AES_SECRET_KEY"), "1234567890123456");
public static String encrypt(String plainText) {
try {
diff --git a/common/common-core/src/main/java/com/tacit/common/utils/JwtUtils.java b/common/common-core/src/main/java/com/tacit/common/utils/JwtUtils.java
index e08cb3e..b8e1a88 100644
--- a/common/common-core/src/main/java/com/tacit/common/utils/JwtUtils.java
+++ b/common/common-core/src/main/java/com/tacit/common/utils/JwtUtils.java
@@ -9,11 +9,14 @@ import javax.crypto.SecretKey;
import java.nio.charset.StandardCharsets;
import java.util.Date;
import java.util.Map;
+import java.util.Objects;
@Slf4j
public class JwtUtils {
- private static final SecretKey SECRET_KEY = Keys.hmacShaKeyFor(CommonConstant.JWT_SECRET.getBytes(StandardCharsets.UTF_8));
+ // JWT密钥,优先从环境变量获取,其次使用配置常量
+ private static final String JWT_SECRET = Objects.requireNonNullElse(System.getenv("JWT_SECRET_KEY"), CommonConstant.JWT_SECRET);
+ private static final SecretKey SECRET_KEY = Keys.hmacShaKeyFor(JWT_SECRET.getBytes(StandardCharsets.UTF_8));
// private static final SecretKey SECRET_KEY = Keys.secretKeyFor(SignatureAlgorithm.HS256);
/**
diff --git a/common/common-feign/pom.xml b/common/common-feign/pom.xml
index 86eccd6..fd0d945 100644
--- a/common/common-feign/pom.xml
+++ b/common/common-feign/pom.xml
@@ -33,6 +33,20 @@
org.springframework.cloud
spring-cloud-starter-openfeign
+
+
+
+ org.springframework.boot
+ spring-boot-starter-security
+ true
+
+
+
+
+ jakarta.servlet
+ jakarta.servlet-api
+ provided
+
diff --git a/common/common-feign/src/main/java/com/tacit/common/feign/AppApiFeignClient.java b/common/common-feign/src/main/java/com/tacit/common/feign/AppApiFeignClient.java
index 3555d93..89c8f73 100644
--- a/common/common-feign/src/main/java/com/tacit/common/feign/AppApiFeignClient.java
+++ b/common/common-feign/src/main/java/com/tacit/common/feign/AppApiFeignClient.java
@@ -5,7 +5,7 @@ import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
-@FeignClient(name = "tacit-app-api", contextId = "appApiFeignClient")
+@FeignClient(name = "tacit-app-api", contextId = "appApiFeignClient", configuration = com.tacit.common.feign.config.FeignClientConfig.class)
public interface AppApiFeignClient {
@GetMapping("/user/info/{userId}")
diff --git a/common/common-feign/src/main/java/com/tacit/common/feign/FeignAuthInterceptor.java b/common/common-feign/src/main/java/com/tacit/common/feign/FeignAuthInterceptor.java
new file mode 100644
index 0000000..6a1c722
--- /dev/null
+++ b/common/common-feign/src/main/java/com/tacit/common/feign/FeignAuthInterceptor.java
@@ -0,0 +1,65 @@
+package com.tacit.common.feign;
+
+import com.tacit.common.constant.CommonConstant;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.context.SecurityContextHolder;
+import org.springframework.web.context.request.RequestAttributes;
+import org.springframework.web.context.request.RequestContextHolder;
+import org.springframework.web.context.request.ServletRequestAttributes;
+
+import feign.RequestInterceptor;
+import feign.RequestTemplate;
+import jakarta.servlet.http.HttpServletRequest;
+
+/**
+ * Feign拦截器,用于在服务间调用时传递用户上下文
+ */
+@Slf4j
+public class FeignAuthInterceptor implements RequestInterceptor {
+
+ @Override
+ public void apply(RequestTemplate template) {
+ // 从当前线程获取认证信息
+ Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
+
+ if (authentication != null && !(authentication instanceof org.springframework.security.authentication.AnonymousAuthenticationToken)) {
+ // 获取用户名
+ String username = authentication.getName();
+ template.header("X-Username", username);
+
+ // 获取角色信息
+ authentication.getAuthorities().forEach(authority -> {
+ template.header("X-Role", authority.getAuthority().replace("ROLE_", ""));
+ });
+ } else {
+ // 从RequestContextHolder中获取请求信息(适用于非Feign调用场景)
+ RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
+ if (requestAttributes instanceof ServletRequestAttributes) {
+ ServletRequestAttributes servletRequestAttributes = (ServletRequestAttributes) requestAttributes;
+ HttpServletRequest request = servletRequestAttributes.getRequest();
+
+ // 传递JWT令牌
+ String authorization = request.getHeader(CommonConstant.JWT_HEADER);
+ if (authorization != null && authorization.startsWith(CommonConstant.JWT_PREFIX)) {
+ template.header(CommonConstant.JWT_HEADER, authorization);
+ }
+
+ // 传递用户上下文
+ String userId = request.getHeader("X-User-Id");
+ String username = request.getHeader("X-Username");
+ String role = request.getHeader("X-Role");
+
+ if (userId != null) {
+ template.header("X-User-Id", userId);
+ }
+ if (username != null) {
+ template.header("X-Username", username);
+ }
+ if (role != null) {
+ template.header("X-Role", role);
+ }
+ }
+ }
+ }
+}
diff --git a/common/common-feign/src/main/java/com/tacit/common/feign/config/FeignClientConfig.java b/common/common-feign/src/main/java/com/tacit/common/feign/config/FeignClientConfig.java
new file mode 100644
index 0000000..50f6f51
--- /dev/null
+++ b/common/common-feign/src/main/java/com/tacit/common/feign/config/FeignClientConfig.java
@@ -0,0 +1,21 @@
+package com.tacit.common.feign.config;
+
+import com.tacit.common.feign.FeignAuthInterceptor;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+/**
+ * Feign客户端配置类,用于配置全局Feign拦截器
+ */
+@Configuration
+public class FeignClientConfig {
+
+ /**
+ * 注册Feign认证拦截器,用于在服务间调用时传递用户上下文
+ * @return FeignAuthInterceptor
+ */
+ @Bean
+ public FeignAuthInterceptor feignAuthInterceptor() {
+ return new FeignAuthInterceptor();
+ }
+}
diff --git a/tacit-admin/src/main/java/com/tacit/admin/config/SecurityConfig.java b/tacit-admin/src/main/java/com/tacit/admin/config/SecurityConfig.java
index f72d6a2..4cda981 100644
--- a/tacit-admin/src/main/java/com/tacit/admin/config/SecurityConfig.java
+++ b/tacit-admin/src/main/java/com/tacit/admin/config/SecurityConfig.java
@@ -1,8 +1,12 @@
package com.tacit.admin.config;
+import com.tacit.common.constant.CommonConstant;
import com.tacit.common.utils.AesPasswordEncoder;
+import com.tacit.common.utils.JwtUtils;
+import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
+import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
@@ -25,6 +29,7 @@ import java.util.Collections;
@Configuration
@EnableWebSecurity
@EnableMethodSecurity(prePostEnabled = true)
+@Slf4j
public class SecurityConfig {
@Bean
@@ -34,7 +39,8 @@ public class SecurityConfig {
.cors(AbstractHttpConfigurer::disable)
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(auth -> auth
- .requestMatchers("/auth/**", "/test/**", "/v3/api-docs/**", "/swagger-ui/**", "/swagger-ui.html").permitAll()
+ .requestMatchers("/auth/**", "/test/hello", "/v3/api-docs/**", "/swagger-ui/**", "/swagger-ui.html").permitAll()
+ .requestMatchers("/test/feign/**").authenticated()
.anyRequest().authenticated()
)
.addFilterBefore(authenticationFilter(), org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter.class);
@@ -47,14 +53,37 @@ public class SecurityConfig {
return new OncePerRequestFilter() {
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
- String userId = request.getHeader("X-User-Id");
- String username = request.getHeader("X-Username");
- String role = request.getHeader("X-Role");
+ // 先检查是否有JWT令牌(直接服务间调用时可能传递JWT)
+ String authorization = request.getHeader(CommonConstant.JWT_HEADER);
+ if (authorization != null && authorization.startsWith(CommonConstant.JWT_PREFIX)) {
+ String token = authorization.substring(CommonConstant.JWT_PREFIX.length());
+ try {
+ // 验证JWT令牌
+ if (JwtUtils.validateToken(token)) {
+ // 从令牌中获取用户信息
+ String username = JwtUtils.getUsernameFromToken(token);
+ String role = JwtUtils.getRoleFromToken(token);
+
+ // 创建认证对象
+ User principal = new User(username, "", Collections.singletonList(new SimpleGrantedAuthority("ROLE_" + role.toUpperCase())));
+ var authentication = new UsernamePasswordAuthenticationToken(principal, null, principal.getAuthorities());
+ SecurityContextHolder.getContext().setAuthentication(authentication);
+ }
+ } catch (Exception e) {
+ log.error("JWT令牌验证失败: {}", e.getMessage());
+ }
+ } else {
+ // 检查是否有用户上下文头(网关转发时添加)
+ String userId = request.getHeader("X-User-Id");
+ String username = request.getHeader("X-Username");
+ String role = request.getHeader("X-Role");
- if (userId != null && username != null && role != null) {
- User principal = new User(username, "", Collections.singletonList(new SimpleGrantedAuthority("ROLE_" + role.toUpperCase())));
- var authentication = new org.springframework.security.authentication.UsernamePasswordAuthenticationToken(principal, null, principal.getAuthorities());
- SecurityContextHolder.getContext().setAuthentication(authentication);
+ if (userId != null && username != null && role != null) {
+ // 创建认证对象
+ User principal = new User(username, "", Collections.singletonList(new SimpleGrantedAuthority("ROLE_" + role.toUpperCase())));
+ var authentication = new UsernamePasswordAuthenticationToken(principal, null, principal.getAuthorities());
+ SecurityContextHolder.getContext().setAuthentication(authentication);
+ }
}
filterChain.doFilter(request, response);
diff --git a/tacit-app-api/src/main/java/com/tacit/app/config/AppSecurityConfig.java b/tacit-app-api/src/main/java/com/tacit/app/config/AppSecurityConfig.java
index bd3a501..667f283 100644
--- a/tacit-app-api/src/main/java/com/tacit/app/config/AppSecurityConfig.java
+++ b/tacit-app-api/src/main/java/com/tacit/app/config/AppSecurityConfig.java
@@ -1,13 +1,95 @@
package com.tacit.app.config;
+import com.tacit.common.constant.CommonConstant;
import com.tacit.common.utils.AesPasswordEncoder;
+import com.tacit.common.utils.JwtUtils;
+import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
+import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
+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.config.http.SessionCreationPolicy;
+import org.springframework.security.core.authority.SimpleGrantedAuthority;
+import org.springframework.security.core.context.SecurityContextHolder;
+import org.springframework.security.core.userdetails.User;
import org.springframework.security.crypto.password.PasswordEncoder;
+import org.springframework.security.web.SecurityFilterChain;
+import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
+import org.springframework.web.filter.OncePerRequestFilter;
+
+import jakarta.servlet.FilterChain;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import java.io.IOException;
+import java.util.Collections;
@Configuration
+@EnableWebSecurity
+@EnableMethodSecurity(prePostEnabled = true)
+@Slf4j
public class AppSecurityConfig {
+ @Bean
+ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
+ http
+ .csrf(AbstractHttpConfigurer::disable)
+ .cors(AbstractHttpConfigurer::disable)
+ .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
+ .authorizeHttpRequests(auth -> auth
+ .requestMatchers("/auth/**", "/v3/api-docs/**", "/swagger-ui/**", "/swagger-ui.html", "/user/info/**").permitAll()
+ .anyRequest().authenticated()
+ )
+ .addFilterBefore(authenticationFilter(), UsernamePasswordAuthenticationFilter.class);
+
+ return http.build();
+ }
+
+ @Bean
+ public OncePerRequestFilter authenticationFilter() {
+ return new OncePerRequestFilter() {
+ @Override
+ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
+ // 先检查是否有JWT令牌(直接服务间调用时可能传递JWT)
+ String authorization = request.getHeader(CommonConstant.JWT_HEADER);
+ if (authorization != null && authorization.startsWith(CommonConstant.JWT_PREFIX)) {
+ String token = authorization.substring(CommonConstant.JWT_PREFIX.length());
+ try {
+ // 验证JWT令牌
+ if (JwtUtils.validateToken(token)) {
+ // 从令牌中获取用户信息
+ Long userId = JwtUtils.getUserIdFromToken(token);
+ String username = JwtUtils.getUsernameFromToken(token);
+ String role = JwtUtils.getRoleFromToken(token);
+
+ // 创建认证对象
+ User principal = new User(username, "", Collections.singletonList(new SimpleGrantedAuthority("ROLE_" + role.toUpperCase())));
+ var authentication = new org.springframework.security.authentication.UsernamePasswordAuthenticationToken(principal, null, principal.getAuthorities());
+ SecurityContextHolder.getContext().setAuthentication(authentication);
+ }
+ } catch (Exception e) {
+ log.error("JWT令牌验证失败: {}", e.getMessage());
+ }
+ } else {
+ // 检查是否有用户上下文头(网关转发或服务间调用时添加)
+ String userId = request.getHeader("X-User-Id");
+ String username = request.getHeader("X-Username");
+ String role = request.getHeader("X-Role");
+
+ if (userId != null && username != null && role != null) {
+ User principal = new User(username, "", Collections.singletonList(new SimpleGrantedAuthority("ROLE_" + role.toUpperCase())));
+ var authentication = new org.springframework.security.authentication.UsernamePasswordAuthenticationToken(principal, null, principal.getAuthorities());
+ SecurityContextHolder.getContext().setAuthentication(authentication);
+ }
+ }
+
+ filterChain.doFilter(request, response);
+ }
+ };
+ }
+
@Bean
public PasswordEncoder passwordEncoder() {
return new AesPasswordEncoder();