如何在 webflux 中实现自定义身份验证管理器时响应自定义 json body 未经授权的请求

How to response custom json body on unauthorized requests while implementing custom authentication manager in webflux

我正在尝试实现自定义 JWT 令牌身份验证,同时我也在处理全局异常以自定义每种异常类型的响应 body。一切正常,除了我想在收到未经授权的请求时 return 自定义 json 响应,而不仅仅是 401 状态代码。

下面是我对 JwtServerAuthenticationConverter 和 JwtAuthenticationManager 的实现。

@Component
public class JwtServerAuthenticationConverter implements ServerAuthenticationConverter {

    private static final String AUTH_HEADER_VALUE_PREFIX = "Bearer ";

    @Override
    public Mono<Authentication> convert(ServerWebExchange exchange) {

        return Mono.justOrEmpty(exchange)
                .flatMap(serverWebExchange -> Mono.justOrEmpty(
                        serverWebExchange
                                .getRequest()
                                .getHeaders()
                                .getFirst(HttpHeaders.AUTHORIZATION)
                        )
                )
                .filter(header -> !header.trim().isEmpty() && header.trim().startsWith(AUTH_HEADER_VALUE_PREFIX))
                .map(header -> header.substring(AUTH_HEADER_VALUE_PREFIX.length()))
                .map(token -> new UsernamePasswordAuthenticationToken(token, token))
                ;
    }
}
@Component
public class JwtAuthenticationManager implements ReactiveAuthenticationManager {

    private final JWTConfig jwtConfig;
    private final ObjectMapper objectMapper;

    public JwtAuthenticationManager(JWTConfig jwtConfig, ObjectMapper objectMapper) {
        this.jwtConfig = jwtConfig;
        this.objectMapper = objectMapper;
    }

    @Override
    public Mono<Authentication> authenticate(Authentication authentication) {

        return Mono.just(authentication)
                .map(auth -> JWTHelper.loadAllClaimsFromToken(auth.getCredentials().toString(), jwtConfig.getSecret()))
                .onErrorResume(throwable -> Mono.error(new JwtException("Unauthorized")))
                .map(claims -> objectMapper.convertValue(claims, JWTUserDetails.class))
                .map(jwtUserDetails ->
                        new UsernamePasswordAuthenticationToken(
                                jwtUserDetails,
                                authentication.getCredentials(),
                                jwtUserDetails.getGrantedAuthorities()
                        )
                )
                ;
    }
}

下面是我的全局异常处理,它工作得非常好,除了 webflux return 401 来自 JwtServerAuthenticationConverter 转换方法的情况。

@Configuration
@Order(-2)
public class ExceptionHandler implements WebExceptionHandler {

    @Override
    public Mono<Void> handle(ServerWebExchange exchange, Throwable ex) {

        exchange.getResponse().getHeaders().set("Content-Type", MediaType.APPLICATION_JSON_VALUE);

        return buildErrorResponse(ex)
                .flatMap(
                        r -> r.writeTo(exchange, new HandlerStrategiesResponseContext(HandlerStrategies.withDefaults()))
                );
    }

    private Mono<ServerResponse> buildErrorResponse(Throwable ex) {

        if (ex instanceof RequestEntityValidationException) {

            return ServerResponse.badRequest().contentType(MediaType.APPLICATION_JSON).body(
                    Mono.just(new ErrorResponse(ex.getMessage())),
                    ErrorResponse.class
            );

        } else if (ex instanceof ResponseStatusException) {
            ResponseStatusException exception = (ResponseStatusException) ex;

            if (exception.getStatus().value() == 404) {
                return ServerResponse.status(HttpStatus.NOT_FOUND).contentType(MediaType.APPLICATION_JSON).body(
                        Mono.just(new ErrorResponse("Resource not found - 404")),
                        ErrorResponse.class
                );
            } else if (exception.getStatus().value() == 400) {
                return ServerResponse.status(HttpStatus.BAD_REQUEST).contentType(MediaType.APPLICATION_JSON).body(
                        Mono.just(new ErrorResponse("Unable to parse request body - 400")),
                        ErrorResponse.class
                );
            }

        } else if (ex instanceof JwtException) {

            return ServerResponse.status(HttpStatus.UNAUTHORIZED).contentType(MediaType.APPLICATION_JSON).body(
                    Mono.just(new ErrorResponse(ex.getMessage())),
                    ErrorResponse.class
            );
        }

        ex.printStackTrace();
        return ServerResponse.status(HttpStatus.INTERNAL_SERVER_ERROR).contentType(MediaType.APPLICATION_JSON).body(
                Mono.just(new ErrorResponse("Internal server error - 500")),
                ErrorResponse.class
        );

    }
}

@RequiredArgsConstructor
class HandlerStrategiesResponseContext implements ServerResponse.Context {

    private final HandlerStrategies handlerStrategies;

    @Override
    public List<HttpMessageWriter<?>> messageWriters() {
        return this.handlerStrategies.messageWriters();
    }

    @Override
    public List<ViewResolver> viewResolvers() {
        return this.handlerStrategies.viewResolvers();
    }
}

@Configuration
@EnableWebFluxSecurity
public class SecurityConfig {

    @Bean
    public SecurityWebFilterChain securityWebFilterChain(
            ServerHttpSecurity http,
            ReactiveAuthenticationManager jwtAuthenticationManager,
            ServerAuthenticationConverter jwtAuthenticationConverter
    ) {

        AuthenticationWebFilter authenticationWebFilter = new AuthenticationWebFilter(jwtAuthenticationManager);
        authenticationWebFilter.setServerAuthenticationConverter(jwtAuthenticationConverter);

        return http
                .authorizeExchange()
                .pathMatchers("/auth/login", "/auth/logout").permitAll()
                .anyExchange().authenticated()
                .and()
                .addFilterAt(authenticationWebFilter, SecurityWebFiltersOrder.AUTHENTICATION)
                .httpBasic()
                .disable()
                .csrf()
                .disable()
                .formLogin()
                .disable()
                .logout()
                .disable()
                .build();
    }

    @Bean
    public BCryptPasswordEncoder bCryptPasswordEncoder() {
        return new BCryptPasswordEncoder();
    }
}

因此,当我在 header 中使用无效的 JWT 令牌访问它时。这由我的 ExceptioHandler class 处理,我得到了低于输出的结果,这很好。

但是当我用空的 jwt 令牌点击它时,我得到了这个。

现在我想 return 与我 return 在 JWT 令牌无效的情况下一样 body。但问题是,当提供空令牌时,它甚至不属于 ExceptionHandler class 的句柄方法。这就是为什么它不像我在同一个 class 中对 JwtException 所做的那样在我的控制之下。我该怎么做,请帮忙?

我自己整理。 webflux 提供 ServerAuthenticationFailureHandler 来处理自定义响应,但不幸的是 ServerAuthenticationFailureHandler 不工作,这是一个已知问题,所以我创建了一个失败路由并在其中写入我的自定义响应并设置登录页面。

.formLogin()
.loginPage("/auth/failed")
.and()
.andRoute(path("/auth/failed").and(accept(MediaType.APPLICATION_JSON)), (serverRequest) ->
        ServerResponse
                .status(HttpStatus.UNAUTHORIZED)
                .body(
                        Mono.just(new ErrorResponse("Unauthorized")),
                        ErrorResponse.class
                )
);