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