SpringSecurity Series 3Based on Spring Webflux and integrating SpringSecurity to realize the permission control of the stateless Rest API separated from the front and back ends

Source Code Portal:
https://github.com/ningzuoxin/zxning-springsecurity-demos/tree/master/T02-springsecurity-stateless-webflux

1. Foreword

Spring WebFlux is an asynchronous non-blocking web framework that can make full use of the hardware resources of multi-core CPUs to handle a large number of concurrent requests. Spring Security has customized a set of APIs for permission control specifically for Webflux, so integrating Spring Security in Webflux applications is still somewhat different from the integration of Spring Security in Web applications mentioned above. As usual, let’s look at the implementation steps first, and then analyze the principles later.

2. Implementation steps

1. Introduce dependencies

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.7.0</version>
</parent>

<dependencies>
    <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>cn.hutool</groupId>
        <artifactId>hutool-all</artifactId>
        <version>5.8.1</version>
    </dependency>
</dependencies>

2. Rewrite the /login request of the Get method

Like the integration of Spring Security with Spring Web, the integration of Spring Security with Spring Webflux also needs to rewrite the default Get method/login request, but there is a small point to pay attention to, which is to return Mono in Webflux. The source code is as follows:

@GetMapping(value = "/login")
public Mono<Result> login() {<!-- -->
    return Mono.just(Result.data(-1, "PLEASE LOGIN", "NO LOGIN"));
}

The Result class is a general response object defined, and the specific code can be found in the attached source code link.

3. Create authentication information storage AuthenticationRepository

In the actual production environment, we should store the authentication information in the cache or database. This is just a demonstration, and it is stored in memory. The specific code is as follows:

@Repository
public class AuthenticationRepository {<!-- -->

    private static ConcurrentHashMap<String, Authentication> authentications = new ConcurrentHashMap<>();

    public void add(String key, Authentication authentication) {<!-- -->
        authentications. put(key, authentication);
    }

    public Authentication get(String key) {<!-- -->
        return authentications. get(key);
    }

    public void delete(String key) {<!-- -->
        if (authentications. containsKey(key)) {<!-- -->
            authentications. remove(key);
        }
    }
}

4. Create authentication success handler TokenServerAuthenticationSuccessHandler and authentication failure handler TokenServerAuthenticationFailureHandler

For Webflux applications, Spring Security provides us with different authentication success interfaces ServerAuthenticationSuccessHandler and authentication failure processing interface ServerAuthenticationFailureHandler, we only need to implement these two interfaces, and then implement the business logic we need. The specific code is as follows:

@Component
public class TokenServerAuthenticationSuccessHandler implements ServerAuthenticationSuccessHandler {<!-- -->

    @Autowired
    private AuthenticationRepository authenticationRepository;

    @Override
    public Mono<Void> onAuthenticationSuccess(WebFilterExchange webFilterExchange, Authentication authentication) {<!-- -->
        String token = IdUtil. simpleUUID();
        authenticationRepository.add(token, authentication);

        Result<String> result = Result.data(token, "LOGIN SUCCESS");
        return ServerHttpResponseUtils.print(webFilterExchange.getExchange().getResponse(), result);
    }
}
@Component
public class TokenServerAuthenticationFailureHandler implements ServerAuthenticationFailureHandler {<!-- -->

    @Override
    public Mono<Void> onAuthenticationFailure(WebFilterExchange webFilterExchange, AuthenticationException exception) {<!-- -->
        Result<String> result = Result.data(-1, exception.getMessage(), "LOGIN FAILED");
        return ServerHttpResponseUtils.print(webFilterExchange.getExchange().getResponse(), result);
    }
}

ServerHttpResponseUtils is an encapsulated tool class that responds to the front end in JSON data format through ServerHttpResponse. For specific codes, please refer to the attached source code link.

5. Create exit success handler TokenServerLogoutSuccessHandler

@Component
public class TokenServerLogoutSuccessHandler implements ServerLogoutSuccessHandler {<!-- -->

    @Autowired
    private AuthenticationRepository authenticationRepository;

