Spring 引导 + JWT Oauth2:Spring 5 对比 Spring <5
Spring Boot + JWT Oauth2: Spring 5 vs Spring <5
我们有一个项目:
+------------------------------------+
| | 1. gets the token
| Authorization Server (Auth) | <------------------+
| - spring-security-oauth2:2.0.14 | |
| | |
+------------------------------------+ +
user
+
+------------------------------------+ |
| | 2. uses the token |
| Resource Server (RS) | to access resourcs|
| - spring-security-oauth2:5.1.0 | <------------------+
| |
+------------------------------------+
我们一直在 没有 Webflux 的环境中工作,一切都按预期进行。值得一提的是,这个 JWT 有以下声明:exp, user_name, authorities, jti, client_id, scope.
所有资源都有一个额外的变量:
@GetMapping("/{id}/car")
public SomeDto someResourceMethod(@PathVariable("id") CarId carId, Principal principal)
在"Spring Web"中(不使用Reactor):Principal
实例化为OAuth2Authentication
,我们有对它的操作getName
。
在"Spring Webflux"中(使用Reactor):Principal
实例化为JwtAuthenticationToken
,我们现在有getSubject
操作,但它为空,因为声明 sub
为空。
您认为我们应该如何处理这个问题?创建一个新的 JwtDecoder
?我们使用的是 Spring 5.1,解码器是 NimbusReactiveJwtDecoder
.
此时,Reactive Resource Server 根据 RFC 7519 支持 JWT 声明,这就是您看到行为变化的原因。
是的,您可以创建自己的解码器,这可能是侵入性最小的方法:
public class CustomDecoder implements ReactiveJwtDecoder {
private final ReactiveJwtDecoder nimbus;
// ...
public Mono<Jwt> decode(String token) {
return this.nimbus.decode(token)
.map(this::mapJwt);
}
private Jwt mapJwt(Jwt jwt) {
Map<String, Object> claims = jwt.getClaims();
// ... map claims accordingly
return new Jwt(...);
}
}
您还可以自定义身份验证管理器,即 introduced in RC2:
public class CustomReactiveAuthenticationManager
implements ReactiveAuthenticationManager {
private final ReactiveAuthenticationManager delegate;
// ...
public Mono<Authentication> authenticate(Authentication authentication) {
return this.delegate.authenticate(authentication)
.map(this::mapAuthentication);
}
private Authentication mapAuthentication(Authentication authentication) {
// ... create a custom authentication where getName does what you need
}
}
或者,如果您能够对方法签名进行一些重构,那么另一种选择是使用 @AuthenticatedPrincipal
:
@GetMapping("/{id}/car")
public SomeDto someResourceMethod(
@PathVariable("id") CarId carId,
@AuthenticatedPrincipal Jwt jwt) {
String name = jwt.getClaims().get("user_name");
// ...
}
或者更简洁
@Target({ ElementType.PARAMETER })
@Retention(RetentionPolicy.RUNTIME)
@AuthenticationPrincipal(expression = "getClaims().get('user_name')")
public @interface CurrentUsername {}
@GetMapping("/{id}/car")
public SomeDto someResourceMethod(
@PathVariable("id") CarId carId,
@CurrentUsername String name) {
// ...
}
您可能还会考虑在 Spring Security 上记录增强功能,以考虑使用户属性名称可配置。
编辑:我更新了 EL 表达式,因为默认情况下 @AuthenticatedPrincipal
首先调用 authentication.getPrincipal()
。
UPDATE:在 Spring Security 5.4+ 中,持有主体名称的声明是可配置的,如下所示:
@Bean
JwtAuthenticationConverter jwtAuthenticationConverter() {
JwtAuthenticationConverter authenticationConverter =
new JwtAuthenticationConverter();
authenticationConverter.setPrincipalClaimName("user_name");
return authenticationConverter;
}
然后允许 OP 像其他主体类型一样使用 Principal#getName
。
我们有一个项目:
+------------------------------------+
| | 1. gets the token
| Authorization Server (Auth) | <------------------+
| - spring-security-oauth2:2.0.14 | |
| | |
+------------------------------------+ +
user
+
+------------------------------------+ |
| | 2. uses the token |
| Resource Server (RS) | to access resourcs|
| - spring-security-oauth2:5.1.0 | <------------------+
| |
+------------------------------------+
我们一直在 没有 Webflux 的环境中工作,一切都按预期进行。值得一提的是,这个 JWT 有以下声明:exp, user_name, authorities, jti, client_id, scope.
所有资源都有一个额外的变量:
@GetMapping("/{id}/car")
public SomeDto someResourceMethod(@PathVariable("id") CarId carId, Principal principal)
在"Spring Web"中(不使用Reactor):
Principal
实例化为OAuth2Authentication
,我们有对它的操作getName
。在"Spring Webflux"中(使用Reactor):
Principal
实例化为JwtAuthenticationToken
,我们现在有getSubject
操作,但它为空,因为声明sub
为空。
您认为我们应该如何处理这个问题?创建一个新的 JwtDecoder
?我们使用的是 Spring 5.1,解码器是 NimbusReactiveJwtDecoder
.
此时,Reactive Resource Server 根据 RFC 7519 支持 JWT 声明,这就是您看到行为变化的原因。
是的,您可以创建自己的解码器,这可能是侵入性最小的方法:
public class CustomDecoder implements ReactiveJwtDecoder {
private final ReactiveJwtDecoder nimbus;
// ...
public Mono<Jwt> decode(String token) {
return this.nimbus.decode(token)
.map(this::mapJwt);
}
private Jwt mapJwt(Jwt jwt) {
Map<String, Object> claims = jwt.getClaims();
// ... map claims accordingly
return new Jwt(...);
}
}
您还可以自定义身份验证管理器,即 introduced in RC2:
public class CustomReactiveAuthenticationManager
implements ReactiveAuthenticationManager {
private final ReactiveAuthenticationManager delegate;
// ...
public Mono<Authentication> authenticate(Authentication authentication) {
return this.delegate.authenticate(authentication)
.map(this::mapAuthentication);
}
private Authentication mapAuthentication(Authentication authentication) {
// ... create a custom authentication where getName does what you need
}
}
或者,如果您能够对方法签名进行一些重构,那么另一种选择是使用 @AuthenticatedPrincipal
:
@GetMapping("/{id}/car")
public SomeDto someResourceMethod(
@PathVariable("id") CarId carId,
@AuthenticatedPrincipal Jwt jwt) {
String name = jwt.getClaims().get("user_name");
// ...
}
或者更简洁
@Target({ ElementType.PARAMETER })
@Retention(RetentionPolicy.RUNTIME)
@AuthenticationPrincipal(expression = "getClaims().get('user_name')")
public @interface CurrentUsername {}
@GetMapping("/{id}/car")
public SomeDto someResourceMethod(
@PathVariable("id") CarId carId,
@CurrentUsername String name) {
// ...
}
您可能还会考虑在 Spring Security 上记录增强功能,以考虑使用户属性名称可配置。
编辑:我更新了 EL 表达式,因为默认情况下 @AuthenticatedPrincipal
首先调用 authentication.getPrincipal()
。
UPDATE:在 Spring Security 5.4+ 中,持有主体名称的声明是可配置的,如下所示:
@Bean
JwtAuthenticationConverter jwtAuthenticationConverter() {
JwtAuthenticationConverter authenticationConverter =
new JwtAuthenticationConverter();
authenticationConverter.setPrincipalClaimName("user_name");
return authenticationConverter;
}
然后允许 OP 像其他主体类型一样使用 Principal#getName
。