Spring Boot 3.2、Spring Security 6
这是一个 Spring Cloud Gateway,我将其实现为 OAuth2 资源服务器,它也具有执行器端点。我正在尝试使用 JWT 角色来限制对执行器端点的访问。我已经在非反应式堆栈中的另一个微服务中成功完成了此操作。但是,网关不支持 starter-web。我无法找出反应式堆栈中等效的 JWT 转换器。
EnableWebFluxSecurity ServerHttpSecurity 设置为允许基于 jwt 令牌中的 JWT 角色访问执行器端点。但是,在调试时,AbstractAuthenticationToken 仅具有“SCOPE_”授予权限。还应该提取 JWT 用户角色。
pom:
<!--#### SECURITY ###-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
</dependency>
<!--#### REST ###-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
道具:
spring:
main:
allow-bean-definition-overriding: true
web-application-type: reactive
security:
oauth2:
resourceserver:
jwt:
issuer-uri: "http://localhost:${env.idp-port}/realms/osint-realm"
jwk-set-uri: ${spring.security.oauth2.resourceserver.jwt.issuer-uri}/protocol/openid-connect/certs
jwt:
token:
converter:
resource-id: osint-keycloak-client
principal-attribute: preferred_username
服务器过滤器
@Configuration
@EnableWebFluxSecurity
public class WebFluxSecurityConfig {
@Value("${spring.application.name}")
private String appName;
private final CustomJwtTokenConverter customJwtTokenConverter;
public WebFluxSecurityConfig(TokenConverterProperties tokenConverterProperties) {
JwtGrantedAuthoritiesConverter jwtGrantedAuthoritiesConverter = new JwtGrantedAuthoritiesConverter();
this.customJwtTokenConverter = new CustomJwtTokenConverter(jwtGrantedAuthoritiesConverter, tokenConverterProperties);
}
@Bean
public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity serverHttpSecurity) {
serverHttpSecurity
.csrf(ServerHttpSecurity.CsrfSpec::disable)
.formLogin(ServerHttpSecurity.FormLoginSpec::disable)
.authorizeExchange(exchange -> {
exchange.pathMatchers("/eureka/**").permitAll();
exchange.pathMatchers("/" + appName + "/actuator/**").hasRole("actuator");
exchange.anyExchange().authenticated();
})
//This is only pulling scopes not roles?
.oauth2ResourceServer(oauth2 -> oauth2
.jwt(Customizer.withDefaults()));
return serverHttpSecurity.build();
}
我认为问题出在 JWT Customizer.withDefaults 上。我认为这给了我 SCOPE_profile、SCOPE_email 的匿名权限,这些权限不在提供的 JWT 令牌上
我尝试遵循Spring Security 6.1.4但是,这些片段对于创建自定义 JWT 转换器来说非常模糊。
这是我对客户 JWT 转换器的尝试
public class CustomJwtTokenConverter implements Converter<Jwt, AbstractAuthenticationToken> {
private static final String RESOURCE_ACCESS = "resource_access";
private static final String ROLES = "roles";
protected static final String ROLE_PREFIX = "ROLE_";
private final JwtGrantedAuthoritiesConverter jwtGrantedAuthoritiesConverter;
private final TokenConverterProperties properties;
public CustomJwtTokenConverter(
JwtGrantedAuthoritiesConverter jwtGrantedAuthoritiesConverter,
TokenConverterProperties properties) {
this.jwtGrantedAuthoritiesConverter = jwtGrantedAuthoritiesConverter;
this.properties = properties;
}
@Override
public AbstractAuthenticationToken convert(@NonNull Jwt jwt) {
List<Collection<String>> roleResources = Optional.of(jwt)
.map(token -> token.getClaimAsMap(RESOURCE_ACCESS))
.map(claimMap -> (Map<String, Object>) claimMap.get(properties.getResourceId()))
.map(resourceData -> (Collection<String>) resourceData.get(ROLES))
.stream().collect(Collectors.toList());
List<SimpleGrantedAuthority> simpleGrantedAuthorities = new ArrayList<>();
roleResources.forEach(role -> role.forEach(roleValue -> simpleGrantedAuthorities.add(new SimpleGrantedAuthority(ROLE_PREFIX + roleValue))));
Stream<SimpleGrantedAuthority> accesses = simpleGrantedAuthorities.stream().distinct();
Set<GrantedAuthority> authorities = Stream
.concat(jwtGrantedAuthoritiesConverter.convert(jwt).stream(), accesses)
.collect(Collectors.toSet());
String principalClaimName = properties.getPrincipalAttribute()
.map(jwt::getClaimAsString)
.orElse(jwt.getClaimAsString(JwtClaimNames.SUB));
return new JwtAuthenticationToken(jwt, authorities, principalClaimName);
}
}
但是,这提供的是 CustomerJwtTokenConverter 而不是过滤器中的转换器:
@Bean
public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity serverHttpSecurity) {
serverHttpSecurity
.csrf(ServerHttpSecurity.CsrfSpec::disable)
.formLogin(ServerHttpSecurity.FormLoginSpec::disable)
.authorizeExchange(exchange -> {
exchange.pathMatchers("/eureka/**").permitAll();
exchange.pathMatchers("/" + appName + "/actuator/**").hasRole("actuator");
exchange.anyExchange().authenticated();
})
//This is only pulling scopes not roles?
.oauth2ResourceServer(oauth2 -> oauth2
// .jwt(Customizer.withDefaults()));
.jwt(jwtSpec -> jwtSpec.jwtAuthenticationConverter(this.customJwtTokenConverter)));
return serverHttpSecurity.build();
您的转换器是一个同步转换器(用于 servlet),而不是反应式转换器。
要实现的接口是
Converter<Jwt, ? extends Mono<? extends AbstractAuthenticationToken>>
,而不是Converter<Jwt, AbstractAuthenticationToken>
。
请注意,您可能可以使用 Spring security 提供的
ReactiveJwtAuthenticationConverter
实现。这使事情变得更容易,因为当您使用 http.oauth2ResourceServer(oauth2 -> oauth2.jwt(Customizer.withDefaults()))
时,Spring Boot 会选择这样的 bean。
@Component
@RequiredArgsConstructor
static class KeycloakClientAuthoritiesConverter implements Converter<Jwt, Flux<GrantedAuthority>> {
private final TokenConverterProperties properties;
@Override
@SuppressWarnings("unchecked")
public Flux<GrantedAuthority> convert(Jwt source) {
final var resourceAccess = (Map<String, Object>) source.getClaims().getOrDefault("resource_access", Map.of());
final var clientAccess = (Map<String, Object>) resourceAccess.getOrDefault(properties.getResourceId(), Map.of());
final var roles = (List<String>) clientAccess.getOrDefault("roles", List.of());
return Flux.fromStream(roles.stream()).map(SimpleGrantedAuthority::new).map(GrantedAuthority.class::cast);
}
}
@Bean
ReactiveJwtAuthenticationConverter authenticationConverter(Converter<Jwt, Flux<GrantedAuthority>> authoritiesConverter) {
final var authenticationConverter = new ReactiveJwtAuthenticationConverter();
authenticationConverter.setJwtGrantedAuthoritiesConverter(authoritiesConverter);
authenticationConverter.setPrincipalClaimName(StandardClaimNames.PREFERRED_USERNAME);
return authenticationConverter;
}