服务间调用传递jwt

This commit is contained in:
panxuejie 2026-01-07 16:55:25 +08:00
parent 399efe49ff
commit f044c4e296
8 changed files with 230 additions and 11 deletions

View File

@ -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 {

View File

@ -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);
/**

View File

@ -33,6 +33,20 @@
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<!-- Spring Security -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
<optional>true</optional>
</dependency>
<!-- Servlet API -->
<dependency>
<groupId>jakarta.servlet</groupId>
<artifactId>jakarta.servlet-api</artifactId>
<scope>provided</scope>
</dependency>
<!-- Testing -->
<dependency>

View File

@ -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}")

View File

@ -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);
}
}
}
}
}

View File

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

View File

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

View File

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