fix: project overseas authorization outside jwt
This commit is contained in:
+5
@@ -1,6 +1,7 @@
|
||||
package com.baiyue.common.security;
|
||||
|
||||
import org.springframework.security.oauth2.jwt.Jwt;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
|
||||
/**
|
||||
* 访问令牌会话有效性校验扩展点,用于在 JWT 校验之外确认服务端登录会话状态。
|
||||
@@ -8,4 +9,8 @@ import org.springframework.security.oauth2.jwt.Jwt;
|
||||
@FunctionalInterface
|
||||
public interface AccessTokenSessionValidator {
|
||||
AccessTokenSession validate(Jwt jwt);
|
||||
|
||||
default AccessTokenSession validate(Jwt jwt, HttpServletRequest request) {
|
||||
return validate(jwt);
|
||||
}
|
||||
}
|
||||
|
||||
+2
@@ -50,9 +50,11 @@ public class BaiyueSecurityAutoConfiguration {
|
||||
org.springframework.beans.factory.ObjectProvider<StringRedisTemplate> redis,
|
||||
ObjectMapper objectMapper,
|
||||
@Value("${baiyue.security.token-key-prefix:baiyue:auth}") String keyPrefix,
|
||||
@Value("${baiyue.security.gateway-internal-service-token:${baiyue.security.internal-service-token:${INTERNAL_SERVICE_TOKEN:}}}") String gatewayToken,
|
||||
@Value("${baiyue.security.session-mode:redis}") String sessionMode) {
|
||||
AccessTokenSessionValidator validator;
|
||||
if ("claims".equalsIgnoreCase(sessionMode)) validator = new ClaimsAccessTokenSessionValidator();
|
||||
else if ("gateway".equalsIgnoreCase(sessionMode)) validator = new GatewayAccessTokenSessionValidator(gatewayToken);
|
||||
else {
|
||||
StringRedisTemplate template=redis.getIfAvailable();
|
||||
if(template==null) throw new IllegalStateException("session-mode=redis requires StringRedisTemplate");
|
||||
|
||||
+57
@@ -0,0 +1,57 @@
|
||||
package com.baiyue.common.security;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import org.springframework.security.oauth2.jwt.Jwt;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.MessageDigest;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.Set;
|
||||
|
||||
/** Uses the overseas gateway's authenticated authorization snapshot instead of large JWT claims. */
|
||||
public final class GatewayAccessTokenSessionValidator implements AccessTokenSessionValidator {
|
||||
public static final String TOKEN_HEADER = "X-Gateway-Internal-Token";
|
||||
public static final String ROLES_HEADER = "X-Overseas-Roles";
|
||||
public static final String AUTHORITIES_HEADER = "X-Overseas-Authorities";
|
||||
|
||||
private final byte[] expectedToken;
|
||||
|
||||
public GatewayAccessTokenSessionValidator(String expectedToken) {
|
||||
if (expectedToken == null || expectedToken.isBlank()) {
|
||||
throw new IllegalStateException("session-mode=gateway requires an internal service token");
|
||||
}
|
||||
this.expectedToken = expectedToken.getBytes(StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
@Override
|
||||
public AccessTokenSession validate(Jwt jwt) {
|
||||
throw new IllegalArgumentException("Gateway authorization headers are required");
|
||||
}
|
||||
|
||||
@Override
|
||||
public AccessTokenSession validate(Jwt jwt, HttpServletRequest request) {
|
||||
String supplied = request.getHeader(TOKEN_HEADER);
|
||||
if (supplied == null || !MessageDigest.isEqual(expectedToken, supplied.getBytes(StandardCharsets.UTF_8))) {
|
||||
throw new IllegalArgumentException("Untrusted overseas gateway authorization context");
|
||||
}
|
||||
Long userId = number(jwt.getClaims().getOrDefault("userId", jwt.getClaims().get("user_id")));
|
||||
return new AccessTokenSession(1, userId, values(request.getHeader(ROLES_HEADER)),
|
||||
values(request.getHeader(AUTHORITIES_HEADER)));
|
||||
}
|
||||
|
||||
private static Long number(Object value) {
|
||||
if (value instanceof Number number) return number.longValue();
|
||||
try { return value == null ? null : Long.valueOf(value.toString()); }
|
||||
catch (NumberFormatException exception) { return null; }
|
||||
}
|
||||
|
||||
private static Set<String> values(String header) {
|
||||
if (header == null || header.isBlank()) return Set.of();
|
||||
LinkedHashSet<String> values = new LinkedHashSet<>();
|
||||
for (String value : header.split(",")) {
|
||||
String normalized = value.trim();
|
||||
if (!normalized.isEmpty()) values.add(normalized);
|
||||
}
|
||||
return Set.copyOf(values);
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -55,7 +55,7 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter {
|
||||
String tokenValue = header.substring(BEARER.length()).trim();
|
||||
try {
|
||||
Jwt jwt = jwtDecoder.decode(tokenValue);
|
||||
AccessTokenSession session = sessionValidator.validate(jwt);
|
||||
AccessTokenSession session = sessionValidator.validate(jwt, request);
|
||||
Authentication authentication = claimsMapper.map(jwt, session);
|
||||
SecurityContextHolder.getContext().setAuthentication(authentication);
|
||||
filterChain.doFilter(request, response);
|
||||
|
||||
+46
@@ -0,0 +1,46 @@
|
||||
package com.baiyue.common.security;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.mock.web.MockHttpServletRequest;
|
||||
import org.springframework.security.oauth2.jwt.Jwt;
|
||||
|
||||
import java.time.Instant;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
|
||||
class GatewayAccessTokenSessionValidatorTest {
|
||||
private final GatewayAccessTokenSessionValidator validator =
|
||||
new GatewayAccessTokenSessionValidator("gateway-secret");
|
||||
|
||||
@Test void acceptsOnlyGatewayAuthenticatedAuthorizationSnapshot() {
|
||||
MockHttpServletRequest request = new MockHttpServletRequest();
|
||||
request.addHeader(GatewayAccessTokenSessionValidator.TOKEN_HEADER, "gateway-secret");
|
||||
request.addHeader(GatewayAccessTokenSessionValidator.ROLES_HEADER, "PLATFORM_ADMIN");
|
||||
request.addHeader(GatewayAccessTokenSessionValidator.AUTHORITIES_HEADER,
|
||||
"platform:overseas-warehouse:last-mile-order:list,platform:finance:fee-source:view");
|
||||
|
||||
AccessTokenSession session = validator.validate(jwt(), request);
|
||||
|
||||
assertThat(session.userId()).isEqualTo(42L);
|
||||
assertThat(session.roles()).containsExactly("PLATFORM_ADMIN");
|
||||
assertThat(session.permissions()).containsExactlyInAnyOrder(
|
||||
"platform:overseas-warehouse:last-mile-order:list", "platform:finance:fee-source:view");
|
||||
}
|
||||
|
||||
@Test void rejectsForgedOrMissingGatewayToken() {
|
||||
MockHttpServletRequest forged = new MockHttpServletRequest();
|
||||
forged.addHeader(GatewayAccessTokenSessionValidator.TOKEN_HEADER, "forged");
|
||||
forged.addHeader(GatewayAccessTokenSessionValidator.AUTHORITIES_HEADER,
|
||||
"platform:overseas-warehouse:last-mile-order:list");
|
||||
assertThatThrownBy(() -> validator.validate(jwt(), forged))
|
||||
.isInstanceOf(IllegalArgumentException.class).hasMessageContaining("Untrusted");
|
||||
}
|
||||
|
||||
private Jwt jwt() {
|
||||
return Jwt.withTokenValue("small-overseas-token").header("alg", "RS256").subject("42")
|
||||
.issuedAt(Instant.now()).expiresAt(Instant.now().plusSeconds(300))
|
||||
.claim("user_id", 42L).claim("tenant_id", 0L).claim("app_code", "PLATFORM")
|
||||
.claim("permission_version", 7L).claim("token_version", 1).build();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user