fix: restore shared HS256 authentication

This commit is contained in:
2026-08-13 17:58:26 +08:00
parent 9eaaa9a0a0
commit f779ab56dd
7 changed files with 10 additions and 51 deletions
@@ -1,8 +1,6 @@
package com.baiyue.auth.config; package com.baiyue.auth.config;
import com.nimbusds.jose.jwk.JWKSet; import com.nimbusds.jose.jwk.source.ImmutableSecret;
import com.nimbusds.jose.jwk.RSAKey;
import com.nimbusds.jose.jwk.source.ImmutableJWKSet;
import com.baiyue.common.security.JwtAuthenticationFilter; import com.baiyue.common.security.JwtAuthenticationFilter;
import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
@@ -33,7 +31,6 @@ public class AuthSecurityConfig {
"/api/auth/refresh", "/api/auth/refresh",
"/api/auth/logout", "/api/auth/logout",
"/api/auth/impersonations/exchange", "/api/auth/impersonations/exchange",
"/api/auth/.well-known/jwks.json",
"/actuator/health", "/actuator/health",
"/v3/api-docs/**", "/v3/api-docs/**",
"/swagger-ui/**", "/swagger-ui/**",
@@ -45,19 +42,9 @@ public class AuthSecurityConfig {
@Bean @Bean
JwtEncoder jwtEncoder(AuthSecurityProperties properties) { JwtEncoder jwtEncoder(AuthSecurityProperties properties) {
try { byte[] secret = properties.jwtSecret().getBytes(StandardCharsets.UTF_8);
JWKSet set = JWKSet.parse(properties.rsaJwkSet()); if (secret.length < 32) throw new IllegalStateException("JWT_SECRET 至少需要 32 字节");
RSAKey current = set.getKeyByKeyId(properties.currentKid()) instanceof RSAKey rsa ? rsa : null; return new NimbusJwtEncoder(new ImmutableSecret<>(secret));
if (current == null || !current.isPrivate()) {
throw new IllegalStateException("current-kid 必须指向一把包含私钥的 RSA 签名密钥");
}
if (set.getKeys().stream().anyMatch(jwk -> !(jwk instanceof RSAKey))) {
throw new IllegalStateException("RSA JWK Set 只能包含 RSA 密钥");
}
return new NimbusJwtEncoder(new ImmutableJWKSet<>(set));
} catch (java.text.ParseException exception) {
throw new IllegalStateException("RSA JWK Set配置无效", exception);
}
} }
@Bean @Bean
@@ -9,7 +9,6 @@ import java.time.Duration;
*/ */
@ConfigurationProperties(prefix = "baiyue.security") @ConfigurationProperties(prefix = "baiyue.security")
public record AuthSecurityProperties(String jwtSecret, String issuer, String audience, public record AuthSecurityProperties(String jwtSecret, String issuer, String audience,
String rsaJwkSet, String currentKid,
Duration accessTokenTtl, Duration refreshTokenTtl, Duration accessTokenTtl, Duration refreshTokenTtl,
int maxLoginFailures, Duration lockDuration) { int maxLoginFailures, Duration lockDuration) {
} }
@@ -1,26 +0,0 @@
package com.baiyue.auth.controller;
import com.baiyue.auth.config.AuthSecurityProperties;
import com.nimbusds.jose.jwk.JWKSet;
import org.springframework.http.CacheControl;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.Map;
import java.util.concurrent.TimeUnit;
/** 仅发布 RSA 公钥;轮换期配置当前与上一把公钥。 */
@RestController
public class JwksController {
private final AuthSecurityProperties properties;
public JwksController(AuthSecurityProperties properties) { this.properties = properties; }
@GetMapping("/api/auth/.well-known/jwks.json")
public ResponseEntity<Map<String, Object>> jwks() throws Exception {
JWKSet publicSet = JWKSet.parse(properties.rsaJwkSet()).toPublicJWKSet();
return ResponseEntity.ok()
.cacheControl(CacheControl.maxAge(5, TimeUnit.MINUTES).cachePublic())
.body(publicSet.toJSONObject());
}
}
@@ -16,7 +16,7 @@ import com.baiyue.common.core.exception.BizException;
import jakarta.annotation.Resource; import jakarta.annotation.Resource;
import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm; import org.springframework.security.oauth2.jose.jws.MacAlgorithm;
import org.springframework.security.oauth2.jwt.JwtClaimsSet; import org.springframework.security.oauth2.jwt.JwtClaimsSet;
import org.springframework.security.oauth2.jwt.JwtEncoder; import org.springframework.security.oauth2.jwt.JwtEncoder;
import org.springframework.security.oauth2.jwt.JwtEncoderParameters; import org.springframework.security.oauth2.jwt.JwtEncoderParameters;
@@ -198,7 +198,7 @@ public class AuthService {
public record ImpersonationTokenResponse(String tokenType, String accessToken, long expiresIn) { } public record ImpersonationTokenResponse(String tokenType, String accessToken, long expiresIn) { }
private JwsHeader signingHeader() { private JwsHeader signingHeader() {
return JwsHeader.with(SignatureAlgorithm.RS256).keyId(properties.currentKid()).build(); return JwsHeader.with(MacAlgorithm.HS256).build();
} }
private static BizException invalidCredentials() { private static BizException invalidCredentials() {
-2
View File
@@ -31,8 +31,6 @@ spring:
encode: UTF-8 encode: UTF-8
baiyue.security: baiyue.security:
jwt-secret: ${JWT_SECRET:change-me-to-at-least-32-bytes-long-before-start} jwt-secret: ${JWT_SECRET:change-me-to-at-least-32-bytes-long-before-start}
rsa-jwk-set: ${JWT_RSA_JWK_SET:}
current-kid: ${JWT_CURRENT_KID:}
internal-service-token: ${INTERNAL_SERVICE_TOKEN:change-me-to-an-internal-token-at-least-32-bytes-long} internal-service-token: ${INTERNAL_SERVICE_TOKEN:change-me-to-an-internal-token-at-least-32-bytes-long}
token-key-prefix: ${REDIS_TOKEN_KEY_PREFIX:baiyue:auth} token-key-prefix: ${REDIS_TOKEN_KEY_PREFIX:baiyue:auth}
issuer: ${JWT_ISSUER:auth-service} issuer: ${JWT_ISSUER:auth-service}
@@ -12,6 +12,7 @@ import org.springframework.boot.test.mock.mockito.MockBean;
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.crypto.password.PasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.oauth2.jose.jws.MacAlgorithm;
import org.springframework.security.oauth2.jwt.Jwt; import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.security.oauth2.jwt.JwtEncoder; import org.springframework.security.oauth2.jwt.JwtEncoder;
import org.springframework.security.oauth2.jwt.JwtEncoderParameters; import org.springframework.security.oauth2.jwt.JwtEncoderParameters;
@@ -39,7 +40,7 @@ class AuthServiceSmallJwtTest {
static class TestConfig { static class TestConfig {
@Bean @Bean
AuthSecurityProperties properties() { AuthSecurityProperties properties() {
return new AuthSecurityProperties("unused", "auth-service", "lastleg-api", "{}", "current-2026-08", return new AuthSecurityProperties("unused", "auth-service", "lastleg-api",
Duration.ofMinutes(30), Duration.ofMinutes(30),
Duration.ofDays(7), 5, Duration.ofMinutes(10)); Duration.ofDays(7), 5, Duration.ofMinutes(10));
} }
@@ -82,7 +83,8 @@ class AuthServiceSmallJwtTest {
.containsEntry("membership_id", 84L) .containsEntry("membership_id", 84L)
.doesNotContainKeys("permissions", "scope"); .doesNotContainKeys("permissions", "scope");
assertThat(jwt.getValue().getClaims().getAudience()).containsExactly("lastleg-api"); assertThat(jwt.getValue().getClaims().getAudience()).containsExactly("lastleg-api");
assertThat(jwt.getValue().getJwsHeader().getKeyId()).isEqualTo("current-2026-08"); assertThat(jwt.getValue().getJwsHeader().getAlgorithm()).isEqualTo(MacAlgorithm.HS256);
assertThat(jwt.getValue().getJwsHeader().getKeyId()).isNull();
verify(tokenSessionStore).store(any(), org.mockito.ArgumentMatchers.eq(42L), verify(tokenSessionStore).store(any(), org.mockito.ArgumentMatchers.eq(42L),
org.mockito.ArgumentMatchers.eq(Set.of("PLATFORM_ADMIN")), org.mockito.ArgumentMatchers.eq(Set.of("PLATFORM_ADMIN")),
org.mockito.ArgumentMatchers.eq(Set.of("platform:order:list")), any(), org.mockito.ArgumentMatchers.eq(Set.of("platform:order:list")), any(),
-1
View File
@@ -7,6 +7,5 @@ spring:
baiyue: baiyue:
security: security:
audience: ${JWT_AUDIENCE:lastleg-api} audience: ${JWT_AUDIENCE:lastleg-api}
current-kid: ${JWT_CURRENT_KID:}
access-token-ttl: ${ACCESS_TOKEN_TTL:15m} access-token-ttl: ${ACCESS_TOKEN_TTL:15m}
refresh-token-ttl: ${REFRESH_TOKEN_TTL:7d} refresh-token-ttl: ${REFRESH_TOKEN_TTL:7d}