Mobile projects, authentication requirements are relatively simple, Spring Cloud Gateway is only responsible for JWT verification and role authentication, login and so on are all custom processing, microservices pass JWS to achieve the purpose of passing credentials, downstream services do not need to authenticate and do not rely on Spring Security, the code that requires the current user directly resolve JWS to get the current user

There is also the fact that WebSecurityConfigurerAdapter has been marked as deprecated in Spring Security 5.4+ and bean injection has been recommended for non-reactive projects, so the following code can also be considered a migration guide for 5.4+

Core Code:

JwsService provides JWS signature and checksum, returns the corresponding JwsPayload object, JwsPayload is a POJO, provides the current principal, contains a List<Authority> authoritys property, Authority is a POJO, because is placed in the lib module, does not import Spring Security dependencies, and inherits directly from Object, which can also be seen in the following code manually converted to GrantedAuthority.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
package com.seliote.bubble.gwsvr.security;

import com.seliote.bubble.svrlib.config.JwsProps;
import com.seliote.bubble.svrlib.service.JwsService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.lang.NonNull;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.context.ReactiveSecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.WebFilter;
import org.springframework.web.server.WebFilterChain;
import reactor.core.publisher.Mono;

import java.util.ArrayList;
import java.util.List;

/**
 * JWS Filter
 * If authorization header exists, will set to security context
 *
 * @author seliote
 * @since 2022-07-06
 */
@Slf4j
@Component
public class JwsFilter implements WebFilter {

    private final JwsService jwsService;
    private final String headerName;

    @Autowired
    public JwsFilter(JwsService jwsService, JwsProps jwsProps) {
        this.jwsService = jwsService;
        this.headerName = jwsProps.getHeader();
    }

    @NonNull
    @Override
    public Mono<Void> filter(@NonNull ServerWebExchange exchange, @NonNull WebFilterChain chain) {
        var header = exchange.getRequest().getHeaders().getFirst(headerName);
        var payloadOpt = jwsService.verify(header);
        if (payloadOpt.isPresent() && payloadOpt.get().available()) {
            var payload = payloadOpt.get();
            List<? extends GrantedAuthority> authorities = new ArrayList<>();
            if (payload.getAuthorities() != null && payload.getAuthorities().size() != 0) {
                authorities = payload.getAuthorities().stream().map(r -> (GrantedAuthority) r::getAuthority).toList();
            }
            var authentication = new UsernamePasswordAuthenticationToken(payload, null, authorities);
            log.trace("Set security context {}", payload);
            return chain.filter(exchange)
                    .contextWrite(ReactiveSecurityContextHolder.withAuthentication(authentication));
        }
        return chain.filter(exchange);
    }
}

Because it is fully customizable to implement login and other operations, an empty ReactiveAuthenticationManager is provided, which corresponds to the default implementation of authenticationManager() in MVC.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
package com.seliote.bubble.gwsvr.security;

import com.seliote.bubble.svrlib.domain.Authority;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.security.authentication.ReactiveAuthenticationManager;
import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity;
import org.springframework.security.config.web.server.SecurityWebFiltersOrder;
import org.springframework.security.config.web.server.ServerHttpSecurity;
import org.springframework.security.web.server.SecurityWebFilterChain;
import reactor.core.publisher.Mono;

/**
 * Security context config
 *
 * @author seliote
 * @since 2022-07-06
 */
@Slf4j
@EnableWebFluxSecurity
public class SecurityConfig {

    private final JwsFilter jwsFilter;

    @Autowired
    public SecurityConfig(JwsFilter jwsFilter) {
        this.jwsFilter = jwsFilter;
    }

    @Bean
    public ReactiveAuthenticationManager authenticationManager() {
        return authentication -> Mono.empty();
    }

    @Bean
    public SecurityWebFilterChain filterChain(ServerHttpSecurity http) {
        final var permitAll = new String[]{"/user-svr/country/list"};
        return http.csrf().disable()
                .httpBasic().disable()
                .formLogin().disable()
                .authorizeExchange().pathMatchers(permitAll).permitAll()
                .pathMatchers("/**").hasAuthority(Authority.USER)
                .anyExchange().authenticated()
                .and().addFilterAt(jwsFilter, SecurityWebFiltersOrder.AUTHENTICATION)
                .build();
    }
}

Reference: https://seliote.fun/2023/02/09/spring-webflux-%e6%94%af%e6%8c%81-spring-security-%e5%ae%9e%e7%8e%b0-jwt-%e9%89%b4%e6%9d%83/