【SpringSecurity系列3】基于Spring Webflux集成SpringSecurity实现前后端分离无状态Rest API的权限控制
源码传送门:
https://github.com/ningzuoxin/zxning-springsecurity-demos/tree/master/02-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:https://gitee.com/ningzxspace/exam-ning-springcloud-v1
Github:https://github.com/ningzuoxin/exam-ning-springcloud-v1
【SpringSecurity系列3】基于Spring Webflux集成SpringSecurity实现前后端分离无状态Rest API的权限控制的更多相关文章
- 【SpringSecurity系列1】基于SpringSecurity实现前后端分离无状态Rest API的权限控制
源码传送门: https://github.com/ningzuoxin/zxning-springsecurity-demos/tree/master/01-springsecurity-state ...
- 【SpringSecurity系列2】基于SpringSecurity实现前后端分离无状态Rest API的权限控制原理分析
源码传送门: https://github.com/ningzuoxin/zxning-springsecurity-demos/tree/master/01-springsecurity-state ...
- 用Spring Security, JWT, Vue实现一个前后端分离无状态认证Demo
简介 完整代码 https://github.com/PuZhiweizuishuai/SpringSecurity-JWT-Vue-Deom 运行展示 后端 主要展示 Spring Security ...
- SpringBoot使用SpringSecurity搭建基于非对称加密的JWT及前后端分离的搭建
SpringBoot使用SpringSecurity搭建基于非对称加密的JWT及前后端分离的搭建 - lhc0512的博客 - CSDN博客 https://blog.csdn.net/lhc0512 ...
- Spring Boot + Vue + Shiro 实现前后端分离、权限控制
本文总结自实习中对项目的重构.原先项目采用Springboot+freemarker模版,开发过程中觉得前端逻辑写的实在恶心,后端Controller层还必须返回Freemarker模版的ModelA ...
- Spring WebFlux 学习笔记 - (一) 前传:学习Java 8 Stream Api (1) - 创建 Stream
影子 在学习Spring WebFlux之前,我们先来了解JDK的Stream,虽然他们之间没有直接的关系,有趣的是 Spring Web Flux 基于 Reactive Stream,他们中都带了 ...
- Spring WebFlux 学习笔记 - (一) 前传:学习Java 8 Stream Api (3) - Stream的终端操作
Stream API Java8中有两大最为重要的改变:第一个是 Lambda 表达式:另外一个则是 Stream API(java.util.stream.*). Stream 是 Java8 中处 ...
- Spring WebFlux 学习笔记 - (一) 前传:学习Java 8 Stream Api (2) - Stream的中间操作
Stream API Java8中有两大最为重要的改变:第一个是 Lambda 表达式:另外一个则是 Stream API(java.util.stream.*). Stream 是 Java8 中处 ...
- 企业快速开发平台Spring Cloud+Spring Boot+Mybatis+ElementUI 实现前后端分离
鸿鹄云架构一系统管理平台 鸿鹄云架构[系统管理平台]使用J2EE技术来实施,是一个大型分布式的面向服务的JavaEE体系快速研发平台,基于模块化.服务化.原子化.热部署的设计思想,使用成熟领先的无商业 ...
随机推荐
- vue-cropper裁剪上传
效果图: 全部代码: npm install vue-cropper //首先 安装vue-cropper main.js全局引用: import VueCropper from 'vue-cropp ...
- apache开启图片缓存压缩
①-浏览器缓存图片信息 开启Apache的expires模块,重启Apache 2.在虚拟主机的配置文件里面,增加对图片信息缓存的配置,重启Apache 3.在网站目录里面填写测试代码 4.测试效果 ...
- Make-learning
Make学习笔记 make是工具,Makefile是指导make工作的文件,而CMake则是生成Makefile的工具 要点: 终极目标是Makefile里面的第一个规则目标 目标下面的命令必须接的是 ...
- 整合SSM框架环境搭建
知识要求 MySQL相关操作 Maven操作 Mybatis.Spring.SpringMVC三个框架基本操作 JavaWeb等知识 搭建环境 MySQL 8.0 Mybatis 3.5.2 使用c3 ...
- IDEA 2022.2.1 Beta 2发布:新增支持Java 18、增强JUnit 5的支持
近日,IDEA 2022.1的Beta 2版本发布了!下面我们一起来看看对于我们Java开发者来说,有哪些重要的更新内容. Java增强 随着Java 18的正式发布,IDEA也在该版本中迅速跟进.目 ...
- 使用基于Roslyn的编译时AOP框架来解决.NET项目的代码复用问题
理想的代码优化方式 团队日常协作中,自然而然的会出现很多重复代码,根据这些代码的种类,之前可能会以以下方式处理 方式 描述 应用时可能产生的问题 硬编码 多数新手,或逐渐腐坏的项目会这么干,会直接复制 ...
- C++五子棋(六&七)——游戏结束
规则原理 如图 判断游戏结束 chessData.h //row,col 表示当前落子 bool checkWin(ChessData* game, int row, int col); 横.竖.斜( ...
- 如何为我的VUE项目编写高效的单元测试--Jest
Unit Testing(单元测试)--Jest 一个完整的测试程序通常由几种不同的测试组合而成,比如end to end(E2E)测试,有时还包括整体测试.简要测试和单元测试.这里要介绍的是Vue中 ...
- 基于Composer的Laravel扩展包开发工作流 ,实现laravle项目的文件管理(记录成长)
PHP Composer包开发 基于Composer的Laravel扩展包开发工作流 实现laravle项目的文件管理,添加文件/文件夹,删除文件,查看代码/文件(代码支持缩进,支持语法高亮) com ...
- 【FAQ】干货满满,接入HMS Core应用内支付服务过程中一些常见问题总结(2)来啦
HMS Core应用内支付服务(In-App Purchases,IAP)为应用提供便捷的应用内支付体验和简便的接入流程.该服务支持客户端和服务端两种开发形式,具体可以参考官方文档 上次,我们分享和总 ...