refactor: 重构所有 starter 组件的 SaTokenContext 上下文读写策略

This commit is contained in:
click33
2025-04-06 23:22:01 +08:00
parent 36cc99a70c
commit 55f0c94aec
92 changed files with 1538 additions and 1234 deletions

View File

@@ -23,7 +23,7 @@
<artifactId>spring-boot-starter</artifactId>
<optional>true</optional>
</dependency>
<!-- spring-web (optional) -->
<dependency>
<groupId>org.springframework</groupId>
@@ -31,11 +31,10 @@
<optional>true</optional>
</dependency>
<!-- reactor-core (optional) -->
<!-- reactor-core -->
<dependency>
<groupId>io.projectreactor</groupId>
<artifactId>reactor-core</artifactId>
<optional>true</optional>
</dependency>
<!-- jackson-databind (optional) -->
@@ -51,36 +50,25 @@
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
<!-- sa-token-core -->
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-core</artifactId>
</dependency>
<!-- sa-token-spring-boot-autoconfig -->
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-spring-boot-autoconfig</artifactId>
</dependency>
<!-- <dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
<version>${springboot.version}</version>
</dependency> -->
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-web</artifactId>
<version>5.3.7</version>
<version>5.3.39</version>
</dependency>
</dependencies>
</dependencyManagement>

View File

