diff --git a/pom.xml b/pom.xml
index 2b3a6d4..b42c78f 100644
--- a/pom.xml
+++ b/pom.xml
@@ -56,6 +56,19 @@
${spring.boot.version}
+
+
+ org.springframework.security
+ spring-security-oauth2-authorization-server
+ 0.4.0
+
+
+
+ org.springframework.security
+ spring-security-oauth2-jose
+ 5.7.11
+
+
org.projectlombok
diff --git a/xs-server/src/main/resources/keys/rsa-private.pem b/xs-server/src/main/resources/keys/rsa-private.pem
new file mode 100644
index 0000000..d132742
--- /dev/null
+++ b/xs-server/src/main/resources/keys/rsa-private.pem
@@ -0,0 +1,28 @@
+-----BEGIN PRIVATE KEY-----
+MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQD7JdqQe8W8byYT
+KuPK7STLYO5TTe5uWhHj1SLq7Xoj1y0agO+x1X3/dUA1X/k5Dd2rmkmIU2VoB/9c
+aoE5naXUhVfE9wDsl5AKdWRCn1PKvuofd/0kuGFS44tNQcLGtDD7e8CQQJZJPZVS
+Z8F2nlHjYSTUPVvi77hKG21qoZLEa0F+TYfqgOohabQtxeBAnyRW50/g281Bqd3w
+ntfM0aKfW9fsFhJnk+AlXWaqxQKkhtzMmKVeqLNfJnXvhgUA6QbnzHPtPxF+fLEx
+BNnBEPF8CsQFVdt1TtsQlfCzVMTMibZU2CcwtqHUP1maEcH5zV5whPfgkahejiNm
+L08Jw74VAgMBAAECggEACJHb0pGHh4IkF/XmM6Qs6xMFYmqmvRkf0JjoTYDl9+JW
+Y9RMYKfqy+Yius+GSNRjPWS7p38MHkiysGL/F7uYyCwvhwU3x+kugM2+/+gWqyaT
+WnwYgZ4YXIRu2ieFr4xq1symnzO14nDny4uqB9PEZFd7wS4I0ZShe5yLM6Yai88V
+0Jm6Hi9RcC5efiE4tWismBKaP13WXamAVySROW30lEaMgyI66DNYgs+RHiZgAFP7
+u7raUD07xrk6eV6YnG/9EvS/oqV+IPEacY+bP3ZUqvPMI50tLTEVN1yJXrr4T3kS
+W1TiApaL87rdDCYem7rtIury+JcSadaI6lwP5OhmLwKBgQD/4mSrFi30IzD6LFam
+N7CYBxSleWgha2fHuycDGNGcXDCn3y4JuqTMRpV1c9F5emPPv1E3ELj4plz/NnVr
+67SkCs3nqoSRjyXJKhN/kJqM/NB+Ic1UgXeI+wGMkfkUHrQ6T6SghYVxWW+hQKm8
+IeV7aVJiM01/Ze938cnJuWd17wKBgQD7QumZzRTMkmnmSFCpOhs2Y3B8JYrJsJXY
+PeYkxea/7brDyuIdWKt0kl9EvpsrIzTe0t4LYV3Vmmfh15PNZp4PEr30NEBxeVOO
+HoglNfyJgP/nvhOGYesNhqPlK96/ajEvu7FpFHwDje5RKRWxCK5qhZneDy0ppjWb
+6seshN1wOwKBgQDqNPxxP/bFu6Qrh4Oz1cs0C18RakMuO5Gc1acKhZ/tntAGBxer
+XgNS2dQY0e5MYwKSdwlN/mdfZ149Vko5gl8vupfmUEPQuxYZvwJjwyZCn2/x0tyO
+WYXggeZUFJPHn6bUrGsBZdTS/8pV7Mqu4NOblrYKHez0C4gY390TXzjcTwKBgQDR
+mD6fYrjf8Z7PTzGiCOucUhUKKpL8rgZBbVknAcL8BYZPP1Whn07fHh7EjK+Jq4O2
+AHbjTWRmA7h2Z0tPAzQEZOD57gB35/pwSj3NtJwl4+sU2LUW22WlUdQ0HoVgbWf8
+ZniWrFTK7kGHiFsk45YDG9F/sG8/F/wORSotWmQR8wKBgQDqrlyvMCwiJHW7vOPs
+ih+utzIvtZ8D4fxzEFluTUqubAAl3N+81NuRJuEFIJLjIAeTNOHj1IdlPj5oe0aa
+IYOzoB2+xJxnLPbvI1tTat/pqgXxPY24/9c9rBoTCsJboTPb0fMh1nHxTZvny4tB
+jP7d5EBvIMWnCEuTo4y39ZFsMA==
+-----END PRIVATE KEY-----
diff --git a/xs-server/src/main/resources/keys/rsa-public.pem b/xs-server/src/main/resources/keys/rsa-public.pem
new file mode 100644
index 0000000..bbf9565
--- /dev/null
+++ b/xs-server/src/main/resources/keys/rsa-public.pem
@@ -0,0 +1,9 @@
+-----BEGIN PUBLIC KEY-----
+MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA+yXakHvFvG8mEyrjyu0k
+y2DuU03ubloR49Ui6u16I9ctGoDvsdV9/3VANV/5OQ3dq5pJiFNlaAf/XGqBOZ2l
+1IVXxPcA7JeQCnVkQp9Tyr7qH3f9JLhhUuOLTUHCxrQw+3vAkECWST2VUmfBdp5R
+42Ek1D1b4u+4ShttaqGSxGtBfk2H6oDqIWm0LcXgQJ8kVudP4NvNQand8J7XzNGi
+n1vX7BYSZ5PgJV1mqsUCpIbczJilXqizXyZ174YFAOkG58xz7T8RfnyxMQTZwRDx
+fArEBVXbdU7bEJXws1TEzIm2VNgnMLah1D9ZmhHB+c1ecIT34JGoXo4jZi9PCcO+
+FQIDAQAB
+-----END PUBLIC KEY-----
diff --git a/xs-service/src/main/java/com/xiang/xservice/gateway/service/config/GatewaySecurityConfig.java b/xs-service/src/main/java/com/xiang/xservice/gateway/service/config/GatewaySecurityConfig.java
index 41a2a8d..573c08f 100644
--- a/xs-service/src/main/java/com/xiang/xservice/gateway/service/config/GatewaySecurityConfig.java
+++ b/xs-service/src/main/java/com/xiang/xservice/gateway/service/config/GatewaySecurityConfig.java
@@ -1,24 +1,24 @@
-//package com.xiang.xservice.gateway.service.config;
-//
-//import org.springframework.context.annotation.Bean;
-//import org.springframework.context.annotation.Configuration;
-//import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity;
-//import org.springframework.security.config.web.server.ServerHttpSecurity;
-//import org.springframework.security.web.server.SecurityWebFilterChain;
-//
-//@Configuration
-//@EnableWebFluxSecurity
-//public class GatewaySecurityConfig {
-//
-// @Bean
-// public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
-// http
-// .authorizeExchange(exchanges -> exchanges
-// // ✅ 网关全放行
-// .anyExchange().permitAll()
-// )
-// .csrf(ServerHttpSecurity.CsrfSpec::disable); // 禁用 CSRF
-//
-// return http.build();
-// }
-//}
\ No newline at end of file
+package com.xiang.xservice.gateway.service.config;
+
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity;
+import org.springframework.security.config.web.server.ServerHttpSecurity;
+import org.springframework.security.web.server.SecurityWebFilterChain;
+
+@Configuration
+@EnableWebFluxSecurity
+public class GatewaySecurityConfig {
+
+ @Bean
+ public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
+ http
+ .authorizeExchange(exchanges -> exchanges
+ // ✅ 网关全放行
+ .anyExchange().permitAll()
+ )
+ .csrf(ServerHttpSecurity.CsrfSpec::disable); // 禁用 CSRF
+
+ return http.build();
+ }
+}
\ No newline at end of file
diff --git a/xs-service/src/main/java/com/xiang/xservice/gateway/service/config/JwtConfig.java b/xs-service/src/main/java/com/xiang/xservice/gateway/service/config/JwtConfig.java
new file mode 100644
index 0000000..04c3dc7
--- /dev/null
+++ b/xs-service/src/main/java/com/xiang/xservice/gateway/service/config/JwtConfig.java
@@ -0,0 +1,71 @@
+package com.xiang.xservice.gateway.service.config;
+
+import com.nimbusds.jose.jwk.JWKSet;
+import com.nimbusds.jose.jwk.RSAKey;
+import com.nimbusds.jose.jwk.source.JWKSource;
+import com.nimbusds.jose.proc.SecurityContext;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.core.io.ClassPathResource;
+import org.springframework.security.oauth2.jwt.JwtDecoder;
+import org.springframework.security.oauth2.jwt.JwtEncoder;
+import org.springframework.security.oauth2.jwt.NimbusJwtEncoder;
+import org.springframework.security.oauth2.server.authorization.config.annotation.web.configuration.OAuth2AuthorizationServerConfiguration;
+
+import java.io.InputStream;
+import java.nio.charset.StandardCharsets;
+import java.security.KeyFactory;
+import java.security.interfaces.RSAPrivateKey;
+import java.security.interfaces.RSAPublicKey;
+import java.security.spec.PKCS8EncodedKeySpec;
+import java.security.spec.X509EncodedKeySpec;
+import java.util.Base64;
+
+@Configuration
+public class JwtConfig {
+
+ private RSAPrivateKey loadPrivateKey(String classpath) throws Exception {
+ InputStream is = new ClassPathResource(classpath).getInputStream();
+ String key = new String(is.readAllBytes(), StandardCharsets.UTF_8)
+ .replace("-----BEGIN PRIVATE KEY-----", "")
+ .replace("-----END PRIVATE KEY-----", "")
+ .replaceAll("\\s+", "");
+ PKCS8EncodedKeySpec spec = new PKCS8EncodedKeySpec(Base64.getDecoder().decode(key));
+ return (RSAPrivateKey) KeyFactory.getInstance("RSA").generatePrivate(spec);
+ }
+
+ private RSAPublicKey loadPublicKey(String classpath) throws Exception {
+ InputStream is = new ClassPathResource(classpath).getInputStream();
+ String key = new String(is.readAllBytes(), StandardCharsets.UTF_8)
+ .replace("-----BEGIN PUBLIC KEY-----", "")
+ .replace("-----END PUBLIC KEY-----", "")
+ .replaceAll("\\s+", "");
+ X509EncodedKeySpec spec = new X509EncodedKeySpec(Base64.getDecoder().decode(key));
+ return (RSAPublicKey) KeyFactory.getInstance("RSA").generatePublic(spec);
+ }
+
+ @Bean
+ public JWKSource jwkSource() throws Exception {
+ // 使用RSA对称加密进行加密
+ RSAPublicKey publicKey = loadPublicKey("keys/rsa-public.pem");
+ RSAPrivateKey privateKey = loadPrivateKey("keys/rsa-private.pem");
+
+ RSAKey rsaKey = new RSAKey.Builder(publicKey)
+ .privateKey(privateKey)
+ .keyID("xservice")
+ .build();
+
+ JWKSet jwkSet = new JWKSet(rsaKey);
+ return (jwkSelector, ctx) -> jwkSelector.select(jwkSet);
+ }
+
+ @Bean
+ public JwtEncoder jwtEncoder(JWKSource jwkSource) {
+ return new NimbusJwtEncoder(jwkSource);
+ }
+
+ @Bean
+ public JwtDecoder jwtDecoder(JWKSource jwkSource) {
+ return OAuth2AuthorizationServerConfiguration.jwtDecoder(jwkSource);
+ }
+}
diff --git a/xs-service/src/main/java/com/xiang/xservice/gateway/service/core/AuthGlobalFilter.java b/xs-service/src/main/java/com/xiang/xservice/gateway/service/core/AuthGlobalFilter.java
index 73a09db..747fbb2 100644
--- a/xs-service/src/main/java/com/xiang/xservice/gateway/service/core/AuthGlobalFilter.java
+++ b/xs-service/src/main/java/com/xiang/xservice/gateway/service/core/AuthGlobalFilter.java
@@ -1,20 +1,31 @@
package com.xiang.xservice.gateway.service.core;
+import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
+import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.reactive.ServerHttpRequest;
+import org.springframework.security.oauth2.jwt.Jwt;
+import org.springframework.security.oauth2.jwt.JwtDecoder;
+import org.springframework.security.oauth2.jwt.JwtException;
+import org.springframework.security.oauth2.jwt.JwtValidationException;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
+import java.util.UUID;
+
@Slf4j
@Component
+@RequiredArgsConstructor
public class AuthGlobalFilter implements GlobalFilter, Ordered {
+ private final JwtDecoder jwtDecoder;
+
@Override
public Mono filter(ServerWebExchange exchange, GatewayFilterChain chain) {
ServerHttpRequest request = exchange.getRequest();
@@ -34,8 +45,42 @@ public class AuthGlobalFilter implements GlobalFilter, Ordered {
return exchange.getResponse().setComplete();
}
+ // 3. 本地验签 + 解析 claims
+ Jwt jwt;
+ try {
+ jwt = jwtDecoder.decode(token);
+ } catch (JwtValidationException e) {
+ boolean isExpired = e.getErrors().stream()
+ .anyMatch(err -> err.getDescription().contains("expired"));
+
+ exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
+ if (isExpired) {
+ log.warn("Token 已过期: path={}", path);
+ return exchange.getResponse().setComplete();
+ }
+ log.warn("Token 校验失败: path={}, reason={}", path, e.getMessage());
+ return exchange.getResponse().setComplete();
+ } catch (JwtException e) {
+ log.error("Token解析异常!path:{}", path);
+ exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
+ return exchange.getResponse().setComplete();
+ }
+ Long userId = (Long) jwt.getClaim("userId");
+ Long tenantId = (Long) jwt.getClaim("tenantId");
+ String username = (String) jwt.getClaim("username");
+ String traceId = UUID.randomUUID().toString();
+ log.info("Token解析结果:userId:{}, tenantId:{}, username:{}", userId, tenantId, username);
+ ServerHttpRequest mutatedRequest = exchange.getRequest()
+ .mutate()
+ .header("X-User-Id", String.valueOf(userId))
+ .header("X-Trace-Id", traceId)
+ .header("X-Tenant-Id", String.valueOf(tenantId))
+ .header("X-Username", username)
+ .headers(h -> h.remove(HttpHeaders.AUTHORIZATION)) // JWT 止步于此
+ .build();
+
log.info("✅ Token 校验通过: {}", token);
- return chain.filter(exchange);
+ return chain.filter(exchange.mutate().request(mutatedRequest).build());
}
@Override