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();