@@ -15,9 +15,12 @@
*/
package cn.dev33.satoken.reactor.context;
import cn.dev33.satoken.fun.SaRetGenericFunction;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.WebFilterChain;
import reactor.core.publisher.Mono;
import reactor.util.context.Context;
import reactor.util.context.ContextView;
/**
* Reactor 上下文操作(异步),持有当前请求的 ServerWebExchange 全局引用
@@ -28,37 +31,67 @@ import reactor.core.publisher.Mono;
public class SaReactorHolder {
/**
* key
* ServerWebExchange key
*/
public static final Class<ServerWebExchange> CONTEXT_KEY = ServerWebExchange.class;
public static final String EXCHANGE_KEY = "SA_REACTOR_EXCHANGE_KEY";
/**
* chain_key
* WebFilterChain key
*/
public static final String CHAIN_KEY = "WEB_FILTER_CHAIN_KEY";
public static final String CHAIN_KEY = "SA_REACTOR__CHAIN_KEY";
/**
* 获取上下文对象
* @return see note
* 在流式上下文写入 ServerWebExchange
* @param ctx 必填
* @param exchange 必填
* @param chain 非必填
* @return /
*/
public static Mono<ServerWebExchange> getContext() {
// 从全局 Mono<Context> 获取
return Mono.subscriberContext().map(ctx -> ctx.get(CONTEXT_KEY));
public static Context setContext(Context ctx, ServerWebExchange exchange, WebFilterChain chain) {
return ctx
.put(EXCHANGE_KEY, exchange)
.put(CHAIN_KEY, chain);
}
/**
* 获取上下文对象, 并设置到同步上下文中
* @return see note
* 在流式上下文获取 ServerWebExchange
* @param ctx /
* @return /
*/
public static Mono<ServerWebExchange> getContextAndSetSync() {
// 从全局 Mono<Context> 获取
return Mono.subscriberContext().map(ctx -> {
// 设置到sync中
SaReactorSyncHolder.setContext(ctx.get(CONTEXT_KEY));
return ctx.get(CONTEXT_KEY);
}).doFinally(r->{
// 从sync中清除
SaReactorSyncHolder.clearContext();
public static ServerWebExchange getExchange(ContextView ctx) {
return ctx.get(EXCHANGE_KEY);
}
/**
* 在流式上下文获取 WebFilterChain
* @param ctx /
* @return /
*/
public static WebFilterChain getChain(ContextView ctx) {
return ctx.get(CHAIN_KEY);
}
/**
* 获取 Mono < ServerWebExchange >
* @return /
*/
public static Mono<ServerWebExchange> getMonoExchange() {
return Mono.deferContextual(ctx -> Mono.just(getExchange(ctx)));
}
/**
* 将 exchange 写入到同步上下文中,并执行一段代码,执行完毕清除上下文
*
* @return /
*/
public static <R> Mono<R> sync(SaRetGenericFunction<R> fun) {
return Mono.deferContextual(ctx -> {
try {
SaReactorSyncHolder.setContext(ctx.get(EXCHANGE_KEY));
return Mono.just(fun.run());
} finally {
SaReactorSyncHolder.clearContext();
}
});
}

View File

@@ -15,17 +15,16 @@
*/
package cn.dev33.satoken.reactor.context;
import org.springframework.web.server.ServerWebExchange;
import cn.dev33.satoken.context.SaTokenContextForThreadLocalStorage;
import cn.dev33.satoken.context.SaTokenContextForThreadLocalStorage.Box;
import cn.dev33.satoken.context.model.SaRequest;
import cn.dev33.satoken.context.model.SaResponse;
import cn.dev33.satoken.context.model.SaStorage;
import cn.dev33.satoken.fun.SaFunction;
import cn.dev33.satoken.fun.SaRetGenericFunction;
import cn.dev33.satoken.reactor.model.SaRequestForReactor;
import cn.dev33.satoken.reactor.model.SaResponseForReactor;
import cn.dev33.satoken.reactor.model.SaStorageForReactor;
import org.springframework.web.server.ServerWebExchange;
/**
* Reactor上下文操作同步持有当前请求的 ServerWebExchange 全局引用
@@ -36,8 +35,8 @@ import cn.dev33.satoken.reactor.model.SaStorageForReactor;
public class SaReactorSyncHolder {
/**
* 写入上下文对象
* @param exchange see note
* 在同步上下文写入 ServerWebExchange
* @param exchange /
*/
public static void setContext(ServerWebExchange exchange) {
SaRequest request = new SaRequestForReactor(exchange.getRequest());
@@ -45,32 +44,32 @@ public class SaReactorSyncHolder {
SaStorage storage = new SaStorageForReactor(exchange);
SaTokenContextForThreadLocalStorage.setBox(request, response, storage);
}
/**
* 获取上下文对象
* @return see note
*/
public static ServerWebExchange getContext() {
Box box = SaTokenContextForThreadLocalStorage.getBoxNotNull();
return (ServerWebExchange)box.getStorage().getSource();
}
/**
* 清除上下文对象
* 在同步上下文清除 ServerWebExchange
*/
public static void clearContext() {
SaTokenContextForThreadLocalStorage.clearBox();
}
/**
* 写入上下文对象, 并在执行函数后将其清除
* @param exchange see note
* @param fun see note
* 在同步上下文获取 ServerWebExchange
* @return /
*/
public static void setContext(ServerWebExchange exchange, SaFunction fun) {
public static ServerWebExchange getExchange() {
Box box = SaTokenContextForThreadLocalStorage.getBoxNotNull();
return (ServerWebExchange)box.getStorage().getSource();
}
/**
* 将 exchange 写入到同步上下文中,并执行一段代码,执行完毕清除上下文
* @param exchange /
* @param fun /
*/
public static <R>R setContext(ServerWebExchange exchange, SaRetGenericFunction<R> fun) {
try {
setContext(exchange);
fun.run();
return fun.run();
} finally {
clearContext();
}

View File

@@ -1,35 +0,0 @@
/*
* Copyright 2020-2099 sa-token.cc
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package cn.dev33.satoken.reactor.error;
/**
* 定义 sa-token-reactor-spring-boot-starter 所有异常细分状态码
*
* @author click33
* @since 1.33.0
*/
public interface SaReactorSpringBootErrorCode {
/** 对象转 JSON 字符串失败 */
int CODE_20203 = 20203;
/** JSON 字符串转 Map 失败 */
int CODE_20204 = 20204;
/** 默认的 Filter 异常处理函数 */
int CODE_20205 = 20205;
}

View File

@@ -15,10 +15,13 @@
*/
package cn.dev33.satoken.reactor.filter;
import cn.dev33.satoken.exception.BackResultException;
import cn.dev33.satoken.exception.FirewallCheckException;
import cn.dev33.satoken.exception.StopMatchException;
import cn.dev33.satoken.reactor.context.SaReactorSyncHolder;
import cn.dev33.satoken.reactor.model.SaRequestForReactor;
import cn.dev33.satoken.reactor.model.SaResponseForReactor;
import cn.dev33.satoken.reactor.util.SaReactorOperateUtil;
import cn.dev33.satoken.strategy.SaFirewallStrategy;
import cn.dev33.satoken.util.SaTokenConsts;
import org.springframework.core.annotation.Order;
@@ -43,21 +46,25 @@ public class SaFirewallCheckFilterForReactor implements WebFilter {
SaResponseForReactor saResponse = new SaResponseForReactor(exchange.getResponse());
try {
SaReactorSyncHolder.setContext(exchange);
SaFirewallStrategy.instance.check.execute(saRequest, saResponse, exchange);
}
catch (StopMatchException e) {
// 如果是 StopMatchException 异常,代表通过了防火墙验证,进入 Controller
catch (StopMatchException ignored) {}
catch (BackResultException e) {
return SaReactorOperateUtil.writeResult(exchange, e.getMessage());
}
// FirewallCheckException 异常则交由异常处理策略处理
catch (FirewallCheckException e) {
// FirewallCheckException 异常则交由异常处理策略处理
if(SaFirewallStrategy.instance.checkFailHandle == null) {
exchange.getResponse().getHeaders().set(SaTokenConsts.CONTENT_TYPE_KEY, SaTokenConsts.CONTENT_TYPE_TEXT_PLAIN);
return exchange.getResponse().writeWith(Mono.just(exchange.getResponse().bufferFactory().wrap(e.getMessage().getBytes())));
return SaReactorOperateUtil.writeResult(exchange, e.getMessage());
} else {
SaFirewallStrategy.instance.checkFailHandle.run(e, saRequest, saResponse, null);
return Mono.empty();
}
}
finally {
SaReactorSyncHolder.clearContext();
}
// 更多异常则不处理,交由 Web 框架处理
// 向下执行

View File

@@ -21,9 +21,8 @@ import cn.dev33.satoken.exception.StopMatchException;
import cn.dev33.satoken.filter.SaFilter;
import cn.dev33.satoken.filter.SaFilterAuthStrategy;
import cn.dev33.satoken.filter.SaFilterErrorStrategy;
import cn.dev33.satoken.reactor.context.SaReactorHolder;
import cn.dev33.satoken.reactor.context.SaReactorSyncHolder;
import cn.dev33.satoken.reactor.error.SaReactorSpringBootErrorCode;
import cn.dev33.satoken.reactor.util.SaReactorOperateUtil;
import cn.dev33.satoken.router.SaRouter;
import cn.dev33.satoken.util.SaTokenConsts;
import org.springframework.core.annotation.Order;
@@ -37,7 +36,7 @@ import java.util.Arrays;
import java.util.List;
/**
* Reactor 全局鉴权过滤器
* 全局鉴权过滤器 (基于 Reactor)
* <p>
* 默认优先级为 -100尽量保证在其它过滤器之前执行
* </p>
@@ -103,7 +102,7 @@ public class SaReactorFilter implements SaFilter, WebFilter {
* 异常处理函数:每次[认证函数]发生异常时执行此函数
*/
public SaFilterErrorStrategy error = e -> {
throw new SaTokenException(e).setCode(SaReactorSpringBootErrorCode.CODE_20205);
throw new SaTokenException(e);
};
/**
@@ -135,55 +134,25 @@ public class SaReactorFilter implements SaFilter, WebFilter {
@Override
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
// 写入WebFilterChain对象
exchange.getAttributes().put(SaReactorHolder.CHAIN_KEY, chain);
// ---------- 全局认证处理
try {
// 写入全局上下文 (同步)
SaReactorSyncHolder.setContext(exchange);
// 执行全局过滤器
beforeAuth.run(null);
SaRouter.match(includeList).notMatch(excludeList).check(r -> {
auth.run(null);
});
} catch (StopMatchException e) {
// StopMatchException 异常代表停止匹配进入Controller
} catch (Throwable e) {
// 1. 获取异常处理策略结果
String result = (e instanceof BackResultException) ? e.getMessage() : String.valueOf(error.run(e));
// 2. 写入输出流
// 请注意此处默认 Content-Type 为 text/plain如果需要返回 JSON 信息,需要在 return 前自行设置 Content-Type 为 application/json
// 例如SaHolder.getResponse().setHeader("Content-Type", "application/json;charset=UTF-8");
if(exchange.getResponse().getHeaders().getFirst(SaTokenConsts.CONTENT_TYPE_KEY) == null) {
exchange.getResponse().getHeaders().set(SaTokenConsts.CONTENT_TYPE_KEY, SaTokenConsts.CONTENT_TYPE_TEXT_PLAIN);
}
return exchange.getResponse().writeWith(Mono.just(exchange.getResponse().bufferFactory().wrap(result.getBytes())));
} finally {
// 清除上下文
SaRouter.match(includeList).notMatch(excludeList).check(r -> auth.run(null));
}
catch (StopMatchException ignored) {}
catch (BackResultException e) {
return SaReactorOperateUtil.writeResult(exchange, e.getMessage());
}
catch (Throwable e) {
return SaReactorOperateUtil.writeResult(exchange, String.valueOf(error.run(e)));
}
finally {
SaReactorSyncHolder.clearContext();
}
// ---------- 执行
// 写入全局上下文 (同步)
SaReactorSyncHolder.setContext(exchange);
// 执行
return chain.filter(exchange).subscriberContext(ctx -> {
// 写入全局上下文 (异步)
ctx = ctx.put(SaReactorHolder.CONTEXT_KEY, exchange);
return ctx;
}).doFinally(r -> {
// 清除上下文
SaReactorSyncHolder.clearContext();
});
return chain.filter(exchange);
}
}

View File

@@ -0,0 +1,59 @@
/*
* Copyright 2020-2099 sa-token.cc
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package cn.dev33.satoken.reactor.filter;
import cn.dev33.satoken.reactor.context.SaReactorHolder;
import cn.dev33.satoken.util.SaTokenConsts;
import org.springframework.core.annotation.Order;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.WebFilter;
import org.springframework.web.server.WebFilterChain;
import reactor.core.publisher.Mono;
/**
* SaTokenContext 上下文初始化过滤器 (基于 Reactor)
*
* @author click33
* @since 1.42.0
*/
@Order(SaTokenConsts.SA_TOKEN_CONTEXT_FILTER_ORDER)
public class SaTokenContextFilterForReactor implements WebFilter {
@Override
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
return chain.filter(exchange)
.contextWrite(ctx -> SaReactorHolder.setContext(ctx, exchange, chain))
.doFinally(r -> {
// 在流式上下文中保存的数据会随着流式操作的结束而销毁,所以此处无需手动清除数据
});
}
}
/*
* 这种写法有问题:
@Override
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
try {
SaReactorSyncHolder.setContext(exchange);
return chain.filter(exchange);
} finally {
SaReactorSyncHolder.clearContext();
}
}
这种写法会先执行 finally然后进入具体的 Controller
*/

View File

@@ -184,7 +184,7 @@ public class SaRequestForReactor implements SaRequest {
*/
@Override
public Object forward(String path) {
ServerWebExchange exchange = SaReactorSyncHolder.getContext();
ServerWebExchange exchange = SaReactorSyncHolder.getExchange();
WebFilterChain chain = exchange.getAttribute(SaReactorHolder.CHAIN_KEY);
ServerHttpRequest newRequest = request.mutate().path(path).build();

View File

@@ -16,22 +16,15 @@
package cn.dev33.satoken.reactor.spring;
import cn.dev33.satoken.context.SaTokenContextForThreadLocal;
import cn.dev33.satoken.spring.pathmatch.SaPathPatternParserUtil;
/**
* <h2> 此为低版本(<1.42.0) 的上下文处理方案,仅做留档,如无必要请勿使用 </h2>
*
* Sa-Token 上下文处理器 [ Spring Reactor 版本实现 ] ,基于 SaTokenContextForThreadLocal 定制
*
* @author click33
* @since 1.33.0
*/
public class SaTokenContextForSpringReactor extends SaTokenContextForThreadLocal {
/**
* 重写路由匹配方法
*/
@Override
public boolean matchPath(String pattern, String path) {
return SaPathPatternParserUtil.match(pattern, path);
}
}

View File

@@ -16,10 +16,11 @@
package cn.dev33.satoken.reactor.spring;
import cn.dev33.satoken.reactor.filter.SaFirewallCheckFilterForReactor;
import cn.dev33.satoken.reactor.filter.SaTokenContextFilterForReactor;
import cn.dev33.satoken.spring.pathmatch.SaPathPatternParserUtil;
import cn.dev33.satoken.strategy.SaStrategy;
import org.springframework.context.annotation.Bean;
import cn.dev33.satoken.context.SaTokenContext;
/**
* 注册 Sa-Token 所需要的 Bean
*
@@ -28,18 +29,25 @@ import cn.dev33.satoken.context.SaTokenContext;
*/
public class SaTokenContextRegister {
/**
* 获取上下文处理器组件 (Spring Reactor 版)
*
* @return /
*/
@Bean
public SaTokenContext getSaTokenContextForSpringReactor() {
return new SaTokenContextForSpringReactor();
public SaTokenContextRegister() {
// 重写路由匹配算法
SaStrategy.instance.routeMatcher = (pattern, path) -> {
return SaPathPatternParserUtil.match(pattern, path);
};
}
/**
* 请求 path 校验过滤器
* 上下文过滤器
*
* @return /
*/
@Bean
public SaTokenContextFilterForReactor saTokenContextFilterForServlet() {
return new SaTokenContextFilterForReactor();
}
/**
* 防火墙过滤器
*
* @return /
*/

View File

@@ -5,7 +5,7 @@ import cn.dev33.satoken.util.SaFoxUtil;
import org.springframework.boot.SpringBootVersion;
/**
* SpringBoot 版本与 Sa-Token 版本兼容检查器
* SpringBoot 版本与 Sa-Token 版本兼容检查器,当开发者错误的在 SpringBoot3.x 项目中引入当前集成包时,将在控制台做出提醒并阻断项目启动
*
* @author Uncarbon
* @since 1.38.0
@@ -22,4 +22,5 @@ public class SpringBootVersionCompatibilityChecker {
System.err.println(str);
throw new SaTokenException(str);
}
}

View File

@@ -0,0 +1,46 @@
/*
* Copyright 2020-2099 sa-token.cc
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package cn.dev33.satoken.reactor.util;
import cn.dev33.satoken.util.SaTokenConsts;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
/**
* Reactor 操作工具类
*
* @author click33
* @since 1.42.0
*/
public class SaReactorOperateUtil {
/**
* 写入结果到输出流
* @param exchange /
* @param result /
* @return /
*/
public static Mono<Void> writeResult(ServerWebExchange exchange, String result) {
// 写入输出流
// 请注意此处默认 Content-Type 为 text/plain如果需要返回 JSON 信息,需要在 return 前自行设置 Content-Type 为 application/json
// 例如SaHolder.getResponse().setHeader("Content-Type", "application/json;charset=UTF-8");
if(exchange.getResponse().getHeaders().getFirst(SaTokenConsts.CONTENT_TYPE_KEY) == null) {
exchange.getResponse().getHeaders().set(SaTokenConsts.CONTENT_TYPE_KEY, SaTokenConsts.CONTENT_TYPE_TEXT_PLAIN);
}
return exchange.getResponse().writeWith(Mono.just(exchange.getResponse().bufferFactory().wrap(result.getBytes())));
}
}