Как установить context в ReactiveSecurityContextHolder Spring Webflux

В своем приложении пытаюсь провести аутентификацию и авторизацию пользователей по JWT токену. Основная задумка следующая:

  1. Пользователь получает токен в сервисе аутентификации
  2. Полученный токен на первом шаге вместе с GET запросом отправляется в cookie в сервис для получения данных
  3. Сервис с данными, перед тем как отдать данные, проверяет данный токен и извлекает из него имя пользователя и роли, создавая UsernamePasswordAuthenticationToken и складывая его в контекст (если с токеном все в порядке)

1й и 2й шаг выполнены, но с 3м проблемы следующие: при запросе данных я получаю 401 ошибку. Токен в порядке, проверку проходит, UsernamePasswordAuthenticationToken создаётся, очевидно проблема в инициализации ReactiveSecurityContextHolder, но что не так делаю не пойму, нужно подсказка.

Пример веб фильтра в котором инициализируется контекст:

@Component("jwtFilter")
public class JwtFilter implements WebFilter {
private final JwtTokenService jwtService;

private final AuthService authService;

@Autowired
public JwtFilter(JwtTokenService jwtService, AuthService authService) {
    this.jwtService = jwtService;
    this.authService = authService;
}

@Nonnull
@Override
public Mono<Void> filter(@Nonnull ServerWebExchange exchange, @Nonnull WebFilterChain chain) {
    extractJwtCookie(exchange.getRequest())
            .ifPresent(tokenCookie -> {
                String token = tokenCookie.getValue();

                jwtService.checkTokenExpired(token);

                Authentication authByJwt = authService.createAuthByJwt(token);

                ReactiveSecurityContextHolder.withSecurityContext(
         Mono.just(new SecurityContextImpl(authByJwt)));
            });

    return chain.filter(exchange);
}

private Optional<HttpCookie> extractJwtCookie(ServerHttpRequest request) {
    return Optional.ofNullable(request.getCookies().get("authToken"))
            .filter(httpCookies -> !httpCookies.isEmpty())
            .map(httpCookies -> httpCookies.get(0));
    }
}

Сервис, который создает UsernamePasswordAuthenticationToken:

@Service("authService")
public class AuthService {
private final JwtTokenService jwtService;

@Autowired
public AuthService(JwtTokenService jwtService) {
    this.jwtService = jwtService;
}

private UserDetails createUserDetails(List<String> roles, String username) {
    return JwtUserDetails.builder()
            .username(username)
            .roles(roles)
            .build();
}

public Authentication createAuthByJwt(String jwt) {

    UserDetails principal = createUserDetails(
            jwtService.getRoleListByToken(jwt),
            jwtService.getUsernameByToken(jwt));

    return new UsernamePasswordAuthenticationToken(
            principal,
            principal.getPassword(),
            principal.getAuthorities());
    }
}

Конфигурация безопасности:

@Configuration
public class SecurityConfig {
private final String allowOrigin;

public SecurityConfig(@Value("${allowOrigin}") String allowOrigin) {
    this.allowOrigin = allowOrigin;
}

@Bean("webFilterChain")
public SecurityWebFilterChain getMainConfig(ServerHttpSecurity httpSecurity,
                                            WebFilter jwtFilter) {
    return httpSecurity
            .addFilterAt(jwtFilter, SecurityWebFiltersOrder.HTTP_BASIC)
            .authorizeExchange(auth -> auth
                    .pathMatchers(HttpMethod.GET, "/articles")
                    .permitAll()
                    .pathMatchers(HttpMethod.GET, "/img/**")
                    .permitAll()
                    .pathMatchers(HttpMethod.GET, "/article")
                    .authenticated())
            .cors(this::configureCors)
            .csrf(ServerHttpSecurity.CsrfSpec::disable)
            .requestCache(requestCacheSpec -> requestCacheSpec.requestCache(NoOpServerRequestCache.getInstance()))
            .securityContextRepository(NoOpServerSecurityContextRepository.getInstance())
            .build();
}

public void configureCors(ServerHttpSecurity.CorsSpec corsSpec) {
    UrlBasedCorsConfigurationSource corsConfigurationSource = new UrlBasedCorsConfigurationSource();

    CorsConfiguration corsConfiguration = new CorsConfiguration();

    corsConfiguration.addAllowedOrigin(allowOrigin);
    corsConfiguration.addAllowedHeader("*");
    corsConfiguration.setAllowCredentials(true);
    corsConfiguration.addAllowedMethod("*");
    corsConfiguration.addExposedHeader(HttpHeaders.LOCATION);

    corsConfigurationSource.registerCorsConfiguration("/**", corsConfiguration);

    corsSpec.configurationSource(corsConfigurationSource);
}
}

Ответы (1 шт):

Автор решения: Saveliy

В ходи исследования как работают другие фильтры в цепочке было замечено что контекст хранится в reactor.util.context.Context, соответственно тот способ установки SecurityContext'а был неверен. Вот как правильно устанавливать:

@Component("jwtFilter")
public class JwtFilter implements WebFilter {
private final JwtTokenService jwtService;

private final AuthService authService;

@Autowired
public JwtFilter(JwtTokenService jwtService, AuthService authService) {
    this.jwtService = jwtService;
    this.authService = authService;
}

@Nonnull
@Override
public Mono<Void> filter(@Nonnull ServerWebExchange exchange, @Nonnull WebFilterChain chain) {
    return chain.filter(exchange)
            .contextWrite(context -> withSecurityContext(context, exchange));
}

private Context withSecurityContext(Context mainContext, ServerWebExchange exchange) {
    return mainContext.putAll(
            getMonoForContext(exchange)
                    .as(ReactiveSecurityContextHolder::withSecurityContext)
                    .readOnly());
}

private Mono<SecurityContextImpl> getMonoForContext(ServerWebExchange exchange) {
    return extractJwtCookie(exchange.getRequest())
            .map(this::checkFromCookieTokenAndGetAuth)
            .map(authentication -> Mono.just(new SecurityContextImpl(authentication)))
            .orElse(Mono.empty());
}

private Authentication checkFromCookieTokenAndGetAuth(HttpCookie tokenCookie) {
    jwtService.checkTokenExpired(tokenCookie.getValue());

    return authService.createAuthByJwt(tokenCookie.getValue());
}

private Optional<HttpCookie> extractJwtCookie(ServerHttpRequest request) {
    return Optional.ofNullable(request.getCookies().get("authToken"))
            .filter(httpCookies -> !httpCookies.isEmpty())
            .map(httpCookies -> httpCookies.get(0));
}
}
→ Ссылка