原始碼傳送門:
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