    @Override
    public Mono<Void> onLogoutSuccess(WebFilterExchange exchange, Authentication authentication) {<!-- -->
        String token = exchange.getExchange().getRequest().getHeaders().getFirst("token");
        if (StrUtil.isNotEmpty(token)) {<!-- -->
            authenticationRepository.delete(token);
        }

        Result<String> result = Result.data(200, "LOGOUT SUCCESS", "OK");
        return ServerHttpResponseUtils.print(exchange.getExchange().getResponse(), result);
    }
}

6. Create a non-access handler TokenServerAccessDeniedHandler

public class TokenServerAccessDeniedHandler implements ServerAccessDeniedHandler {<!-- -->

    @Override
    public Mono<Void> handle(ServerWebExchange exchange, AccessDeniedException denied) {<!-- -->
        Result<String> result = Result.data(403, denied.getMessage(), "ACCESS DENIED");
        return ServerHttpResponseUtils.print(exchange.getResponse(), result);
    }
}

7. Create a SpringSecurity context warehouse TokenServerSecurityContextRepository

Unlike Web application integration SpringSecurity, SpringSecurity does not provide a stateless SpringSecurity context access strategy for Webflux for the time being. Currently, the ServerSecurityContextRepository interface only has two implementation strategies: NoOpServerSecurityContextRepository (does not store SecurityContext) and WebSessionServerSecurityContextRepository (based on WebSession). Therefore, if we want to achieve stateless SpringSecurity context access, we need to implement the ServerSecurityContextRepository interface ourselves. The source code is as follows:

@Component
public class TokenServerSecurityContextRepository implements ServerSecurityContextRepository {<!-- -->

    @Autowired
    private AuthenticationRepository authenticationRepository;

    @Override
    public Mono<Void> save(ServerWebExchange exchange, SecurityContext context) {<!-- -->
        return Mono. empty();
    }

    @Override
    public Mono<SecurityContext> load(ServerWebExchange exchange) {<!-- -->
        String token = exchange.getRequest().getHeaders().getFirst("token");
        if (StrUtil.isNotEmpty(token)) {<!-- -->
            Authentication authentication = authenticationRepository. get(token);
            if (ObjectUtil. isNotEmpty(authentication)) {<!-- -->
                SecurityContextImpl securityContext = new SecurityContextImpl();
                securityContext.setAuthentication(authentication);
                return Mono. just(securityContext);
            }
        }
        return Mono. empty();
    }
}

8. Configure WebFluxSecurityConfig, this is the key point! ! !

Create the WebFluxSecurityConfig class and configure the SecurityWebFilterChain Bean object. For Webflux application SpringSecurity is to configure various properties through ServerHttpSecurity. The specific configuration is as follows:

// [Note] The annotations used in Webflux are different
@EnableWebFluxSecurity
@EnableReactiveMethodSecurity

@Bean
public MapReactiveUserDetailsService userDetailsService() {
    // Rights Profile
    List<GrantedAuthority> authorities = new ArrayList<>();
    authorities.add(new SimpleGrantedAuthority("index"));
    authorities.add(new SimpleGrantedAuthority("hasAuthority"));
    authorities.add(new SimpleGrantedAuthority("ROLE_hasRole"));

    // Certification Information
    UserDetails userDetails = User.builder().username("admin")
            .passwordEncoder(passwordEncoder()::encode)
            .password("123456")
            .authorities(authorities)
            .build();
    return new MapReactiveUserDetailsService(userDetails);
}

@Bean
public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) {
    // disable prevent csrf
    http.csrf(s -> s.disable())
        // Customize ServerSecurityContextRepository
        .securityContextRepository(tokenServerSecurityContextRepository)
        .formLogin(s -> s
            // Specify the login request url
            .loginPage("/login")
            // Configure authentication success handler
            .authenticationSuccessHandler(tokenServerAuthenticationSuccessHandler)
            // configure authentication failure handler
            .authenticationFailureHandler(tokenServerAuthenticationFailureHandler)
        )
        // Configure exit success handler
        .logout(s -> s.logoutSuccessHandler(tokenServerLogoutSuccessHandler))
        // Release the /login request, other requests must be authenticated
        .authorizeExchange(s -> s.pathMatchers("/login").permitAll().anyExchange().authenticated())
        // configure no access handler
        .exceptionHandling().accessDeniedHandler(new TokenServerAccessDeniedHandler());
    return http. build();
}

