更新
This commit is contained in:
@@ -5,8 +5,14 @@ public final class SecurityConstants {
|
||||
public static final String HEADER_USER_ID = "X-User-Id";
|
||||
public static final String HEADER_USERNAME = "X-Username";
|
||||
public static final String HEADER_DISPLAY_NAME = "X-Display-Name";
|
||||
public static final String HEADER_ADCODE = "X-Adcode";
|
||||
public static final String HEADER_TENANT_ID = "X-Tenant-Id";
|
||||
public static final String HEADER_TENANT_PATH = "X-Tenant-Path";
|
||||
public static final String HEADER_DEPT_ID = "X-Dept-Id";
|
||||
public static final String HEADER_DEPT_PATH = "X-Dept-Path";
|
||||
public static final String HEADER_ROLE_CODES = "X-Role-Codes";
|
||||
public static final String HEADER_CLIENT_TYPE = "X-Client-Type";
|
||||
public static final String HEADER_SESSION_ID = "X-Session-Id";
|
||||
|
||||
private SecurityConstants() {
|
||||
}
|
||||
|
||||
45
backend/common/common-feign/pom.xml
Normal file
45
backend/common/common-feign/pom.xml
Normal file
@@ -0,0 +1,45 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<parent>
|
||||
<groupId>com.k12study</groupId>
|
||||
<artifactId>k12study-backend</artifactId>
|
||||
<version>0.1.0-SNAPSHOT</version>
|
||||
<relativePath>../../pom.xml</relativePath>
|
||||
</parent>
|
||||
<artifactId>common-feign</artifactId>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-web</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<artifactId>spring-cloud-starter-openfeign</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.github.openfeign</groupId>
|
||||
<artifactId>feign-okhttp</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<artifactId>spring-cloud-starter-loadbalancer</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework</groupId>
|
||||
<artifactId>spring-webmvc</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.alibaba.cloud</groupId>
|
||||
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
||||
@@ -0,0 +1,76 @@
|
||||
package com.k12study.common.feign.config;
|
||||
|
||||
import com.k12study.common.feign.contract.FeignContractValidator;
|
||||
import com.k12study.common.feign.interceptor.FeignAuthRelayInterceptor;
|
||||
import feign.Client;
|
||||
import feign.Logger;
|
||||
import feign.RequestInterceptor;
|
||||
import feign.okhttp.OkHttpClient;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import org.springframework.boot.autoconfigure.AutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||
import org.springframework.cloud.openfeign.EnableFeignClients;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
|
||||
/**
|
||||
* @description Feign 自动装配入口;引入本模块即启用 @EnableFeignClients、OkHttp 客户端、Authorization 透传拦截器与启动期契约自检
|
||||
* @filename FeignAutoConfiguration.java
|
||||
* @author wangys
|
||||
* @copyright xyzh
|
||||
* @since 2026-04-17
|
||||
*/
|
||||
@AutoConfiguration
|
||||
@ConditionalOnClass(name = "org.springframework.cloud.openfeign.FeignClient")
|
||||
@EnableConfigurationProperties(FeignClientProperties.class)
|
||||
@EnableFeignClients(basePackages = "com.k12study.api")
|
||||
public class FeignAutoConfiguration {
|
||||
|
||||
/** 默认透传 Authorization 到下游 Feign 调用;业务方自定义时会覆盖此 Bean */
|
||||
@Bean
|
||||
@ConditionalOnMissingBean(FeignAuthRelayInterceptor.class)
|
||||
public RequestInterceptor feignAuthRelayInterceptor() {
|
||||
return new FeignAuthRelayInterceptor();
|
||||
}
|
||||
|
||||
/** 将配置中的日志级别转为 Feign Logger.Level Bean,Feign 会自动应用到所有客户端 */
|
||||
@Bean
|
||||
@ConditionalOnMissingBean(Logger.Level.class)
|
||||
public Logger.Level feignLoggerLevel(FeignClientProperties properties) {
|
||||
return properties.getLogLevel();
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用 OkHttp 作为底层 HTTP 客户端;相比 JDK HttpURLConnection 提供连接池、HTTP/2、细粒度超时,
|
||||
* 生产环境高并发必备。classpath 无 okhttp3 时不生效(Conditional)。
|
||||
*/
|
||||
@Bean
|
||||
@ConditionalOnMissingBean(Client.class)
|
||||
@ConditionalOnClass(name = "okhttp3.OkHttpClient")
|
||||
public Client feignOkHttpClient(FeignClientProperties properties) {
|
||||
okhttp3.OkHttpClient delegate = new okhttp3.OkHttpClient.Builder()
|
||||
.connectTimeout(properties.getConnectTimeout().toMillis(), TimeUnit.MILLISECONDS)
|
||||
.readTimeout(properties.getReadTimeout().toMillis(), TimeUnit.MILLISECONDS)
|
||||
.writeTimeout(properties.getWriteTimeout().toMillis(), TimeUnit.MILLISECONDS)
|
||||
.retryOnConnectionFailure(properties.isRetryOnConnectionFailure())
|
||||
.connectionPool(new okhttp3.ConnectionPool(
|
||||
properties.getMaxIdleConnections(),
|
||||
properties.getKeepAliveDuration().toMillis(),
|
||||
TimeUnit.MILLISECONDS))
|
||||
.build();
|
||||
return new OkHttpClient(delegate);
|
||||
}
|
||||
|
||||
/**
|
||||
* 启动期契约自检:对比 Feign 接口路径与本地 RequestMappingHandlerMapping 注册的 Controller 路径,
|
||||
* 不一致即告警。仅 Provider 进程(同时持有接口 + Controller)生效,纯 Consumer 自动跳过。
|
||||
*/
|
||||
@Bean
|
||||
@ConditionalOnClass(name = "org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping")
|
||||
@ConditionalOnProperty(prefix = "k12study.feign", name = "contract-validate", havingValue = "true", matchIfMissing = true)
|
||||
public FeignContractValidator feignContractValidator() {
|
||||
return new FeignContractValidator("com.k12study.api");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
package com.k12study.common.feign.config;
|
||||
|
||||
import java.time.Duration;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
|
||||
/**
|
||||
* @description Feign 客户端外部化参数;前缀 k12study.feign,覆盖 OkHttp 超时/连接池/日志级别/契约自检开关
|
||||
* @filename FeignClientProperties.java
|
||||
* @author wangys
|
||||
* @copyright xyzh
|
||||
* @since 2026-04-17
|
||||
*/
|
||||
@ConfigurationProperties(prefix = "k12study.feign")
|
||||
public class FeignClientProperties {
|
||||
|
||||
/** 连接建立超时 */
|
||||
private Duration connectTimeout = Duration.ofSeconds(5);
|
||||
|
||||
/** 读超时 */
|
||||
private Duration readTimeout = Duration.ofSeconds(10);
|
||||
|
||||
/** 写超时 */
|
||||
private Duration writeTimeout = Duration.ofSeconds(10);
|
||||
|
||||
/** 是否在连接失败时重试 */
|
||||
private boolean retryOnConnectionFailure = true;
|
||||
|
||||
/** 连接池最大空闲连接数 */
|
||||
private int maxIdleConnections = 64;
|
||||
|
||||
/** 连接池 keep-alive 时长 */
|
||||
private Duration keepAliveDuration = Duration.ofMinutes(5);
|
||||
|
||||
/** Feign 日志级别:NONE / BASIC / HEADERS / FULL */
|
||||
private feign.Logger.Level logLevel = feign.Logger.Level.NONE;
|
||||
|
||||
/** 启动时校验 Feign 契约与本地 Controller 一致 */
|
||||
private boolean contractValidate = true;
|
||||
|
||||
public Duration getConnectTimeout() { return connectTimeout; }
|
||||
public void setConnectTimeout(Duration connectTimeout) { this.connectTimeout = connectTimeout; }
|
||||
public Duration getReadTimeout() { return readTimeout; }
|
||||
public void setReadTimeout(Duration readTimeout) { this.readTimeout = readTimeout; }
|
||||
public Duration getWriteTimeout() { return writeTimeout; }
|
||||
public void setWriteTimeout(Duration writeTimeout) { this.writeTimeout = writeTimeout; }
|
||||
public boolean isRetryOnConnectionFailure() { return retryOnConnectionFailure; }
|
||||
public void setRetryOnConnectionFailure(boolean retryOnConnectionFailure) {
|
||||
this.retryOnConnectionFailure = retryOnConnectionFailure;
|
||||
}
|
||||
public int getMaxIdleConnections() { return maxIdleConnections; }
|
||||
public void setMaxIdleConnections(int maxIdleConnections) { this.maxIdleConnections = maxIdleConnections; }
|
||||
public Duration getKeepAliveDuration() { return keepAliveDuration; }
|
||||
public void setKeepAliveDuration(Duration keepAliveDuration) { this.keepAliveDuration = keepAliveDuration; }
|
||||
public feign.Logger.Level getLogLevel() { return logLevel; }
|
||||
public void setLogLevel(feign.Logger.Level logLevel) { this.logLevel = logLevel; }
|
||||
public boolean isContractValidate() { return contractValidate; }
|
||||
public void setContractValidate(boolean contractValidate) { this.contractValidate = contractValidate; }
|
||||
}
|
||||
@@ -0,0 +1,203 @@
|
||||
package com.k12study.common.feign.contract;
|
||||
|
||||
import java.lang.annotation.Annotation;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.BeansException;
|
||||
import org.springframework.beans.factory.ObjectProvider;
|
||||
import org.springframework.cloud.openfeign.FeignClient;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.context.ApplicationContextAware;
|
||||
import org.springframework.context.ApplicationListener;
|
||||
import org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider;
|
||||
import org.springframework.context.event.ContextRefreshedEvent;
|
||||
import org.springframework.core.annotation.AnnotationUtils;
|
||||
import org.springframework.core.type.classreading.MetadataReader;
|
||||
import org.springframework.core.type.classreading.MetadataReaderFactory;
|
||||
import org.springframework.core.type.filter.AnnotationTypeFilter;
|
||||
import org.springframework.core.type.filter.TypeFilter;
|
||||
import org.springframework.web.bind.annotation.DeleteMapping;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PatchMapping;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.PutMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMethod;
|
||||
import org.springframework.web.method.HandlerMethod;
|
||||
import org.springframework.web.servlet.mvc.method.RequestMappingInfo;
|
||||
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
|
||||
|
||||
/**
|
||||
* @description 启动期 Feign 契约自检;扫描 @FeignClient 接口路径与本地 RequestMappingHandlerMapping 注册的 Controller 路径,不一致则告警
|
||||
* @filename FeignContractValidator.java
|
||||
* @author wangys
|
||||
* @copyright xyzh
|
||||
* @since 2026-04-17
|
||||
*/
|
||||
public class FeignContractValidator
|
||||
implements ApplicationListener<ContextRefreshedEvent>, ApplicationContextAware {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(FeignContractValidator.class);
|
||||
|
||||
private final String basePackage;
|
||||
private ApplicationContext applicationContext;
|
||||
|
||||
public FeignContractValidator(String basePackage) {
|
||||
this.basePackage = basePackage;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
|
||||
this.applicationContext = applicationContext;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onApplicationEvent(ContextRefreshedEvent event) {
|
||||
ObjectProvider<RequestMappingHandlerMapping> provider =
|
||||
applicationContext.getBeanProvider(RequestMappingHandlerMapping.class);
|
||||
RequestMappingHandlerMapping handlerMapping = provider.getIfAvailable();
|
||||
if (handlerMapping == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
Set<Endpoint> mvcEndpoints = collectMvcEndpoints(handlerMapping);
|
||||
if (mvcEndpoints.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
Set<Class<?>> feignInterfaces = scanFeignInterfaces(basePackage);
|
||||
if (feignInterfaces.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
int totalChecked = 0;
|
||||
int mismatchCount = 0;
|
||||
for (Class<?> feignInterface : feignInterfaces) {
|
||||
FeignClient feignClient = AnnotationUtils.findAnnotation(feignInterface, FeignClient.class);
|
||||
String basePath = feignClient == null ? "" : trimSlashes(feignClient.path());
|
||||
for (Method method : feignInterface.getDeclaredMethods()) {
|
||||
Endpoint expected = resolveEndpoint(method, basePath);
|
||||
if (expected == null) {
|
||||
continue;
|
||||
}
|
||||
totalChecked++;
|
||||
if (!mvcEndpoints.contains(expected)) {
|
||||
mismatchCount++;
|
||||
log.warn("[feign-contract] mismatch: {}#{}() expects {} {} but no matching @RequestMapping found",
|
||||
feignInterface.getSimpleName(), method.getName(),
|
||||
expected.method(), expected.path());
|
||||
}
|
||||
}
|
||||
}
|
||||
if (mismatchCount > 0) {
|
||||
log.warn("[feign-contract] validation finished: {} method(s) checked, {} mismatch(es)",
|
||||
totalChecked, mismatchCount);
|
||||
} else {
|
||||
log.info("[feign-contract] validation passed: {} method(s) aligned with local controllers",
|
||||
totalChecked);
|
||||
}
|
||||
}
|
||||
|
||||
private Set<Endpoint> collectMvcEndpoints(RequestMappingHandlerMapping handlerMapping) {
|
||||
Set<Endpoint> endpoints = new LinkedHashSet<>();
|
||||
for (Map.Entry<RequestMappingInfo, HandlerMethod> entry : handlerMapping.getHandlerMethods().entrySet()) {
|
||||
RequestMappingInfo info = entry.getKey();
|
||||
Set<RequestMethod> methods = info.getMethodsCondition().getMethods();
|
||||
Set<String> patterns = info.getPathPatternsCondition() != null
|
||||
? info.getPathPatternsCondition().getPatternValues()
|
||||
: Set.of();
|
||||
if (methods.isEmpty() || patterns.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
for (RequestMethod m : methods) {
|
||||
for (String p : patterns) {
|
||||
endpoints.add(new Endpoint(m.name(), normalize(p)));
|
||||
}
|
||||
}
|
||||
}
|
||||
return endpoints;
|
||||
}
|
||||
|
||||
private Set<Class<?>> scanFeignInterfaces(String basePackage) {
|
||||
ClassPathScanningCandidateComponentProvider scanner =
|
||||
new ClassPathScanningCandidateComponentProvider(false) {
|
||||
@Override
|
||||
protected boolean isCandidateComponent(
|
||||
org.springframework.beans.factory.annotation.AnnotatedBeanDefinition beanDefinition) {
|
||||
return beanDefinition.getMetadata().isInterface();
|
||||
}
|
||||
};
|
||||
TypeFilter filter = new AnnotationTypeFilter(FeignClient.class) {
|
||||
@Override
|
||||
protected boolean matchClassName(String className) {
|
||||
return false;
|
||||
}
|
||||
@Override
|
||||
public boolean match(MetadataReader metadataReader, MetadataReaderFactory factory) {
|
||||
return metadataReader.getAnnotationMetadata().hasAnnotation(FeignClient.class.getName());
|
||||
}
|
||||
};
|
||||
scanner.addIncludeFilter(filter);
|
||||
Set<Class<?>> classes = new LinkedHashSet<>();
|
||||
scanner.findCandidateComponents(basePackage).forEach(beanDef -> {
|
||||
try {
|
||||
classes.add(Class.forName(beanDef.getBeanClassName()));
|
||||
} catch (ClassNotFoundException ignore) {
|
||||
// skip
|
||||
}
|
||||
});
|
||||
return classes;
|
||||
}
|
||||
|
||||
private Endpoint resolveEndpoint(Method method, String basePath) {
|
||||
for (Annotation annotation : method.getAnnotations()) {
|
||||
HttpDescriptor descriptor = describe(annotation);
|
||||
if (descriptor != null) {
|
||||
String path = normalize("/" + basePath + "/" + trimSlashes(descriptor.path()));
|
||||
return new Endpoint(descriptor.method(), path);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private HttpDescriptor describe(Annotation annotation) {
|
||||
if (annotation instanceof GetMapping a) return new HttpDescriptor("GET", firstPath(a.value(), a.path()));
|
||||
if (annotation instanceof PostMapping a) return new HttpDescriptor("POST", firstPath(a.value(), a.path()));
|
||||
if (annotation instanceof PutMapping a) return new HttpDescriptor("PUT", firstPath(a.value(), a.path()));
|
||||
if (annotation instanceof DeleteMapping a) return new HttpDescriptor("DELETE", firstPath(a.value(), a.path()));
|
||||
if (annotation instanceof PatchMapping a) return new HttpDescriptor("PATCH", firstPath(a.value(), a.path()));
|
||||
if (annotation instanceof RequestMapping a && a.method().length > 0) {
|
||||
return new HttpDescriptor(a.method()[0].name(), firstPath(a.value(), a.path()));
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private String firstPath(String[] values, String[] paths) {
|
||||
if (values != null && values.length > 0) return values[0];
|
||||
if (paths != null && paths.length > 0) return paths[0];
|
||||
return "";
|
||||
}
|
||||
|
||||
private String trimSlashes(String s) {
|
||||
if (s == null || s.isEmpty()) return "";
|
||||
String result = s;
|
||||
while (result.startsWith("/")) result = result.substring(1);
|
||||
while (result.endsWith("/")) result = result.substring(0, result.length() - 1);
|
||||
return result;
|
||||
}
|
||||
|
||||
private String normalize(String path) {
|
||||
String n = path.replaceAll("/+", "/");
|
||||
if (n.length() > 1 && n.endsWith("/")) {
|
||||
n = n.substring(0, n.length() - 1);
|
||||
}
|
||||
return n;
|
||||
}
|
||||
|
||||
private record Endpoint(String method, String path) {}
|
||||
private record HttpDescriptor(String method, String path) {}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package com.k12study.common.feign.interceptor;
|
||||
|
||||
import feign.RequestInterceptor;
|
||||
import feign.RequestTemplate;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.web.context.request.RequestContextHolder;
|
||||
import org.springframework.web.context.request.ServletRequestAttributes;
|
||||
|
||||
/**
|
||||
* @description Feign 请求拦截器:从当前 Servlet 请求提取 Authorization 并透传下游,消费方调用时无需显式传递 token
|
||||
* @filename FeignAuthRelayInterceptor.java
|
||||
* @author wangys
|
||||
* @copyright xyzh
|
||||
* @since 2026-04-17
|
||||
*/
|
||||
public class FeignAuthRelayInterceptor implements RequestInterceptor {
|
||||
|
||||
@Override
|
||||
public void apply(RequestTemplate template) {
|
||||
// 调用方如已显式 setHeader("Authorization"),保留调用方意图
|
||||
if (template.headers().containsKey(HttpHeaders.AUTHORIZATION)) {
|
||||
return;
|
||||
}
|
||||
ServletRequestAttributes attributes =
|
||||
(ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
|
||||
// 非 HTTP 请求上下文(定时任务/MQ 消费)下不做任何注入
|
||||
if (attributes == null) {
|
||||
return;
|
||||
}
|
||||
HttpServletRequest request = attributes.getRequest();
|
||||
String authorization = request.getHeader(HttpHeaders.AUTHORIZATION);
|
||||
if (authorization != null && !authorization.isBlank()) {
|
||||
template.header(HttpHeaders.AUTHORIZATION, authorization);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
com.k12study.common.feign.config.FeignAutoConfiguration
|
||||
@@ -1,10 +1,17 @@
|
||||
package com.k12study.common.security.context;
|
||||
import java.util.List;
|
||||
|
||||
public record RequestUserContext(
|
||||
String userId,
|
||||
String username,
|
||||
String displayName,
|
||||
String adcode,
|
||||
String tenantId,
|
||||
String deptId
|
||||
String tenantPath,
|
||||
String deptId,
|
||||
String deptPath,
|
||||
List<String> roleCodes,
|
||||
String clientType,
|
||||
String sessionId
|
||||
) {
|
||||
}
|
||||
|
||||
@@ -5,8 +5,10 @@ import io.jsonwebtoken.Claims;
|
||||
import io.jsonwebtoken.Jwts;
|
||||
import io.jsonwebtoken.security.Keys;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import javax.crypto.SecretKey;
|
||||
|
||||
public class JwtTokenProvider {
|
||||
@@ -19,15 +21,29 @@ public class JwtTokenProvider {
|
||||
}
|
||||
|
||||
public String createAccessToken(JwtUserPrincipal principal) {
|
||||
return createToken(principal, authProperties.getAccessTokenTtl());
|
||||
}
|
||||
|
||||
public String createRefreshToken(JwtUserPrincipal principal) {
|
||||
return createToken(principal, authProperties.getRefreshTokenTtl());
|
||||
}
|
||||
|
||||
private String createToken(JwtUserPrincipal principal, Duration ttl) {
|
||||
Instant now = Instant.now();
|
||||
return Jwts.builder()
|
||||
.subject(principal.userId())
|
||||
.claim("username", principal.username())
|
||||
.claim("displayName", principal.displayName())
|
||||
.claim("adcode", principal.adcode())
|
||||
.claim("tenantId", principal.tenantId())
|
||||
.claim("tenantPath", principal.tenantPath())
|
||||
.claim("deptId", principal.deptId())
|
||||
.claim("deptPath", principal.deptPath())
|
||||
.claim("roleCodes", principal.roleCodes())
|
||||
.claim("clientType", principal.clientType())
|
||||
.claim("sessionId", principal.sessionId())
|
||||
.issuedAt(Date.from(now))
|
||||
.expiration(Date.from(now.plus(authProperties.getAccessTokenTtl())))
|
||||
.expiration(Date.from(now.plus(ttl)))
|
||||
.signWith(secretKey)
|
||||
.compact();
|
||||
}
|
||||
@@ -38,12 +54,23 @@ public class JwtTokenProvider {
|
||||
.build()
|
||||
.parseSignedClaims(token)
|
||||
.getPayload();
|
||||
@SuppressWarnings("unchecked")
|
||||
List<String> roleCodes = claims.get("roleCodes", List.class);
|
||||
if (roleCodes == null) {
|
||||
roleCodes = List.of();
|
||||
}
|
||||
return new JwtUserPrincipal(
|
||||
claims.getSubject(),
|
||||
claims.get("username", String.class),
|
||||
claims.get("displayName", String.class),
|
||||
claims.get("adcode", String.class),
|
||||
claims.get("tenantId", String.class),
|
||||
claims.get("deptId", String.class)
|
||||
claims.get("tenantPath", String.class),
|
||||
claims.get("deptId", String.class),
|
||||
claims.get("deptPath", String.class),
|
||||
roleCodes,
|
||||
claims.get("clientType", String.class),
|
||||
claims.get("sessionId", String.class)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,17 @@
|
||||
package com.k12study.common.security.jwt;
|
||||
import java.util.List;
|
||||
|
||||
public record JwtUserPrincipal(
|
||||
String userId,
|
||||
String username,
|
||||
String displayName,
|
||||
String adcode,
|
||||
String tenantId,
|
||||
String deptId
|
||||
String tenantPath,
|
||||
String deptId,
|
||||
String deptPath,
|
||||
List<String> roleCodes,
|
||||
String clientType,
|
||||
String sessionId
|
||||
) {
|
||||
}
|
||||
|
||||
@@ -9,6 +9,8 @@ import jakarta.servlet.ServletException;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import java.io.IOException;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.web.filter.OncePerRequestFilter;
|
||||
|
||||
@@ -27,8 +29,14 @@ public class CommonWebMvcConfiguration extends OncePerRequestFilter {
|
||||
request.getHeader(SecurityConstants.HEADER_USER_ID),
|
||||
request.getHeader(SecurityConstants.HEADER_USERNAME),
|
||||
request.getHeader(SecurityConstants.HEADER_DISPLAY_NAME),
|
||||
request.getHeader(SecurityConstants.HEADER_ADCODE),
|
||||
request.getHeader(SecurityConstants.HEADER_TENANT_ID),
|
||||
request.getHeader(SecurityConstants.HEADER_DEPT_ID)
|
||||
request.getHeader(SecurityConstants.HEADER_TENANT_PATH),
|
||||
request.getHeader(SecurityConstants.HEADER_DEPT_ID),
|
||||
request.getHeader(SecurityConstants.HEADER_DEPT_PATH),
|
||||
parseRoleCodes(request.getHeader(SecurityConstants.HEADER_ROLE_CODES)),
|
||||
request.getHeader(SecurityConstants.HEADER_CLIENT_TYPE),
|
||||
request.getHeader(SecurityConstants.HEADER_SESSION_ID)
|
||||
));
|
||||
response.setHeader(SecurityConstants.TRACE_ID, TraceIdHolder.getOrCreate());
|
||||
filterChain.doFilter(request, response);
|
||||
@@ -37,4 +45,14 @@ public class CommonWebMvcConfiguration extends OncePerRequestFilter {
|
||||
RequestUserContextHolder.clear();
|
||||
}
|
||||
}
|
||||
|
||||
private List<String> parseRoleCodes(String roleCodesHeader) {
|
||||
if (roleCodesHeader == null || roleCodesHeader.isBlank()) {
|
||||
return List.of();
|
||||
}
|
||||
return Arrays.stream(roleCodesHeader.split(","))
|
||||
.map(String::trim)
|
||||
.filter(code -> !code.isBlank())
|
||||
.toList();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,5 +20,6 @@
|
||||
<module>common-security</module>
|
||||
<module>common-mybatis</module>
|
||||
<module>common-redis</module>
|
||||
<module>common-feign</module>
|
||||
</modules>
|
||||
</project>
|
||||
|
||||
Reference in New Issue
Block a user