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)

您认为我们应该如何处理这个问题?创建一个新的 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