【问题标题】:Spring Boot + JWT Oauth2: Spring 5 vs Spring <5Spring Boot + JWT Oauth2:Spring 5 与 Spring <5
【发布时间】:2019-02-10 15:37:13
【问题描述】:

我们有一个项目:

+------------------------------------+
|                                    |    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 操作,但自声明 @ 以来它为空987654329@ 为空。

您认为我们应该如何处理这个问题?创建一个新的JwtDecoder?我们使用的是 Spring 5.1,解码器是NimbusReactiveJwtDecoder

【问题讨论】:

    标签: spring-boot spring-security project-reactor


    【解决方案1】:

    此时,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()

    更新:在 Spring Security 5.4+ 中,持有主体名称的声明是可配置的,如下所示:

    @Bean
    JwtAuthenticationConverter jwtAuthenticationConverter() {
        JwtAuthenticationConverter authenticationConverter =
            new JwtAuthenticationConverter();
        authenticationConverter.setPrincipalClaimName("user_name");
        return authenticationConverter;
    }
    

    然后允许 OP 将Principal#getName 与其他主体类型一起使用。

    【讨论】:

    • 非常感谢@jzheaux!认真完美的答案。我刚刚收到错误"message": "EL1004E: Method call: Method getTokenAttributes() cannot be found on type org.springframework.security.oauth2.jwt.Jwt"。这是我认为的类型的铸造问题。我使用以下表达式解决了这个问题:getClaims().get('user_name').toString() 你觉得怎么样?
    • 为了在控制器方面进行测试,由于 SEPL 表达式,我们不能再使用 @WithMockUser ... 相反,我们可以使用 authentication 并创建一个很好的 JwtAuthenticationToken。你有如何做到这一点的任何例子吗?非常感谢。
    猜你喜欢
    • 2018-10-29
    • 2020-10-25
    • 2020-07-07
    • 2020-05-26
    • 2020-06-19
    • 2018-08-18
    • 2017-09-09
    • 2023-03-18
    • 2017-01-31
    相关资源
    最近更新 更多