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/…