打印箱唛修改 尾程垮桥事件提交
This commit is contained in:
@@ -1,6 +1,8 @@
|
||||
package com.baiyue.auth.config;
|
||||
|
||||
import com.nimbusds.jose.jwk.source.ImmutableSecret;
|
||||
import com.nimbusds.jose.jwk.JWKSet;
|
||||
import com.nimbusds.jose.jwk.RSAKey;
|
||||
import com.nimbusds.jose.jwk.source.ImmutableJWKSet;
|
||||
import com.baiyue.common.security.JwtAuthenticationFilter;
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
@@ -31,6 +33,7 @@ public class AuthSecurityConfig {
|
||||
"/api/auth/refresh",
|
||||
"/api/auth/logout",
|
||||
"/api/auth/impersonations/exchange",
|
||||
"/api/auth/.well-known/jwks.json",
|
||||
"/actuator/health",
|
||||
"/v3/api-docs/**",
|
||||
"/swagger-ui/**",
|
||||
@@ -42,9 +45,19 @@ public class AuthSecurityConfig {
|
||||
|
||||
@Bean
|
||||
JwtEncoder jwtEncoder(AuthSecurityProperties properties) {
|
||||
byte[] secret = properties.jwtSecret().getBytes(StandardCharsets.UTF_8);
|
||||
if (secret.length < 32) throw new IllegalStateException("JWT_SECRET 至少需要 32 字节");
|
||||
return new NimbusJwtEncoder(new ImmutableSecret<>(secret));
|
||||
try {
|
||||
JWKSet set = JWKSet.parse(properties.rsaJwkSet());
|
||||
RSAKey current = set.getKeyByKeyId(properties.currentKid()) instanceof RSAKey rsa ? rsa : null;
|
||||
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
|
||||
|
||||
@@ -8,7 +8,8 @@ import java.time.Duration;
|
||||
* 认证安全配置属性,承载 JWT 密钥、签发方及访问令牌、刷新令牌有效期。
|
||||
*/
|
||||
@ConfigurationProperties(prefix = "baiyue.security")
|
||||
public record AuthSecurityProperties(String jwtSecret, String issuer,
|
||||
public record AuthSecurityProperties(String jwtSecret, String issuer, String audience,
|
||||
String rsaJwkSet, String currentKid,
|
||||
Duration accessTokenTtl, Duration refreshTokenTtl,
|
||||
int maxLoginFailures, Duration lockDuration) {
|
||||
}
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
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 org.springframework.security.crypto.password.PasswordEncoder;
|
||||
import org.springframework.security.oauth2.jose.jws.MacAlgorithm;
|
||||
import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm;
|
||||
import org.springframework.security.oauth2.jwt.JwtClaimsSet;
|
||||
import org.springframework.security.oauth2.jwt.JwtEncoder;
|
||||
import org.springframework.security.oauth2.jwt.JwtEncoderParameters;
|
||||
@@ -30,6 +30,7 @@ import java.time.LocalDateTime;
|
||||
import java.util.Base64;
|
||||
import java.util.UUID;
|
||||
import java.time.Duration;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 统一认证应用服务,编排密码校验、IAM 权限快照加载、小 JWT 签发、刷新令牌轮换与注销事务。
|
||||
@@ -108,7 +109,8 @@ public class AuthService {
|
||||
String jwtId = UUID.randomUUID().toString();
|
||||
Instant expiresAt = issuedAt.plus(properties.accessTokenTtl());
|
||||
JwtClaimsSet claims = JwtClaimsSet.builder()
|
||||
.issuer(properties.issuer()).id(jwtId).issuedAt(issuedAt).expiresAt(expiresAt)
|
||||
.issuer(properties.issuer()).audience(List.of(properties.audience()))
|
||||
.id(jwtId).issuedAt(issuedAt).expiresAt(expiresAt)
|
||||
.subject(account.getId().toString())
|
||||
.claim("user_id", account.getId())
|
||||
.claim("username", account.getUsername())
|
||||
@@ -119,9 +121,15 @@ public class AuthService {
|
||||
.claim("client_type", authorization.getClientType())
|
||||
.claim("token_version", account.getTokenVersion())
|
||||
.claim("password_change_required", Boolean.TRUE.equals(account.getPasswordChangeRequired()))
|
||||
.claim("userId", account.getId())
|
||||
.claim("tenantId", account.getDefaultTenantId())
|
||||
.claim("appCode", account.getDefaultAppCode())
|
||||
.claim("roles", authorization.getRoles())
|
||||
.claim("authorities", authorization.getPermissions())
|
||||
.claim("permissionVersion", account.getTokenVersion())
|
||||
.build();
|
||||
String accessToken = jwtEncoder.encode(JwtEncoderParameters.from(
|
||||
JwsHeader.with(MacAlgorithm.HS256).build(), claims)).getTokenValue();
|
||||
signingHeader(), claims)).getTokenValue();
|
||||
|
||||
byte[] random = new byte[48];
|
||||
secureRandom.nextBytes(random);
|
||||
@@ -167,16 +175,21 @@ public class AuthService {
|
||||
Instant issuedAt = Instant.now();
|
||||
Instant expiresAt = issuedAt.plus(Duration.ofMinutes(15));
|
||||
String jwtId = UUID.randomUUID().toString();
|
||||
JwtClaimsSet claims = JwtClaimsSet.builder().issuer(properties.issuer()).id(jwtId)
|
||||
JwtClaimsSet claims = JwtClaimsSet.builder().issuer(properties.issuer())
|
||||
.audience(List.of(properties.audience())).id(jwtId)
|
||||
.issuedAt(issuedAt).expiresAt(expiresAt).subject(account.getId().toString())
|
||||
.claim("user_id", account.getId()).claim("username", account.getUsername())
|
||||
.claim("tenant_id", account.getDefaultTenantId()).claim("operator_tenant_id", account.getOperatorTenantId())
|
||||
.claim("membership_id", account.getDefaultMembershipId()).claim("app_code", "MERCHANT")
|
||||
.claim("client_type", authorization.getClientType()).claim("token_version", account.getTokenVersion())
|
||||
.claim("userId", account.getId()).claim("tenantId", account.getDefaultTenantId())
|
||||
.claim("appCode", "MERCHANT").claim("roles", authorization.getRoles())
|
||||
.claim("authorities", authorization.getPermissions())
|
||||
.claim("permissionVersion", account.getTokenVersion())
|
||||
.claim("impersonated", true).claim("impersonation_id", ticket.ticketId())
|
||||
.claim("actor_user_id", ticket.actorUserId()).claim("actor_membership_id", ticket.actorMembershipId())
|
||||
.claim("password_change_required", Boolean.TRUE.equals(account.getPasswordChangeRequired())).build();
|
||||
String accessToken = jwtEncoder.encode(JwtEncoderParameters.from(JwsHeader.with(MacAlgorithm.HS256).build(), claims)).getTokenValue();
|
||||
String accessToken = jwtEncoder.encode(JwtEncoderParameters.from(signingHeader(), claims)).getTokenValue();
|
||||
tokenSessionStore.storeAccessOnly(jwtId, account.getId(), authorization.getRoles(),
|
||||
authorization.getPermissions(), Duration.ofMinutes(15));
|
||||
return new ImpersonationTokenResponse("Bearer", accessToken, 900);
|
||||
@@ -184,6 +197,10 @@ public class AuthService {
|
||||
|
||||
public record ImpersonationTokenResponse(String tokenType, String accessToken, long expiresIn) { }
|
||||
|
||||
private JwsHeader signingHeader() {
|
||||
return JwsHeader.with(SignatureAlgorithm.RS256).keyId(properties.currentKid()).build();
|
||||
}
|
||||
|
||||
private static BizException invalidCredentials() {
|
||||
return new BizException(ResultCode.UNAUTHORIZED, "用户名或密码错误");
|
||||
}
|
||||
|
||||
@@ -31,10 +31,13 @@ spring:
|
||||
encode: UTF-8
|
||||
baiyue.security:
|
||||
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}
|
||||
token-key-prefix: ${REDIS_TOKEN_KEY_PREFIX:baiyue:auth}
|
||||
issuer: ${JWT_ISSUER:auth-service}
|
||||
access-token-ttl: ${ACCESS_TOKEN_TTL:24h}
|
||||
audience: ${JWT_AUDIENCE:lastleg-api}
|
||||
access-token-ttl: ${ACCESS_TOKEN_TTL:24m}
|
||||
refresh-token-ttl: ${REFRESH_TOKEN_TTL:7d}
|
||||
max-login-failures: ${MAX_LOGIN_FAILURES:5}
|
||||
lock-duration: ${LOGIN_LOCK_DURATION:15m}
|
||||
|
||||
+1
-1
@@ -15,7 +15,7 @@ class AuthTokenTtlConfigurationContractTest {
|
||||
Path.of("..", "config", "nacos", "cn", "auth-service.yml"), StandardCharsets.UTF_8);
|
||||
|
||||
assertThat(configuration)
|
||||
.contains("access-token-ttl: ${ACCESS_TOKEN_TTL:2h}")
|
||||
.contains("access-token-ttl: ${ACCESS_TOKEN_TTL:15m}")
|
||||
.contains("refresh-token-ttl: ${REFRESH_TOKEN_TTL:7d}")
|
||||
.doesNotContain("access-token-ttl: ${ACCESS_TOKEN_TTL:7d}");
|
||||
}
|
||||
|
||||
@@ -39,13 +39,14 @@ class AuthServiceSmallJwtTest {
|
||||
static class TestConfig {
|
||||
@Bean
|
||||
AuthSecurityProperties properties() {
|
||||
return new AuthSecurityProperties("unused", "auth-service", Duration.ofMinutes(30),
|
||||
return new AuthSecurityProperties("unused", "auth-service", "lastleg-api", "{}", "current-2026-08",
|
||||
Duration.ofMinutes(30),
|
||||
Duration.ofDays(7), 5, Duration.ofMinutes(10));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void signsIdentityOnlyJwtAndStoresAuthorizationInRedis() {
|
||||
void signsCrossRegionClaimsAndStoresSession() {
|
||||
AuthAccount account = new AuthAccount();
|
||||
account.setId(42L);
|
||||
account.setUsername("operator");
|
||||
@@ -72,8 +73,16 @@ class AuthServiceSmallJwtTest {
|
||||
verify(jwtEncoder).encode(jwt.capture());
|
||||
assertThat(jwt.getValue().getClaims().getClaims())
|
||||
.containsEntry("user_id", 42L)
|
||||
.containsEntry("userId", 42L)
|
||||
.containsEntry("tenantId", 0L)
|
||||
.containsEntry("appCode", "PLATFORM")
|
||||
.containsEntry("roles", Set.of("PLATFORM_ADMIN"))
|
||||
.containsEntry("authorities", Set.of("platform:order:list"))
|
||||
.containsEntry("permissionVersion", 1)
|
||||
.containsEntry("membership_id", 84L)
|
||||
.doesNotContainKeys("roles", "permissions", "authorities", "scope");
|
||||
.doesNotContainKeys("permissions", "scope");
|
||||
assertThat(jwt.getValue().getClaims().getAudience()).containsExactly("lastleg-api");
|
||||
assertThat(jwt.getValue().getJwsHeader().getKeyId()).isEqualTo("current-2026-08");
|
||||
verify(tokenSessionStore).store(any(), org.mockito.ArgumentMatchers.eq(42L),
|
||||
org.mockito.ArgumentMatchers.eq(Set.of("PLATFORM_ADMIN")),
|
||||
org.mockito.ArgumentMatchers.eq(Set.of("platform:order:list")), any(),
|
||||
|
||||
Reference in New Issue
Block a user