Summary

I want to manually set Authentication to SecurityContext in a Spring Cloud Gateway application, but ReactiveSecurityContextHolder.getContext always resolve to empty. The full source code can be found at Spring Cloud in Practice.

package net.jaggerwang.scip.gateway.adapter.controller;

...

@RestController
@RequestMapping("/auth")
public class AuthController {
    private UserAsyncService userAsyncService;
    private ReactiveAuthenticationManager authManager;

    public AuthController(UserAsyncService userAsyncService, ReactiveAuthenticationManager authManager) {
        this.userAsyncService = userAsyncService;
        this.authManager = authManager;
    }

    @PostMapping("/login")
    public Mono<RootDto> login(@RequestBody UserDto userDto) {
        return authManager.authenticate(new UsernamePasswordAuthenticationToken(
                userDto.getUsername(), userDto.getPassword()))
                .flatMap(auth -> ReactiveSecurityContextHolder.getContext()
                        .flatMap(context -> {
                            context.setAuthentication(auth);
                            var loggedUser = (LoggedUser) auth.getPrincipal();
                            return userAsyncService.info(loggedUser.getId());
                        })
                        .map(user -> new RootDto().addDataEntry("user", user))
                        .defaultIfEmpty(new RootDto().addDataEntry("user", null)));
    }
}

Configuration

package net.jaggerwang.scip.gateway.api.config;

@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true, jsr250Enabled = true)
@EnableWebFluxSecurity
public class SecurityConfig {
    private ObjectMapper objectMapper;
    private ReactiveUserDetailsService userDetailsService;

    public SecurityConfig(ObjectMapper objectMapper, ReactiveUserDetailsService userDetailsService) {
        this.objectMapper = objectMapper;
        this.userDetailsService = userDetailsService;
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Bean
    public ReactiveAuthenticationManager authManager(
            ) {
        var authManager = new UserDetailsRepositoryReactiveAuthenticationManager(userDetailsService);
        authManager.setPasswordEncoder(passwordEncoder());
        return authManager;
    }

    private Mono<Void> responseJson(ServerWebExchange exchange, HttpStatus status, RootDto data) {
        var response = exchange.getResponse();
        response.setStatusCode(status);
        response.getHeaders().setContentType(MediaType.APPLICATION_JSON);
        var body = new byte[0];
        try {
            body = objectMapper.writeValueAsBytes(data);
        } catch (IOException e) {
        }
        return response.writeWith(Flux.just(response.bufferFactory().wrap(body)));
    }

    @Bean
    public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
        return http
                .csrf(csrf -> csrf.disable())
                .authenticationManager(authManager())
                .exceptionHandling(exceptionHandling -> exceptionHandling
                        .authenticationEntryPoint((exchange, exception) -> {
                            if (exchange.getRequest().getHeaders().getAccept()
                                    .contains(MediaType.APPLICATION_JSON)) {
                                return responseJson(exchange, HttpStatus.UNAUTHORIZED,
                                        new RootDto("unauthenticated", "未认证"));
                            } else {
                                var response = exchange.getResponse();
                                response.setStatusCode(HttpStatus.FOUND);
                                response.getHeaders().setLocation(
                                        UriComponentsBuilder.fromPath("/login").build().toUri());
                                return response.writeWith(Flux.just(
                                        response.bufferFactory().wrap("".getBytes())));
                            }
                        })
                        .accessDeniedHandler((exchange, accessDeniedException) -> {
                            if (exchange.getRequest().getHeaders().getAccept()
                                    .contains(MediaType.APPLICATION_JSON)) {
                                return responseJson(exchange, HttpStatus.FORBIDDEN,
                                        new RootDto("unauthorized", "未授权"));
                            } else {
                                var response = exchange.getResponse();
                                response.setStatusCode(HttpStatus.FORBIDDEN);
                                return response.writeWith(Flux.just(
                                        response.bufferFactory().wrap("未授权".getBytes())));
                            }
                        })
                )
                .authorizeExchange(authorizeExchange -> authorizeExchange
                        .pathMatchers("/favicon.ico", "/*/actuator/**", "/login", "/logout",
                                "/auth/login", "/auth/logout", "/auth/logged",
                                "/user/register").permitAll()
                        .anyExchange().authenticated())
                .formLogin(formLogin -> {})
                .logout(logout -> {})
                .build();
    }
}

