diff --git a/sa-token-demo/sa-token-demo-webflux-springboot4/pom.xml b/sa-token-demo/sa-token-demo-webflux-springboot4/pom.xml
new file mode 100644
index 00000000..5baef48e
--- /dev/null
+++ b/sa-token-demo/sa-token-demo-webflux-springboot4/pom.xml
@@ -0,0 +1,61 @@
+
+ 4.0.0
+ cn.dev33
+ sa-token-demo-webflux-springboot4
+ 0.0.1-SNAPSHOT
+
+
+
+ org.springframework.boot
+ spring-boot-starter-parent
+ 4.0.3
+
+
+
+
+
+ 1.44.0
+
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-webflux
+
+
+
+
+ cn.dev33
+ sa-token-reactor-spring-boot4-starter
+ ${sa-token.version}
+
+
+
+
+
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-configuration-processor
+ true
+
+
+
+
+
+
+
diff --git a/sa-token-demo/sa-token-demo-webflux-springboot4/src/main/java/com/pj/SaTokenWebfluxSpringboot4Application.java b/sa-token-demo/sa-token-demo-webflux-springboot4/src/main/java/com/pj/SaTokenWebfluxSpringboot4Application.java
new file mode 100644
index 00000000..1a10bab3
--- /dev/null
+++ b/sa-token-demo/sa-token-demo-webflux-springboot4/src/main/java/com/pj/SaTokenWebfluxSpringboot4Application.java
@@ -0,0 +1,22 @@
+package com.pj;
+
+import cn.dev33.satoken.SaManager;
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+
+/**
+ * Sa-Token整合webflux 示例 (springboot4)
+ *
+ * @author click33
+ * @since 2023年1月3日
+ *
+ */
+@SpringBootApplication
+public class SaTokenWebfluxSpringboot4Application {
+
+ public static void main(String[] args) {
+ SpringApplication.run(SaTokenWebfluxSpringboot4Application.class, args);
+ System.out.println("\n启动成功:Sa-Token配置如下:" + SaManager.getConfig());
+ }
+
+}
diff --git a/sa-token-demo/sa-token-demo-webflux-springboot4/src/main/java/com/pj/satoken/MyFilter.java b/sa-token-demo/sa-token-demo-webflux-springboot4/src/main/java/com/pj/satoken/MyFilter.java
new file mode 100644
index 00000000..130582a3
--- /dev/null
+++ b/sa-token-demo/sa-token-demo-webflux-springboot4/src/main/java/com/pj/satoken/MyFilter.java
@@ -0,0 +1,48 @@
+/*
+ * 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 com.pj.satoken;
+
+import cn.dev33.satoken.reactor.context.SaReactorSyncHolder;
+import cn.dev33.satoken.stp.StpUtil;
+import org.springframework.stereotype.Component;
+import org.springframework.web.server.ServerWebExchange;
+import org.springframework.web.server.WebFilter;
+import org.springframework.web.server.WebFilterChain;
+import reactor.core.publisher.Mono;
+
+/**
+ * 自定义过滤器
+ */
+@Component
+public class MyFilter implements WebFilter {
+
+ @Override
+ public Mono filter(ServerWebExchange exchange, WebFilterChain chain) {
+ System.out.println("进入自定义过滤器");
+
+ try {
+ // 先 set 上下文,再调用 Sa-Token 同步 API,并在 finally 里清除上下文
+ SaReactorSyncHolder.setContext(exchange);
+ System.out.println(StpUtil.isLogin());
+ }
+ finally {
+ SaReactorSyncHolder.clearContext();
+ }
+
+ return chain.filter(exchange);
+ }
+
+}
diff --git a/sa-token-demo/sa-token-demo-webflux-springboot4/src/main/java/com/pj/satoken/SaTokenConfigure.java b/sa-token-demo/sa-token-demo-webflux-springboot4/src/main/java/com/pj/satoken/SaTokenConfigure.java
new file mode 100644
index 00000000..8524f11f
--- /dev/null
+++ b/sa-token-demo/sa-token-demo-webflux-springboot4/src/main/java/com/pj/satoken/SaTokenConfigure.java
@@ -0,0 +1,39 @@
+package com.pj.satoken;
+
+import cn.dev33.satoken.reactor.filter.SaReactorFilter;
+import cn.dev33.satoken.util.SaResult;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+/**
+ * [Sa-Token 权限认证] 配置类
+ * @author click33
+ *
+ */
+@Configuration
+public class SaTokenConfigure {
+
+ /**
+ * 注册 [sa-token全局过滤器]
+ */
+ @Bean
+ public SaReactorFilter getSaReactorFilter() {
+ return new SaReactorFilter()
+ // 指定 [拦截路由]
+ .addInclude("/**")
+ // 指定 [放行路由]
+ .addExclude("/favicon.ico")
+ // 指定[认证函数]: 每次请求执行
+ .setAuth(r -> {
+ System.out.println("---------- sa全局认证");
+ // SaRouter.match("/test/test", () -> StpUtil.checkLogin());
+ })
+ // 指定[异常处理函数]:每次[认证函数]发生异常时执行此函数
+ .setError(e -> {
+ System.out.println("---------- sa全局异常 ");
+ return SaResult.error(e.getMessage());
+ })
+ ;
+ }
+
+}
diff --git a/sa-token-demo/sa-token-demo-webflux-springboot4/src/main/java/com/pj/satoken/StpInterfaceImpl.java b/sa-token-demo/sa-token-demo-webflux-springboot4/src/main/java/com/pj/satoken/StpInterfaceImpl.java
new file mode 100644
index 00000000..b6cc79f7
--- /dev/null
+++ b/sa-token-demo/sa-token-demo-webflux-springboot4/src/main/java/com/pj/satoken/StpInterfaceImpl.java
@@ -0,0 +1,44 @@
+package com.pj.satoken;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.springframework.stereotype.Component;
+
+import cn.dev33.satoken.stp.StpInterface;
+
+/**
+ * 自定义权限验证接口扩展
+ */
+@Component // 打开此注解,保证此类被springboot扫描,即可完成sa-token的自定义权限验证扩展
+public class StpInterfaceImpl implements StpInterface {
+
+ /**
+ * 返回一个账号所拥有的权限码集合
+ */
+ @Override
+ public List getPermissionList(Object loginId, String loginType) {
+ // 本list仅做模拟,实际项目中要根据具体业务逻辑来查询权限
+ List list = new ArrayList();
+ list.add("101");
+ list.add("user-add");
+ list.add("user-delete");
+ list.add("user-update");
+ list.add("user-get");
+ list.add("article-get");
+ return list;
+ }
+
+ /**
+ * 返回一个账号所拥有的角色标识集合
+ */
+ @Override
+ public List getRoleList(Object loginId, String loginType) {
+ // 本list仅做模拟,实际项目中要根据具体业务逻辑来查询角色
+ List list = new ArrayList();
+ list.add("admin");
+ list.add("super-admin");
+ return list;
+ }
+
+}
diff --git a/sa-token-demo/sa-token-demo-webflux-springboot4/src/main/java/com/pj/test/DefineRoutes.java b/sa-token-demo/sa-token-demo-webflux-springboot4/src/main/java/com/pj/test/DefineRoutes.java
new file mode 100644
index 00000000..4e406f31
--- /dev/null
+++ b/sa-token-demo/sa-token-demo-webflux-springboot4/src/main/java/com/pj/test/DefineRoutes.java
@@ -0,0 +1,32 @@
+package com.pj.test;
+
+import cn.dev33.satoken.reactor.context.SaReactorSyncHolder;
+import cn.dev33.satoken.stp.StpUtil;
+import cn.dev33.satoken.util.SaResult;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.http.MediaType;
+import org.springframework.web.reactive.function.server.RequestPredicates;
+import org.springframework.web.reactive.function.server.RouterFunction;
+import org.springframework.web.reactive.function.server.RouterFunctions;
+import org.springframework.web.reactive.function.server.ServerResponse;
+
+@Configuration
+public class DefineRoutes {
+
+ /**
+ * 函数式编程,初始化路由表
+ * @return 路由表
+ */
+ @Bean
+ public RouterFunction getRoutes() {
+ return RouterFunctions.route(RequestPredicates.GET("/fun"), req -> {
+ return SaReactorSyncHolder.setContext(req.exchange(), () -> {
+ System.out.println("是否登录:" + StpUtil.isLogin());
+ SaResult res = SaResult.data(StpUtil.getTokenInfo());
+ return ServerResponse.ok().contentType(MediaType.APPLICATION_JSON).bodyValue(res);
+ });
+ });
+ }
+
+}
diff --git a/sa-token-demo/sa-token-demo-webflux-springboot4/src/main/java/com/pj/test/GlobalException.java b/sa-token-demo/sa-token-demo-webflux-springboot4/src/main/java/com/pj/test/GlobalException.java
new file mode 100644
index 00000000..34743ae8
--- /dev/null
+++ b/sa-token-demo/sa-token-demo-webflux-springboot4/src/main/java/com/pj/test/GlobalException.java
@@ -0,0 +1,19 @@
+package com.pj.test;
+
+import cn.dev33.satoken.util.SaResult;
+import org.springframework.web.bind.annotation.ExceptionHandler;
+import org.springframework.web.bind.annotation.RestControllerAdvice;
+
+/**
+ * 全局异常处理
+ */
+@RestControllerAdvice
+public class GlobalException {
+
+ @ExceptionHandler
+ public SaResult handlerException(Exception e) {
+ e.printStackTrace();
+ return SaResult.error(e.getMessage());
+ }
+
+}
diff --git a/sa-token-demo/sa-token-demo-webflux-springboot4/src/main/java/com/pj/test/TestController.java b/sa-token-demo/sa-token-demo-webflux-springboot4/src/main/java/com/pj/test/TestController.java
new file mode 100644
index 00000000..ac90d9cc
--- /dev/null
+++ b/sa-token-demo/sa-token-demo-webflux-springboot4/src/main/java/com/pj/test/TestController.java
@@ -0,0 +1,115 @@
+package com.pj.test;
+
+import cn.dev33.satoken.reactor.context.SaReactorHolder;
+import cn.dev33.satoken.reactor.context.SaReactorSyncHolder;
+import cn.dev33.satoken.stp.StpUtil;
+import cn.dev33.satoken.util.SaResult;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.CookieValue;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.RestController;
+import org.springframework.web.server.ServerWebExchange;
+import reactor.core.publisher.Mono;
+
+import java.time.Duration;
+
+/**
+ * 测试专用Controller
+ * @author click33
+ *
+ */
+@RestController
+@RequestMapping("/test/")
+public class TestController {
+
+ @Autowired
+ UserService userService;
+
+ // 登录测试:Controller 里调用 Sa-Token API --- http://localhost:8081/test/login
+ @RequestMapping("login")
+ public Mono login(@RequestParam(defaultValue="10001") String id) {
+ return SaReactorHolder.sync(() -> {
+ StpUtil.login(id);
+ return SaResult.ok("登录成功");
+ });
+ }
+
+ // API测试:手动设置上下文、try-finally 形式 --- http://localhost:8081/test/isLogin
+ @RequestMapping("isLogin")
+ public SaResult isLogin(ServerWebExchange exchange) {
+ try {
+ SaReactorSyncHolder.setContext(exchange);
+ System.out.println("是否登录:" + StpUtil.isLogin());
+ return SaResult.data(StpUtil.getTokenInfo());
+ } finally {
+ SaReactorSyncHolder.clearContext();
+ }
+ }
+
+ // API测试:手动设置上下文、lambda 表达式形式 --- http://localhost:8081/test/isLogin2
+ @RequestMapping("isLogin2")
+ public SaResult isLogin2(ServerWebExchange exchange) {
+ SaResult res = SaReactorSyncHolder.setContext(exchange, ()->{
+ System.out.println("是否登录:" + StpUtil.isLogin());
+ return SaResult.data(StpUtil.getTokenInfo());
+ });
+ return SaResult.data(res);
+ }
+
+ // API测试:自动设置上下文、lambda 表达式形式 --- http://localhost:8081/test/isLogin3
+ @RequestMapping("isLogin3")
+ public Mono isLogin3() {
+ return SaReactorHolder.sync(() -> {
+ System.out.println("是否登录:" + StpUtil.isLogin());
+ userService.isLogin();
+ return SaResult.data(StpUtil.getTokenInfo());
+ });
+ }
+
+ // API测试:自动设置上下文、调用 userService Mono 方法 --- http://localhost:8081/test/isLogin4
+ @RequestMapping("isLogin4")
+ public Mono isLogin4() {
+ return userService.findUserIdByNamePwd("ZhangSan", "123456").flatMap(userId -> {
+ return SaReactorHolder.sync(() -> {
+ StpUtil.login(userId);
+ return SaResult.data(StpUtil.getTokenInfo());
+ });
+ });
+ }
+
+ // API测试:切换线程、复杂嵌套调用 --- http://localhost:8081/test/isLogin5
+ @RequestMapping("isLogin5")
+ public Mono isLogin5() {
+ System.out.println("线程id-----" + Thread.currentThread().getId());
+ // 要点:在流里调用 Sa-Token API 之前,必须用 SaReactorHolder.sync( () -> {} ) 进行包裹
+ return Mono.delay(Duration.ofSeconds(1))
+ .doOnNext(r-> System.out.println("线程id-----" + Thread.currentThread().getId()))
+ .map(r-> SaReactorHolder.sync( () -> userService.isLogin() ))
+ .map(r-> userService.findUserIdByNamePwd("ZhangSan", "123456"))
+ .map(r-> SaReactorHolder.sync( () -> userService.isLogin() ))
+ .flatMap(isLogin -> {
+ System.out.println("是否登录 " + isLogin);
+ return SaReactorHolder.sync(() -> {
+ System.out.println("是否登录 " + StpUtil.isLogin());
+ return SaResult.data(StpUtil.getTokenInfo());
+ });
+ });
+ }
+
+ // API测试:使用上下文无关的API --- http://localhost:8081/test/isLogin6
+ @RequestMapping("isLogin6")
+ public SaResult isLogin6(@CookieValue("satoken") String satoken) {
+ System.out.println("token 为:" + satoken);
+ System.out.println("登录人:" + StpUtil.getLoginIdByToken(satoken));
+ return SaResult.ok("登录人:" + StpUtil.getLoginIdByToken(satoken));
+ }
+
+ // 测试 浏览器访问: http://localhost:8081/test/test
+ @RequestMapping("test")
+ public SaResult test() {
+ System.out.println("线程id------- " + Thread.currentThread().getId());
+ return SaResult.ok();
+ }
+
+}
diff --git a/sa-token-demo/sa-token-demo-webflux-springboot4/src/main/java/com/pj/test/UserService.java b/sa-token-demo/sa-token-demo-webflux-springboot4/src/main/java/com/pj/test/UserService.java
new file mode 100644
index 00000000..004ef0ef
--- /dev/null
+++ b/sa-token-demo/sa-token-demo-webflux-springboot4/src/main/java/com/pj/test/UserService.java
@@ -0,0 +1,25 @@
+package com.pj.test;
+
+import cn.dev33.satoken.stp.StpUtil;
+import org.springframework.stereotype.Service;
+import reactor.core.publisher.Mono;
+
+/**
+ * 模拟 Service 方法
+ * @author click33
+ * @since 2025/4/6
+ */
+@Service
+public class UserService {
+
+ public boolean isLogin() {
+ System.out.println("UserService 里调用 API 测试,是否登录:" + StpUtil.isLogin());
+ return StpUtil.isLogin();
+ }
+
+ public Mono findUserIdByNamePwd(String name, String pwd) {
+ // ...
+ return Mono.just(10001L);
+ }
+
+}
diff --git a/sa-token-demo/sa-token-demo-webflux-springboot4/src/main/resources/application.yml b/sa-token-demo/sa-token-demo-webflux-springboot4/src/main/resources/application.yml
new file mode 100644
index 00000000..94579ef3
--- /dev/null
+++ b/sa-token-demo/sa-token-demo-webflux-springboot4/src/main/resources/application.yml
@@ -0,0 +1,46 @@
+# 端口
+server:
+ port: 8081
+
+# sa-token 配置
+sa-token:
+ # token 名称 (同时也是 cookie 名称)
+ token-name: satoken
+ # token 有效期(单位:秒) 默认30天,-1 代表永久有效
+ timeout: 2592000
+ # token 最低活跃频率(单位:秒),如果 token 超过此时间没有访问系统就会被冻结,默认-1 代表不限制,永不冻结
+ active-timeout: -1
+ # 是否允许同一账号多地同时登录 (为 true 时允许一起登录, 为 false 时新登录挤掉旧登录)
+ is-concurrent: true
+ # 在多人登录同一账号时,是否共用一个 token (为 true 时所有登录共用一个 token, 为 false 时每次登录新建一个 token)
+ is-share: false
+ # token 风格(默认可取值:uuid、simple-uuid、random-32、random-64、random-128、tik)
+ token-style: uuid
+ # 是否输出操作日志
+ is-log: true
+
+spring:
+ # redis配置
+ redis:
+ # Redis数据库索引(默认为0)
+ database: 0
+ # Redis服务器地址
+ host: 127.0.0.1
+ # Redis服务器连接端口
+ port: 6379
+ # Redis服务器连接密码(默认为空)
+ password:
+ # 连接超时时间(毫秒)
+ timeout: 10000ms
+ lettuce:
+ pool:
+ # 连接池最大连接数
+ max-active: 200
+ # 连接池最大阻塞等待时间(使用负值表示没有限制)
+ max-wait: -1ms
+ # 连接池中的最大空闲连接
+ max-idle: 10
+ # 连接池中的最小空闲连接
+ min-idle: 0
+
+