源码传送门:
https://github.com/ningzuoxin/zxning-springsecurity-demos/tree/master/T02-springsecurity-stateless-webflux
一、前语
Spring WebFlux 是一个异步非阻塞式的 Web 框架,它可以充分利用多核 CPU 的硬件资源去处理很多的并发恳求。SpringSecurity 专门为 Webflux 定制了一套用于权限操控的 API,因而在 Webflux 使用中集成
SpringSecurity,和前面讲的 Web 使用集成 SpringSecurity 还是有必定区别。老规矩,咱们先看完成步骤,后续再来剖析原理。
二、完成步骤
1、引进依靠
<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、改写 Get 办法的 /login 恳求
和 Spring Web 集成 SpringSecurity 相同,Spring Webflux 集成 SpringSecurity 也需要改写默许的 Get 办法 /login 恳求,但是有个小地方要留意下,就是在 Webflux 中要回来 Mono。源码如下:
@GetMapping(value = "/login")
public Mono<Result> login() {
return Mono.just(Result.data(-1, "PLEASE LOGIN", "NO LOGIN"));
}
Result 类是定义的一个通用呼应目标,详细代码可检查附上的源码链接。
3、创立认证信息存储器 AuthenticationRepository
在实践出产环境中,咱们应该把认证信息存储在缓存或者数据库中,此处只是演示,就放在内存中了。详细代码如下:
@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、创立认证成功处理器 TokenServerAuthenticationSuccessHandler 和认证失败处理器 TokenServerAuthenticationFailureHandler
关于 Webflux 使用 SpringSecurity 为咱们供给了不同的认证成功接口 ServerAuthenticationSuccessHandler 和 认证失败处理接口 ServerAuthenticationFailureHandler,咱们只需要完成这两个接口,然后完成咱们需要的业务逻辑即可,详细代码如下:
@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 是封装的一个经过 ServerHttpResponse 向前端呼应 JSON 数据格式的东西类,详细代码可检查附上的源码链接。
5、创立退出成功处理器 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、创立无拜访权限处理器 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、创立 SpringSecurity 上下文库房 TokenServerSecurityContextRepository
和 Web 使用集成 SpringSecurity 不同的是,SpringSecurity 暂时没有为 Webflux 供给无状况的 SpringSecurity 上下文存取战略。现在 ServerSecurityContextRepository 接口暂时只要
NoOpServerSecurityContextRepository(不存储 SecurityContext) 和 WebSessionServerSecurityContextRepository (依据 WebSession)两种完成战略。因而,咱们要想完成无状况的
SpringSecurity 上下文存取,需要咱们自己去完成 ServerSecurityContextRepository 接口。源码如下:
@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、装备 WebFluxSecurityConfig,这是重点!!!
创立 WebFluxSecurityConfig 类,并装备 SecurityWebFilterChain Bean目标,关于 Webflux 使用 SpringSecurity 是经过 ServerHttpSecurity 装备各项属性,详细装备如下:
// 【留意】Webflux 中使用的注解是不相同的哦
@EnableWebFluxSecurity
@EnableReactiveMethodSecurity
@Bean
public MapReactiveUserDetailsService userDetailsService() {
// 权限装备
List<GrantedAuthority> authorities = new ArrayList<>();
authorities.add(new SimpleGrantedAuthority("index"));
authorities.add(new SimpleGrantedAuthority("hasAuthority"));
authorities.add(new SimpleGrantedAuthority("ROLE_hasRole"));
// 认证信息
UserDetails userDetails = User.builder().username("admin")
.passwordEncoder(passwordEncoder()::encode)
.password("123456")
.authorities(authorities)
.build();
return new MapReactiveUserDetailsService(userDetails);
}
@Bean
public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) {
// 禁用避免 csrf
http.csrf(s -> s.disable())
// 自定义 ServerSecurityContextRepository
.securityContextRepository(tokenServerSecurityContextRepository)
.formLogin(s -> s
// 指定登录恳求url
.loginPage("/login")
// 装备认证成功处理器
.authenticationSuccessHandler(tokenServerAuthenticationSuccessHandler)
// 装备认证失败处理器
.authenticationFailureHandler(tokenServerAuthenticationFailureHandler)
)
// 装备退出成功处理器
.logout(s -> s.logoutSuccessHandler(tokenServerLogoutSuccessHandler))
// 放行 /login 恳求,其他恳求有必要经过认证
.authorizeExchange(s -> s.pathMatchers("/login").permitAll().anyExchange().authenticated())
// 装备无拜访权限处理器
.exceptionHandling().accessDeniedHandler(new TokenServerAccessDeniedHandler());
return http.build();
}
9、创立一些测试用的 API 接口
@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");
}
}
三、测试
1、未登录拜访受保护 API
// 恳求地址 GET恳求
http://localhost:8080/index
// curl
curl --location --request GET 'http://localhost:8080/index'
// 呼应成果
{
"code": -1,
"msg": "NO LOGIN",
"time": 1654524412270,
"data": "PLEASE LOGIN"
}
2、登录 API
// 恳求地址 POST恳求 【留意:参数格式要指定为 x-www-form-urlencoded,源码中是经过 getFormData 获取 username 和 password】
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'
// 呼应成果
{
"code": 200,
"msg": "LOGIN SUCCESS",
"time": 1654524449600,
"data": "80b60ffc7e2b419f9a8e7d8dec355e02"
}
3、带着 token 拜访受保护 API
// 恳求地址 GET恳求 恳求头中增加认证 token
http://localhost:8080/index
// curl
curl --location --request GET 'http://localhost:8080/index' --header 'token: 80b60ffc7e2b419f9a8e7d8dec355e02'
// 呼应成果
index
4、带着 token 拜访未授权 API
// 恳求地址 GET恳求 恳求头中增加认证 token
http://localhost:8080/home
// curl
curl --location --request GET 'http://localhost:8080/home' --header 'token: 612c29a2dd824191b6afe07a38285e81'
// 呼应成果
{
"code": 403,
"msg": "ACCESS DENIED",
"time": 1654524759366,
"data": "Denied"
}
5、退出 API
// 恳求地址 POST恳求 恳求头中增加认证 token【留意:是 POST 恳求,源码中退出匹配的是 POST 办法 /logout 恳求】
http://localhost:8080/logout
// curl
curl --location --request POST 'http://localhost:8080/logout' --header 'token: 612c29a2dd824191b6afe07a38285e81'
// 呼应成果
{
"code": 200,
"msg": "OK",
"time": 1654524806801,
"data": "LOGOUT SUCCESS"
}
四、总结
有了依据 Spring Web 集成 SpringSecurity 经验,依据类比的思想,完成 Spring Webflux 集成 SpringSecurity 不算困难。经过简单的改造之后,基本能满意前后端分离无状况 API 权限操控的需求。但是,在使用于出产环境前,有两点需要进一步改造:
1、将身份认证和权限获取,改为从数据库中获取。
2、将经过认证的身份信息存储在缓存或数据库中。
鄙人一篇,咱们将进一步剖析 Spring Webflux 集成 SpringSecurity 的完成原理,我们多多重视哦~
【打个广告】推荐下个人的依据 SpringCloud 开源项目,供我们学习参考,欢迎我们留言进群交流
Gitee:gitee.com/ningzxspace…
Github:github.com/ningzuoxin/…