Version

Spring Boot: 2.2.2.RELEASE Spring Cloud: Hoxton.SR1

<?xml version="1.0" ?>
<project xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd" xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
    <modelVersion>4.0.0</modelVersion>

    <parent>
        <groupId>net.jaggerwang</groupId>
        <artifactId>spring-cloud-in-practice</artifactId>
        <version>1.0.0-SNAPSHOT</version>
    </parent>

    <groupId>net.jaggerwang</groupId>
    <artifactId>spring-cloud-in-practice-gateway</artifactId>
    <version>1.0.0-SNAPSHOT</version>
    <name>spring-cloud-in-practice-gateway</name>
    <description>Spring cloud in practice gateway</description>

    <dependencies>
        <dependency>
            <groupId>net.jaggerwang</groupId>
            <artifactId>spring-cloud-in-practice-common</artifactId>
            <version>${scip-common.version}</version>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-webflux</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <scope>runtime</scope>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-configuration-processor</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
            <exclusions>
                <exclusion>
                    <groupId>org.junit.vintage</groupId>
                    <artifactId>junit-vintage-engine</artifactId>
                </exclusion>
            </exclusions>
        </dependency>

        <dependency>
            <groupId>org.springframework.session</groupId>
            <artifactId>spring-session-data-redis</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-gateway</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-consul-discovery</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-circuitbreaker-reactor-resilience4j</artifactId>
        </dependency>

        <dependency>
            <groupId>com.graphql-java</groupId>
            <artifactId>graphql-java-spring-boot-starter-webflux</artifactId>
            <version>${graphql-starter.version}</version>
        </dependency>
        <dependency>
            <groupId>com.graphql-java</groupId>
            <artifactId>graphql-java-extended-scalars</artifactId>
            <version>${graphql-extended-scalars.version}</version>
        </dependency>

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>

        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-pool2</artifactId>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>

Comment From: rwinch

Thanks for getting in touch, but it feels like this is a question that would be better suited to Stack Overflow. As mentioned in the guidelines for contributing, we prefer to use GitHub issues only for bugs and enhancements. Feel free to update this issue with a link to the re-posted question (so that other people can find it) or add some more details if you feel this is a genuine bug.

Comment From: jaggerwang

Figured it out by myself. Create a new SecurityContextImpl instance when ReactiveSecurityContextHolder.getContext() resolve to empty, and save the new instance to session.

package net.jaggerwang.scip.gateway.adapter.controller;

...

abstract public class AbstractController {
    @Autowired
    private ReactiveAuthenticationManager authManager;

    protected Mono<ServerWebExchange> getServerWebExchange() {
        return Mono.subscriberContext()
                .map(ctx -> ctx.get(ServerWebExchange.class));
    }

    protected Mono<WebSession> getWebSession() {
        return getServerWebExchange()
                .flatMap(exchange -> exchange.getSession());
    }

    protected Mono<SecurityContext> getSecurityContext() {
        return getWebSession()
                .flatMap(session -> ReactiveSecurityContextHolder.getContext()
                        .switchIfEmpty(Mono.fromSupplier(() -> {
                            var context = new SecurityContextImpl();
                            session.getAttributes().put(
                                    DEFAULT_SPRING_SECURITY_CONTEXT_ATTR_NAME, context);
                            return context;
                        })));
    }

    protected Mono<LoggedUser> loginUser(String username, String password) {
        return authManager.authenticate(new UsernamePasswordAuthenticationToken(username, password))
                .flatMap(auth -> getSecurityContext()
                        .flatMap(context -> getWebSession()
                                .map(session -> {
                                    context.setAuthentication(auth);
                                    session.getAttributes().put(
                                            DEFAULT_SPRING_SECURITY_CONTEXT_ATTR_NAME, context);
                                    return (LoggedUser) auth.getPrincipal();
                                })));
    }

    protected Mono<LoggedUser> logoutUser() {
        return loggedUser()
                .flatMap(loggedUser -> getSecurityContext()
                        .flatMap(context -> getWebSession()
                                .map(session -> {
                                    context.setAuthentication(null);
                                    session.getAttributes().remove(
                                            DEFAULT_SPRING_SECURITY_CONTEXT_ATTR_NAME);
                                    return loggedUser;
                                })));
    }
}