9. Create some API interfaces for testing

@RestController
public class IndexController {<!-- -->

    @RequestMapping(value = "/index")
    @PreAuthorize("hasAuthority('index')")
    public Mono<String> index() {<!-- -->
        return Mono. just("index");
    }

    @RequestMapping(value = "/hasAuthority")
    @PreAuthorize("hasAuthority('hasAuthority')")
    public Mono<String> hasAuthority() {<!-- -->
        return Mono. just("hasAuthority");
    }

    @RequestMapping(value = "/hasRole")
    @PreAuthorize("hasRole('hasRole')")
    public Mono<String> hasRole() {<!-- -->
        return Mono. just("hasRole");
    }

    @RequestMapping(value = "/home")
    @PreAuthorize("hasRole('home')")
    public Mono<String> home() {<!-- -->
        return Mono. just("home");
    }

}

3. Test

1. Access protected API without login

// request address GET request
http://localhost:8080/index

// curl
curl --location --request GET 'http://localhost:8080/index'

// response result
{<!-- -->
    "code": -1,
    "msg": "NO LOGIN",
    "time": 1654524412270,
    "data": "PLEASE LOGIN"
}

2. Login API

// Request address POST request [Note: The parameter format should be specified as x-www-form-urlencoded, and the username and password are obtained through getFormData in the source code]
http://localhost:8080/login

// curl
curl --location --request POST 'http://localhost:8080/login' \
--header 'Content-Type: application/x-www-form-urlencoded' \
--data-urlencode 'username=admin' \
--data-urlencode 'password=123456'

// response result
{<!-- -->
    "code": 200,
    "msg": "LOGIN SUCCESS",
    "time": 1654524449600,
    "data": "80b60ffc7e2b419f9a8e7d8dec355e02"
}

3. Carry token to access protected API

// request address GET request add authentication token in request header
http://localhost:8080/index

// curl
curl --location --request GET 'http://localhost:8080/index' --header 'token: 80b60ffc7e2b419f9a8e7d8dec355e02'

// response result
index

4. Carry token to access unauthorized API

// request address GET request add authentication token in request header
http://localhost:8080/home

// curl
curl --location --request GET 'http://localhost:8080/home' --header 'token: 612c29a2dd824191b6afe07a38285e81'

// response result
{
    "code": 403,
    "msg": "ACCESS DENIED",
    "time": 1654524759366,
    "data": "Denied"
}

5. Exit API

// Request address POST request Add authentication token to the request header [Note: It is a POST request, the source code exit matches the POST method /logout request]
http://localhost:8080/logout

// curl
curl --location --request POST 'http://localhost:8080/logout' --header 'token: 612c29a2dd824191b6afe07a38285e81'

// response result
{
    "code": 200,
    "msg": "OK",
    "time": 1654524806801,
    "data": "LOGOUT SUCCESS"
}

Fourth, summary

With the experience of integrating Spring Security based on Spring Web, and based on the idea of analogy, it is not difficult to realize the integration of Spring Security with Spring Webflux. After a simple transformation, it can basically meet the needs of front-end and back-end separation and stateless API permission control. However, there are two points that need to be further modified before being applied to the production environment:

1. Change the identity authentication and permission acquisition to obtain from the database.

2. Store the authenticated identity information in the cache or database.

In the next article, we will further analyze the implementation principle of Spring Webflux integrating SpringSecurity, please pay more attention~

[Put an advertisement] Recommend a personal SpringCloud-based open source project for your study and reference. Welcome to leave a message and enter the group for communication

Gitee: gitee.com/ningzxspace…

Github: github.com/ningzuoxin/…