feat:网关校验Token,并将token值存入header供下游服务使用

This commit is contained in:
xiang
2026-03-22 00:35:18 +08:00
parent d8c5c602a9
commit 78a26b2ed2
6 changed files with 191 additions and 25 deletions

13
pom.xml
View File

@@ -56,6 +56,19 @@
<version>${spring.boot.version}</version> <version>${spring.boot.version}</version>
</dependency> </dependency>
<!-- Authorization Server -->
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-oauth2-authorization-server</artifactId>
<version>0.4.0</version>
</dependency>
<!-- JWT (optional: for convenience) -->
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-oauth2-jose</artifactId>
<version>5.7.11</version>
</dependency>
<!-- Lombok可选简化开发 --> <!-- Lombok可选简化开发 -->
<dependency> <dependency>
<groupId>org.projectlombok</groupId> <groupId>org.projectlombok</groupId>

View File

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

View File

@@ -0,0 +1,9 @@
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA+yXakHvFvG8mEyrjyu0k
y2DuU03ubloR49Ui6u16I9ctGoDvsdV9/3VANV/5OQ3dq5pJiFNlaAf/XGqBOZ2l
1IVXxPcA7JeQCnVkQp9Tyr7qH3f9JLhhUuOLTUHCxrQw+3vAkECWST2VUmfBdp5R
42Ek1D1b4u+4ShttaqGSxGtBfk2H6oDqIWm0LcXgQJ8kVudP4NvNQand8J7XzNGi
n1vX7BYSZ5PgJV1mqsUCpIbczJilXqizXyZ174YFAOkG58xz7T8RfnyxMQTZwRDx
fArEBVXbdU7bEJXws1TEzIm2VNgnMLah1D9ZmhHB+c1ecIT34JGoXo4jZi9PCcO+
FQIDAQAB
-----END PUBLIC KEY-----

View File

@@ -1,24 +1,24 @@
//package com.xiang.xservice.gateway.service.config; package com.xiang.xservice.gateway.service.config;
//
//import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
//import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
//import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity; import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity;
//import org.springframework.security.config.web.server.ServerHttpSecurity; import org.springframework.security.config.web.server.ServerHttpSecurity;
//import org.springframework.security.web.server.SecurityWebFilterChain; import org.springframework.security.web.server.SecurityWebFilterChain;
//
//@Configuration @Configuration
//@EnableWebFluxSecurity @EnableWebFluxSecurity
//public class GatewaySecurityConfig { public class GatewaySecurityConfig {
//
// @Bean @Bean
// public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) { public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
// http http
// .authorizeExchange(exchanges -> exchanges .authorizeExchange(exchanges -> exchanges
// // ✅ 网关全放行 // ✅ 网关全放行
// .anyExchange().permitAll() .anyExchange().permitAll()
// ) )
// .csrf(ServerHttpSecurity.CsrfSpec::disable); // 禁用 CSRF .csrf(ServerHttpSecurity.CsrfSpec::disable); // 禁用 CSRF
//
// return http.build(); return http.build();
// } }
//} }

View File

@@ -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<SecurityContext> 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<SecurityContext> jwkSource) {
return new NimbusJwtEncoder(jwkSource);
}
@Bean
public JwtDecoder jwtDecoder(JWKSource<SecurityContext> jwkSource) {
return OAuth2AuthorizationServerConfiguration.jwtDecoder(jwkSource);
}
}

View File

@@ -1,20 +1,31 @@
package com.xiang.xservice.gateway.service.core; package com.xiang.xservice.gateway.service.core;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.gateway.filter.GatewayFilterChain; import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter; import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered; import org.springframework.core.Ordered;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
import org.springframework.http.server.reactive.ServerHttpRequest; 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.stereotype.Component;
import org.springframework.util.StringUtils; import org.springframework.util.StringUtils;
import org.springframework.web.server.ServerWebExchange; import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono; import reactor.core.publisher.Mono;
import java.util.UUID;
@Slf4j @Slf4j
@Component @Component
@RequiredArgsConstructor
public class AuthGlobalFilter implements GlobalFilter, Ordered { public class AuthGlobalFilter implements GlobalFilter, Ordered {
private final JwtDecoder jwtDecoder;
@Override @Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) { public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
ServerHttpRequest request = exchange.getRequest(); ServerHttpRequest request = exchange.getRequest();
@@ -34,8 +45,42 @@ public class AuthGlobalFilter implements GlobalFilter, Ordered {
return exchange.getResponse().setComplete(); 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); log.info("✅ Token 校验通过: {}", token);
return chain.filter(exchange); return chain.filter(exchange.mutate().request(mutatedRequest).build());
} }
@Override @Override