diff --git a/sa-token-bom/pom.xml b/sa-token-bom/pom.xml
index 11909dfc..61e0d2d3 100644
--- a/sa-token-bom/pom.xml
+++ b/sa-token-bom/pom.xml
@@ -124,6 +124,11 @@
sa-token-redisson-jackson
${revision}
+
+ cn.dev33
+ sa-token-redisson-jackson2
+ ${revision}
+
cn.dev33
sa-token-redisx
diff --git a/sa-token-core/src/main/java/cn/dev33/satoken/stp/StpLogic.java b/sa-token-core/src/main/java/cn/dev33/satoken/stp/StpLogic.java
index e62c827c..5277099a 100644
--- a/sa-token-core/src/main/java/cn/dev33/satoken/stp/StpLogic.java
+++ b/sa-token-core/src/main/java/cn/dev33/satoken/stp/StpLogic.java
@@ -569,7 +569,7 @@ public class StpLogic {
throw new SaTokenException("loginId 不能为以下值:" + NotLoginException.ABNORMAL_LIST);
}
- // 3、账号 id 不能是简单类型
+ // 3、账号 id 不能是复杂类型
if( ! SaFoxUtil.isBasicType(id.getClass())) {
SaManager.log.warn("loginId 应该为简单类型,例如:String | int | long,不推荐使用复杂类型:" + id.getClass());
}
diff --git a/sa-token-demo/sa-token-demo-solon-redisson/pom.xml b/sa-token-demo/sa-token-demo-solon-redisson/pom.xml
new file mode 100644
index 00000000..fb7dc2d7
--- /dev/null
+++ b/sa-token-demo/sa-token-demo-solon-redisson/pom.xml
@@ -0,0 +1,84 @@
+
+ 4.0.0
+ cn.dev33
+ sa-token-demo-solon-redisson
+ 0.0.1-SNAPSHOT
+
+
+
+ org.noear
+ solon-parent
+ 2.3.7
+
+
+
+
+
+ 1.35.0.RC
+ UTF-8
+ UTF-8
+
+
+
+
+
+
+ org.noear
+ solon-web
+
+
+
+ org.noear
+ solon.logging.simple
+
+
+
+ org.noear
+ redisson-solon-plugin
+
+
+
+
+ cn.dev33
+ sa-token-solon-plugin
+ ${sa-token.version}
+
+
+
+
+ cn.dev33
+ sa-token-redisson-jackson2
+ ${sa-token.version}
+
+
+
+
+
+
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+ 3.8.1
+
+ -parameters
+
+ 1.8
+ UTF-8
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/sa-token-demo/sa-token-demo-solon-redisson/src/main/java/com/pj/SaTokenDemoApp.java b/sa-token-demo/sa-token-demo-solon-redisson/src/main/java/com/pj/SaTokenDemoApp.java
new file mode 100644
index 00000000..8bc72dfa
--- /dev/null
+++ b/sa-token-demo/sa-token-demo-solon-redisson/src/main/java/com/pj/SaTokenDemoApp.java
@@ -0,0 +1,20 @@
+package com.pj;
+
+
+import cn.dev33.satoken.SaManager;
+import org.noear.solon.Solon;
+import org.noear.solon.annotation.SolonMain;
+
+/**
+ * sa-token整合 solon 示例
+ * @author noear
+ *
+ */
+@SolonMain
+public class SaTokenDemoApp {
+
+ public static void main(String[] args) {
+ Solon.start(SaTokenDemoApp.class, args);
+ System.out.println("\n启动成功:Sa-Token配置如下:" + SaManager.getConfig());
+ }
+}
\ No newline at end of file
diff --git a/sa-token-demo/sa-token-demo-solon-redisson/src/main/java/com/pj/satoken/SaLogForSlf4j.java b/sa-token-demo/sa-token-demo-solon-redisson/src/main/java/com/pj/satoken/SaLogForSlf4j.java
new file mode 100644
index 00000000..41f47c65
--- /dev/null
+++ b/sa-token-demo/sa-token-demo-solon-redisson/src/main/java/com/pj/satoken/SaLogForSlf4j.java
@@ -0,0 +1,51 @@
+package com.pj.satoken;
+
+import cn.dev33.satoken.SaManager;
+import cn.dev33.satoken.config.SaTokenConfig;
+import cn.dev33.satoken.log.SaLog;
+import cn.dev33.satoken.log.SaLogForConsole;
+import cn.dev33.satoken.util.StrFormatter;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * 将 Sa-Token log 信息转接到 slf4j 接口
+ *
+ * @author noear 2022/11/14 created
+ */
+//@Component
+public class SaLogForSlf4j extends SaLogForConsole implements SaLog {
+ static final Logger log = LoggerFactory.getLogger(SaLogForSlf4j.class);
+
+ /**
+ * 打印日志到控制台
+ *
+ * @param level 日志等级
+ * @param str 字符串
+ * @param args 参数列表
+ */
+ public void println(int level, String str, Object... args) {
+ SaTokenConfig config = SaManager.getConfig();
+
+ if (config.getIsLog() && level >= config.getLogLevelInt()) {
+ switch (level) {
+ case trace:
+ log.trace(LOG_PREFIX + StrFormatter.format(str, args));
+ break;
+ case debug:
+ log.debug(LOG_PREFIX + StrFormatter.format(str, args));
+ break;
+ case info:
+ log.info(LOG_PREFIX + StrFormatter.format(str, args));
+ break;
+ case warn:
+ log.warn(LOG_PREFIX + StrFormatter.format(str, args));
+ break;
+ case error:
+ case fatal:
+ log.error(LOG_PREFIX + StrFormatter.format(str, args));
+ break;
+ }
+ }
+ }
+}
diff --git a/sa-token-demo/sa-token-demo-solon-redisson/src/main/java/com/pj/satoken/SaLogForSolon.java b/sa-token-demo/sa-token-demo-solon-redisson/src/main/java/com/pj/satoken/SaLogForSolon.java
new file mode 100644
index 00000000..01ee2354
--- /dev/null
+++ b/sa-token-demo/sa-token-demo-solon-redisson/src/main/java/com/pj/satoken/SaLogForSolon.java
@@ -0,0 +1,51 @@
+package com.pj.satoken;
+
+import org.noear.solon.core.util.LogUtil;
+
+import cn.dev33.satoken.SaManager;
+import cn.dev33.satoken.config.SaTokenConfig;
+import cn.dev33.satoken.log.SaLog;
+import cn.dev33.satoken.log.SaLogForConsole;
+import cn.dev33.satoken.util.StrFormatter;
+
+/**
+ * 将 Sa-Token log 信息转接到 Solon
+ *
+ * @author click33
+ * @since 2022-11-2
+ */
+//@Component
+public class SaLogForSolon extends SaLogForConsole implements SaLog {
+
+ /**
+ * 打印日志到控制台
+ *
+ * @param level 日志等级
+ * @param str 字符串
+ * @param args 参数列表
+ */
+ public void println(int level, String str, Object... args) {
+ SaTokenConfig config = SaManager.getConfig();
+
+ if (config.getIsLog() && level >= config.getLogLevelInt()) {
+ switch (level) {
+ case trace:
+ LogUtil.global().trace(LOG_PREFIX + StrFormatter.format(str, args));
+ break;
+ case debug:
+ LogUtil.global().debug(LOG_PREFIX + StrFormatter.format(str, args));
+ break;
+ case info:
+ LogUtil.global().info(LOG_PREFIX + StrFormatter.format(str, args));
+ break;
+ case warn:
+ LogUtil.global().warn(LOG_PREFIX + StrFormatter.format(str, args));
+ break;
+ case error:
+ case fatal:
+ LogUtil.global().error(LOG_PREFIX + StrFormatter.format(str, args));
+ break;
+ }
+ }
+ }
+}
diff --git a/sa-token-demo/sa-token-demo-solon-redisson/src/main/java/com/pj/satoken/SaTokenConfigure.java b/sa-token-demo/sa-token-demo-solon-redisson/src/main/java/com/pj/satoken/SaTokenConfigure.java
new file mode 100644
index 00000000..3c2e72ef
--- /dev/null
+++ b/sa-token-demo/sa-token-demo-solon-redisson/src/main/java/com/pj/satoken/SaTokenConfigure.java
@@ -0,0 +1,81 @@
+package com.pj.satoken;
+
+
+import cn.dev33.satoken.dao.SaTokenDao;
+import cn.dev33.satoken.dao.SaTokenDaoRedissonJackson;
+import cn.dev33.satoken.solon.integration.SaTokenInterceptor;
+import org.noear.solon.Solon;
+import org.noear.solon.annotation.Bean;
+import org.noear.solon.annotation.Configuration;
+
+import com.pj.util.AjaxJson;
+
+import cn.dev33.satoken.context.SaHolder;
+import org.noear.solon.annotation.Inject;
+import org.redisson.api.RedissonClient;
+import org.redisson.solon.RedissonSupplier;
+
+
+/**
+ * [Sa-Token 权限认证] 配置类
+ * @author click33
+ * @author noear
+ */
+@Configuration
+public class SaTokenConfigure {
+
+ /**
+ * 注册 [sa-token全局过滤器]
+ */
+ @Bean(index = -100)
+ public SaTokenInterceptor tokenPathFilter() {
+ return new SaTokenInterceptor()
+
+ // 指定 [拦截路由] 与 [放行路由]
+ .addInclude("/**").addExclude("/favicon.ico")
+
+ // 认证函数: 每次请求执行
+ .setAuth(r -> {
+ // System.out.println("---------- sa全局认证");
+
+ // SaRouter.match("/test/test", () -> new Object());
+ })
+
+ // 异常处理函数:每次认证函数发生异常时执行此函数
+ .setError(e -> {
+ System.out.println("---------- sa全局异常 ");
+ return AjaxJson.getError(e.getMessage());
+ })
+
+ // 前置函数:在每次认证函数之前执行(BeforeAuth 不受 includeList 与 excludeList 的限制,所有请求都会进入)
+ .setBeforeAuth(r -> {
+ // ---------- 设置一些安全响应头 ----------
+ SaHolder.getResponse()
+ // 服务器名称
+ .setServer("sa-server")
+ // 是否可以在iframe显示视图: DENY=不可以 | SAMEORIGIN=同域下可以 | ALLOW-FROM uri=指定域名下可以
+ .setHeader("X-Frame-Options", "SAMEORIGIN")
+ // 是否启用浏览器默认XSS防护: 0=禁用 | 1=启用 | 1; mode=block 启用, 并在检查到XSS攻击时,停止渲染页面
+ .setHeader("X-Frame-Options", "1; mode=block")
+ // 禁用浏览器内容嗅探
+ .setHeader("X-Content-Type-Options", "nosniff")
+ ;
+ });
+ }
+
+ /**
+ * 构造 RedissonClient
+ * */
+ @Bean
+ public RedissonClient saTokenDaoInit(@Inject("${sa-token-dao}") RedissonSupplier supplier) {
+ return supplier.get();
+ }
+
+ /**
+ * 构建 SaTokenDao
+ * */
+ @Bean
+ public SaTokenDao saTokenDaoInit(RedissonClient redissonClient) {
+ return new SaTokenDaoRedissonJackson(redissonClient);
+ }
+}
diff --git a/sa-token-demo/sa-token-demo-solon-redisson/src/main/java/com/pj/satoken/StpInterfaceImpl.java b/sa-token-demo/sa-token-demo-solon-redisson/src/main/java/com/pj/satoken/StpInterfaceImpl.java
new file mode 100644
index 00000000..5833b67d
--- /dev/null
+++ b/sa-token-demo/sa-token-demo-solon-redisson/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 cn.dev33.satoken.stp.StpInterface;
+import org.noear.solon.annotation.Component;
+
+/**
+ * 自定义权限验证接口扩展
+ */
+@Component // 打开此注解,保证此类被 solon 扫描,即可完成 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-solon-redisson/src/main/java/com/pj/test/GlobalExceptionFilter.java b/sa-token-demo/sa-token-demo-solon-redisson/src/main/java/com/pj/test/GlobalExceptionFilter.java
new file mode 100644
index 00000000..1daeec16
--- /dev/null
+++ b/sa-token-demo/sa-token-demo-solon-redisson/src/main/java/com/pj/test/GlobalExceptionFilter.java
@@ -0,0 +1,46 @@
+package com.pj.test;
+
+import com.pj.util.AjaxJson;
+
+import cn.dev33.satoken.exception.*;
+
+import org.noear.solon.annotation.Component;
+import org.noear.solon.core.handle.Context;
+import org.noear.solon.core.handle.Filter;
+import org.noear.solon.core.handle.FilterChain;
+
+
+/**
+ * 全局异常处理
+ *
+ * @author noear
+ */
+@Component
+public class GlobalExceptionFilter implements Filter {
+ @Override
+ public void doFilter(Context ctx, FilterChain chain) throws Throwable {
+ try {
+ chain.doFilter(ctx);
+ } catch (SaTokenException e) {
+ // 不同异常返回不同状态码
+ AjaxJson aj = null;
+ if (e instanceof NotLoginException) { // 如果是未登录异常
+ NotLoginException ee = (NotLoginException) e;
+ aj = AjaxJson.getNotLogin().setMsg(ee.getMessage());
+ } else if (e instanceof NotRoleException) { // 如果是角色异常
+ NotRoleException ee = (NotRoleException) e;
+ aj = AjaxJson.getNotJur("无此角色:" + ee.getRole());
+ } else if (e instanceof NotPermissionException) { // 如果是权限异常
+ NotPermissionException ee = (NotPermissionException) e;
+ aj = AjaxJson.getNotJur("无此权限:" + ee.getPermission());
+ } else if (e instanceof DisableServiceException) { // 如果是被封禁异常
+ DisableServiceException ee = (DisableServiceException) e;
+ aj = AjaxJson.getNotJur("账号被封禁:" + ee.getDisableTime() + "秒后解封");
+ } else { // 普通异常, 输出:500 + 异常信息
+ aj = AjaxJson.getError(e.getMessage());
+ }
+
+ ctx.render(aj);
+ }
+ }
+}
diff --git a/sa-token-demo/sa-token-demo-solon-redisson/src/main/java/com/pj/test/SSOController.java b/sa-token-demo/sa-token-demo-solon-redisson/src/main/java/com/pj/test/SSOController.java
new file mode 100644
index 00000000..8cc55287
--- /dev/null
+++ b/sa-token-demo/sa-token-demo-solon-redisson/src/main/java/com/pj/test/SSOController.java
@@ -0,0 +1,36 @@
+package com.pj.test;
+
+import org.noear.solon.annotation.Controller;
+import org.noear.solon.annotation.Mapping;
+
+import com.pj.util.AjaxJson;
+
+import cn.dev33.satoken.stp.StpUtil;
+import org.noear.solon.annotation.Param;
+
+/**
+ * 测试: 同域单点登录
+ * @author click33
+ * @author noear
+ */
+@Controller
+@Mapping("/sso/")
+public class SSOController {
+
+ // 测试:进行登录
+ @Mapping("doLogin")
+ public AjaxJson doLogin(@Param(defaultValue = "10001") String id) {
+ System.out.println("---------------- 进行登录 ");
+ StpUtil.login(id);
+ return AjaxJson.getSuccess("登录成功: " + id);
+ }
+
+ // 测试:是否登录
+ @Mapping("isLogin")
+ public AjaxJson isLogin() {
+ System.out.println("---------------- 是否登录 ");
+ boolean isLogin = StpUtil.isLogin();
+ return AjaxJson.getSuccess("是否登录: " + isLogin);
+ }
+
+}
diff --git a/sa-token-demo/sa-token-demo-solon-redisson/src/main/java/com/pj/test/StressTestController.java b/sa-token-demo/sa-token-demo-solon-redisson/src/main/java/com/pj/test/StressTestController.java
new file mode 100644
index 00000000..cb203812
--- /dev/null
+++ b/sa-token-demo/sa-token-demo-solon-redisson/src/main/java/com/pj/test/StressTestController.java
@@ -0,0 +1,61 @@
+package com.pj.test;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.noear.solon.annotation.Controller;
+import org.noear.solon.annotation.Mapping;
+
+import com.pj.util.AjaxJson;
+import com.pj.util.Ttime;
+
+import cn.dev33.satoken.stp.StpUtil;
+
+/**
+ * 压力测试
+ * @author click33
+ * @author noear
+ */
+@Controller
+@Mapping("/s-test/")
+public class StressTestController {
+
+
+ // 测试 浏览器访问: http://localhost:8081/s-test/login
+ // 测试前,请先将 is-read-cookie 配置为 false
+ @Mapping("login")
+ public AjaxJson login() {
+// StpUtil.getTokenSession().logout();
+// StpUtil.logoutByLoginId(10001);
+
+ int count = 10; // 循环多少轮
+ int loginCount = 10000; // 每轮循环多少次
+
+ // 循环10次 取平均时间
+ List list = new ArrayList<>();
+ for (int i = 1; i <= count; i++) {
+ System.out.println("\n---------------------第" + i + "轮---------------------");
+ Ttime t = new Ttime().start();
+ // 每次登录的次数
+ for (int j = 1; j <= loginCount; j++) {
+ StpUtil.login("1000" + j, "PC-" + j);
+ if(j % 1000 == 0) {
+ System.out.println("已登录:" + j);
+ }
+ }
+ t.end();
+ list.add((t.returnMs() + 0.0) / 1000);
+ System.out.println("第" + i + "轮" + "用时:" + t.toString());
+ }
+// System.out.println(((SaTokenDaoDefaultImpl)SaTokenManager.getSaTokenDao()).dataMap.size());
+
+ System.out.println("\n---------------------测试结果---------------------");
+ System.out.println(list.size() + "次测试: " + list);
+ double ss = 0;
+ for (int i = 0; i < list.size(); i++) {
+ ss += list.get(i);
+ }
+ System.out.println("平均用时: " + ss / list.size());
+ return AjaxJson.getSuccess();
+ }
+}
diff --git a/sa-token-demo/sa-token-demo-solon-redisson/src/main/java/com/pj/test/TestController.java b/sa-token-demo/sa-token-demo-solon-redisson/src/main/java/com/pj/test/TestController.java
new file mode 100644
index 00000000..30f06694
--- /dev/null
+++ b/sa-token-demo/sa-token-demo-solon-redisson/src/main/java/com/pj/test/TestController.java
@@ -0,0 +1,252 @@
+package com.pj.test;
+
+import java.util.Date;
+import java.util.List;
+
+import com.pj.util.AjaxJson;
+import com.pj.util.Ttime;
+
+import cn.dev33.satoken.annotation.SaCheckLogin;
+import cn.dev33.satoken.annotation.SaCheckPermission;
+import cn.dev33.satoken.annotation.SaCheckRole;
+import cn.dev33.satoken.annotation.SaMode;
+import cn.dev33.satoken.session.SaSessionCustomUtil;
+import cn.dev33.satoken.stp.SaTokenInfo;
+import cn.dev33.satoken.stp.StpUtil;
+import org.noear.snack.ONode;
+import org.noear.solon.annotation.Controller;
+import org.noear.solon.annotation.Mapping;
+import org.noear.solon.annotation.Param;
+
+/**
+ * 测试专用Controller
+ * @author click33
+ * @author noear
+ */
+@Controller
+@Mapping("/test/")
+public class TestController {
+
+
+ // 测试登录接口, 浏览器访问: http://localhost:8081/test/login
+ @Mapping("login")
+ public AjaxJson login(@Param(defaultValue="10001") String id) {
+ System.out.println("======================= 进入方法,测试登录接口 ========================= ");
+ System.out.println("当前会话的token:" + StpUtil.getTokenValue());
+ System.out.println("当前是否登录:" + StpUtil.isLogin());
+ System.out.println("当前登录账号:" + StpUtil.getLoginIdDefaultNull());
+
+ StpUtil.login(id); // 在当前会话登录此账号
+ System.out.println("登录成功");
+ System.out.println("当前是否登录:" + StpUtil.isLogin());
+ System.out.println("当前登录账号:" + StpUtil.getLoginId());
+// System.out.println("当前登录账号并转为int:" + StpUtil.getLoginIdAsInt());
+ System.out.println("当前登录设备:" + StpUtil.getLoginDevice());
+// System.out.println("当前token信息:" + StpUtil.getTokenInfo());
+
+ return AjaxJson.getSuccess();
+ }
+
+ // 测试退出登录 , 浏览器访问: http://localhost:8081/test/logout
+ @Mapping("logout")
+ public AjaxJson logout() {
+ StpUtil.logout();
+// StpUtil.logoutByLoginId(10001);
+ return AjaxJson.getSuccess();
+ }
+
+ // 测试角色接口, 浏览器访问: http://localhost:8081/test/testRole
+ @Mapping("testRole")
+ public AjaxJson testRole() {
+ System.out.println("======================= 进入方法,测试角色接口 ========================= ");
+
+ System.out.println("是否具有角色标识 user " + StpUtil.hasRole("user"));
+ System.out.println("是否具有角色标识 admin " + StpUtil.hasRole("admin"));
+
+ System.out.println("没有admin权限就抛出异常");
+ StpUtil.checkRole("admin");
+
+ System.out.println("在【admin、user】中只要拥有一个就不会抛出异常");
+ StpUtil.checkRoleOr("admin", "user");
+
+ System.out.println("在【admin、user】中必须全部拥有才不会抛出异常");
+ StpUtil.checkRoleAnd("admin", "user");
+
+ System.out.println("角色测试通过");
+
+ return AjaxJson.getSuccess();
+ }
+
+ // 测试权限接口, 浏览器访问: http://localhost:8081/test/testJur
+ @Mapping("testJur")
+ public AjaxJson testJur() {
+ System.out.println("======================= 进入方法,测试权限接口 ========================= ");
+
+ System.out.println("是否具有权限101" + StpUtil.hasPermission("101"));
+ System.out.println("是否具有权限user-add" + StpUtil.hasPermission("user-add"));
+ System.out.println("是否具有权限article-get" + StpUtil.hasPermission("article-get"));
+
+ System.out.println("没有user-add权限就抛出异常");
+ StpUtil.checkPermission("user-add");
+
+ System.out.println("在【101、102】中只要拥有一个就不会抛出异常");
+ StpUtil.checkPermissionOr("101", "102");
+
+ System.out.println("在【101、102】中必须全部拥有才不会抛出异常");
+ StpUtil.checkPermissionAnd("101", "102");
+
+ System.out.println("权限测试通过");
+
+ return AjaxJson.getSuccess();
+ }
+
+ // 测试会话session接口, 浏览器访问: http://localhost:8081/test/session
+ @Mapping("session")
+ public AjaxJson session() {
+ System.out.println("======================= 进入方法,测试会话session接口 ========================= ");
+ System.out.println("当前是否登录:" + StpUtil.isLogin());
+ System.out.println("当前登录账号session的id" + StpUtil.getSession().getId());
+ System.out.println("当前登录账号session的id" + StpUtil.getSession().getId());
+ System.out.println("测试取值name:" + StpUtil.getSession().get("name"));
+ StpUtil.getSession().set("name", new Date()); // 写入一个值
+ System.out.println("测试取值name:" + StpUtil.getSession().get("name"));
+ System.out.println( ONode.stringify(StpUtil.getSession()));
+ return AjaxJson.getSuccess();
+ }
+
+ // 测试自定义session接口, 浏览器访问: http://localhost:8081/test/session2
+ @Mapping("session2")
+ public AjaxJson session2() {
+ System.out.println("======================= 进入方法,测试自定义session接口 ========================= ");
+ // 自定义session就是无需登录也可以使用 的session :比如拿用户的手机号当做 key, 来获取 session
+ System.out.println("自定义 session的id为:" + SaSessionCustomUtil.getSessionById("1895544896").getId());
+ System.out.println("测试取值name:" + SaSessionCustomUtil.getSessionById("1895544896").get("name"));
+ SaSessionCustomUtil.getSessionById("1895544896").set("name", "张三"); // 写入值
+ System.out.println("测试取值name:" + SaSessionCustomUtil.getSessionById("1895544896").get("name"));
+ System.out.println("测试取值name:" + SaSessionCustomUtil.getSessionById("1895544896").get("name"));
+ return AjaxJson.getSuccess();
+ }
+
+ // ----------
+ // 测试token专属session, 浏览器访问: http://localhost:8081/test/getTokenSession
+ @Mapping("getTokenSession")
+ public AjaxJson getTokenSession() {
+ System.out.println("======================= 进入方法,测试会话session接口 ========================= ");
+ System.out.println("当前是否登录:" + StpUtil.isLogin());
+ System.out.println("当前token专属session: " + StpUtil.getTokenSession().getId());
+
+ System.out.println("测试取值name:" + StpUtil.getTokenSession().get("name"));
+ StpUtil.getTokenSession().set("name", "张三"); // 写入一个值
+ System.out.println("测试取值name:" + StpUtil.getTokenSession().get("name"));
+
+ return AjaxJson.getSuccess();
+ }
+
+ // 打印当前token信息, 浏览器访问: http://localhost:8081/test/tokenInfo
+ @Mapping("tokenInfo")
+ public AjaxJson tokenInfo() {
+ System.out.println("======================= 进入方法,打印当前token信息 ========================= ");
+ SaTokenInfo tokenInfo = StpUtil.getTokenInfo();
+ System.out.println(tokenInfo);
+ return AjaxJson.getSuccessData(tokenInfo);
+ }
+
+ // 测试注解式鉴权, 浏览器访问: http://localhost:8081/test/atCheck
+ @SaCheckLogin // 注解式鉴权:当前会话必须登录才能通过
+ @SaCheckRole("super-admin") // 注解式鉴权:当前会话必须具有指定角色标识才能通过
+ @SaCheckPermission("user-add") // 注解式鉴权:当前会话必须具有指定权限才能通过
+ @Mapping("atCheck")
+ public AjaxJson atCheck() {
+ System.out.println("======================= 进入方法,测试注解鉴权接口 ========================= ");
+ System.out.println("只有通过注解鉴权,才能进入此方法");
+// StpUtil.checkActiveTimeout();
+// StpUtil.updateLastActiveToNow();
+ return AjaxJson.getSuccess();
+ }
+
+ // 测试注解式鉴权, 浏览器访问: http://localhost:8081/test/atJurOr
+ @Mapping("atJurOr")
+ @SaCheckPermission(value = {"user-add", "user-all", "user-delete"}, mode = SaMode.OR) // 注解式鉴权:只要具有其中一个权限即可通过校验
+ public AjaxJson atJurOr() {
+ return AjaxJson.getSuccessData("用户信息");
+ }
+
+ // [活动时间] 续签: http://localhost:8081/test/rene
+ @Mapping("rene")
+ public AjaxJson rene() {
+ StpUtil.checkActiveTimeout();
+ StpUtil.updateLastActiveToNow();
+ return AjaxJson.getSuccess("续签成功");
+ }
+
+ // 测试踢人下线 浏览器访问: http://localhost:8081/test/kickOut
+ @Mapping("kickOut")
+ public AjaxJson kickOut() {
+ // 先登录上
+ StpUtil.login(10001);
+ // 踢下线
+ StpUtil.kickout(10001);
+ // 再尝试获取
+ StpUtil.getLoginId();
+ // 返回
+ return AjaxJson.getSuccess();
+ }
+
+ // 测试登录接口, 按照设备类型登录, 浏览器访问: http://localhost:8081/test/login2
+ @Mapping("login2")
+ public AjaxJson login2(@Param(defaultValue="10001") String id, @Param(defaultValue="PC") String device) {
+ StpUtil.login(id, device);
+ return AjaxJson.getSuccess();
+ }
+
+ // 测试身份临时切换: http://localhost:8081/test/switchTo
+ @Mapping("switchTo")
+ public AjaxJson switchTo() {
+ System.out.println("当前会话身份:" + StpUtil.getLoginIdDefaultNull());
+ System.out.println("是否正在身份临时切换中: " + StpUtil.isSwitch());
+ StpUtil.switchTo(10044, () -> {
+ System.out.println("是否正在身份临时切换中: " + StpUtil.isSwitch());
+ System.out.println("当前会话身份已被切换为:" + StpUtil.getLoginId());
+ });
+ System.out.println("是否正在身份临时切换中: " + StpUtil.isSwitch());
+ return AjaxJson.getSuccess();
+ }
+
+ // 测试会话治理 浏览器访问: http://localhost:8081/test/search
+ @Mapping("search")
+ public AjaxJson search() {
+ System.out.println("--------------");
+ Ttime t = new Ttime().start();
+ List tokenValue = StpUtil.searchTokenValue("8feb8265f773", 0, 10, true);
+ for (String v : tokenValue) {
+// SaSession session = StpUtil.getSessionBySessionId(sid);
+ System.out.println(v);
+ }
+ System.out.println("用时:" + t.end().toString());
+ return AjaxJson.getSuccess();
+ }
+
+ // 测试指定设备类型登录 浏览器访问: http://localhost:8081/test/loginByDevice
+ @Mapping("loginByDevice")
+ public AjaxJson loginByDevice() {
+ System.out.println("--------------");
+ StpUtil.login(10001, "PC");
+ return AjaxJson.getSuccessData("登录成功");
+ }
+
+
+ // 测试 浏览器访问: http://localhost:8081/test/test
+ @Mapping("test")
+ public AjaxJson test() {
+ System.out.println("进来了");
+ return AjaxJson.getSuccess("访问成功");
+ }
+
+ // 测试 浏览器访问: http://localhost:8081/test/test2
+ @Mapping("test2")
+ public AjaxJson test2() {
+ return AjaxJson.getSuccess();
+ }
+
+
+}
diff --git a/sa-token-demo/sa-token-demo-solon-redisson/src/main/java/com/pj/test/UserController.java b/sa-token-demo/sa-token-demo-solon-redisson/src/main/java/com/pj/test/UserController.java
new file mode 100644
index 00000000..939d5c4d
--- /dev/null
+++ b/sa-token-demo/sa-token-demo-solon-redisson/src/main/java/com/pj/test/UserController.java
@@ -0,0 +1,33 @@
+package com.pj.test;
+
+import cn.dev33.satoken.stp.StpUtil;
+import org.noear.solon.annotation.Controller;
+import org.noear.solon.annotation.Mapping;
+
+/**
+ * 登录测试
+ * @author click33
+ * @author noear
+ */
+@Controller
+@Mapping("/user/")
+public class UserController {
+
+ // 测试登录,浏览器访问: http://localhost:8081/user/doLogin?username=zhang&password=123456
+ @Mapping("doLogin")
+ public String doLogin(String username, String password) {
+ // 此处仅作模拟示例,真实项目需要从数据库中查询数据进行比对
+ if("zhang".equals(username) && "123456".equals(password)) {
+ StpUtil.login(10001);
+ return "登录成功";
+ }
+ return "登录失败";
+ }
+
+ // 查询登录状态,浏览器访问: http://localhost:8081/user/isLogin
+ @Mapping("isLogin")
+ public String isLogin(String username, String password) {
+ return "当前会话是否登录:" + StpUtil.isLogin();
+ }
+
+}
diff --git a/sa-token-demo/sa-token-demo-solon-redisson/src/main/java/com/pj/util/AjaxJson.java b/sa-token-demo/sa-token-demo-solon-redisson/src/main/java/com/pj/util/AjaxJson.java
new file mode 100644
index 00000000..768d0578
--- /dev/null
+++ b/sa-token-demo/sa-token-demo-solon-redisson/src/main/java/com/pj/util/AjaxJson.java
@@ -0,0 +1,162 @@
+package com.pj.util;
+
+import java.io.Serializable;
+import java.util.List;
+
+
+/**
+ * ajax请求返回Json格式数据的封装
+ */
+public class AjaxJson implements Serializable{
+
+ private static final long serialVersionUID = 1L; // 序列化版本号
+
+ public static final int CODE_SUCCESS = 200; // 成功状态码
+ public static final int CODE_ERROR = 500; // 错误状态码
+ public static final int CODE_WARNING = 501; // 警告状态码
+ public static final int CODE_NOT_JUR = 403; // 无权限状态码
+ public static final int CODE_NOT_LOGIN = 401; // 未登录状态码
+ public static final int CODE_INVALID_REQUEST = 400; // 无效请求状态码
+
+ public int code; // 状态码
+ public String msg; // 描述信息
+ public Object data; // 携带对象
+ public Long dataCount; // 数据总数,用于分页
+
+ /**
+ * 返回code
+ * @return
+ */
+ public int getCode() {
+ return this.code;
+ }
+
+ /**
+ * 给msg赋值,连缀风格
+ */
+ public AjaxJson setMsg(String msg) {
+ this.msg = msg;
+ return this;
+ }
+ public String getMsg() {
+ return this.msg;
+ }
+
+ /**
+ * 给data赋值,连缀风格
+ */
+ public AjaxJson setData(Object data) {
+ this.data = data;
+ return this;
+ }
+
+ /**
+ * 将data还原为指定类型并返回
+ */
+ @SuppressWarnings("unchecked")
+ public T getData(Class cs) {
+ return (T) data;
+ }
+
+ // ============================ 构建 ==================================
+
+ public AjaxJson(int code, String msg, Object data, Long dataCount) {
+ this.code = code;
+ this.msg = msg;
+ this.data = data;
+ this.dataCount = dataCount;
+ }
+
+ // 返回成功
+ public static AjaxJson getSuccess() {
+ return new AjaxJson(CODE_SUCCESS, "ok", null, null);
+ }
+ public static AjaxJson getSuccess(String msg) {
+ return new AjaxJson(CODE_SUCCESS, msg, null, null);
+ }
+ public static AjaxJson getSuccess(String msg, Object data) {
+ return new AjaxJson(CODE_SUCCESS, msg, data, null);
+ }
+ public static AjaxJson getSuccessData(Object data) {
+ return new AjaxJson(CODE_SUCCESS, "ok", data, null);
+ }
+ public static AjaxJson getSuccessArray(Object... data) {
+ return new AjaxJson(CODE_SUCCESS, "ok", data, null);
+ }
+
+ // 返回失败
+ public static AjaxJson getError() {
+ return new AjaxJson(CODE_ERROR, "error", null, null);
+ }
+ public static AjaxJson getError(String msg) {
+ return new AjaxJson(CODE_ERROR, msg, null, null);
+ }
+
+ // 返回警告
+ public static AjaxJson getWarning() {
+ return new AjaxJson(CODE_ERROR, "warning", null, null);
+ }
+ public static AjaxJson getWarning(String msg) {
+ return new AjaxJson(CODE_WARNING, msg, null, null);
+ }
+
+ // 返回未登录
+ public static AjaxJson getNotLogin() {
+ return new AjaxJson(CODE_NOT_LOGIN, "未登录,请登录后再次访问", null, null);
+ }
+
+ // 返回没有权限的
+ public static AjaxJson getNotJur(String msg) {
+ return new AjaxJson(CODE_NOT_JUR, msg, null, null);
+ }
+
+ // 返回一个自定义状态码的
+ public static AjaxJson get(int code, String msg){
+ return new AjaxJson(code, msg, null, null);
+ }
+
+ // 返回分页和数据的
+ public static AjaxJson getPageData(Long dataCount, Object data){
+ return new AjaxJson(CODE_SUCCESS, "ok", data, dataCount);
+ }
+
+ // 返回,根据受影响行数的(大于0=ok,小于0=error)
+ public static AjaxJson getByLine(int line){
+ if(line > 0){
+ return getSuccess("ok", line);
+ }
+ return getError("error").setData(line);
+ }
+
+ // 返回,根据布尔值来确定最终结果的 (true=ok,false=error)
+ public static AjaxJson getByBoolean(boolean b){
+ return b ? getSuccess("ok") : getError("error");
+ }
+
+ /* (non-Javadoc)
+ * @see java.lang.Object#toString()
+ */
+ @SuppressWarnings("rawtypes")
+ @Override
+ public String toString() {
+ String data_string = null;
+ if(data == null){
+
+ } else if(data instanceof List){
+ data_string = "List(length=" + ((List)data).size() + ")";
+ } else {
+ data_string = data.toString();
+ }
+ return "{"
+ + "\"code\": " + this.getCode()
+ + ", \"msg\": \"" + this.getMsg() + "\""
+ + ", \"data\": " + data_string
+ + ", \"dataCount\": " + dataCount
+ + "}";
+ }
+
+
+
+
+
+}
diff --git a/sa-token-demo/sa-token-demo-solon-redisson/src/main/java/com/pj/util/Ttime.java b/sa-token-demo/sa-token-demo-solon-redisson/src/main/java/com/pj/util/Ttime.java
new file mode 100644
index 00000000..dec4f1cb
--- /dev/null
+++ b/sa-token-demo/sa-token-demo-solon-redisson/src/main/java/com/pj/util/Ttime.java
@@ -0,0 +1,63 @@
+package com.pj.util;
+
+
+/**
+ * 用于测试用时
+ * @author click33
+ *
+ */
+public class Ttime {
+
+ private long start=0; //开始时间
+ private long end=0; //结束时间
+
+ public static Ttime t = new Ttime(); //static快捷使用
+
+ /**
+ * 开始计时
+ * @return
+ */
+ public Ttime start() {
+ start=System.currentTimeMillis();
+ return this;
+ }
+
+
+ /**
+ * 结束计时
+ */
+ public Ttime end() {
+ end=System.currentTimeMillis();
+ return this;
+ }
+
+
+ /**
+ * 返回所用毫秒数
+ */
+ public long returnMs() {
+ return end-start;
+ }
+
+ /**
+ * 格式化输出结果
+ */
+ public void outTime() {
+ System.out.println(this.toString());
+ }
+
+ /**
+ * 结束并格式化输出结果
+ */
+ public void endOutTime() {
+ this.end().outTime();
+ }
+
+ @Override
+ public String toString() {
+ return (returnMs() + 0.0) / 1000 + "s"; // 格式化为:0.01s
+ }
+
+
+
+}
diff --git a/sa-token-demo/sa-token-demo-solon-redisson/src/main/resources/app.yml b/sa-token-demo/sa-token-demo-solon-redisson/src/main/resources/app.yml
new file mode 100644
index 00000000..5a5a7c8b
--- /dev/null
+++ b/sa-token-demo/sa-token-demo-solon-redisson/src/main/resources/app.yml
@@ -0,0 +1,32 @@
+# 端口
+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: true
+ # token 风格(默认可取值:uuid、simple-uuid、random-32、random-64、random-128、tik)
+ token-style: uuid
+ # 是否输出操作日志
+ is-log: true
+
+
+
+sa-token-dao:
+ config: |
+ singleServerConfig:
+ password: "123456"
+ address: "redis://localhost:6379"
+ database: 0
+
+
+
\ No newline at end of file
diff --git a/sa-token-demo/sa-token-demo-solon/pom.xml b/sa-token-demo/sa-token-demo-solon/pom.xml
index a1c9f2c1..795ba01b 100644
--- a/sa-token-demo/sa-token-demo-solon/pom.xml
+++ b/sa-token-demo/sa-token-demo-solon/pom.xml
@@ -10,14 +10,13 @@
org.noear
solon-parent
- 2.2.3
+ 2.3.7
1.35.0.RC
- 2.2.3
UTF-8
UTF-8
@@ -28,13 +27,11 @@
org.noear
solon-web
- ${solon.version}
org.noear
- solon.logging
- ${solon.version}
+ solon.logging.simple
@@ -45,18 +42,11 @@
-
-
-
-
-
+
+
+ org.redisson
+ redisson
+ ${redisson.version}
+
+
org.redisson
redisson-spring-boot-starter
diff --git a/sa-token-doc/more/common-questions.md b/sa-token-doc/more/common-questions.md
index 6bc287ae..4d33b876 100644
--- a/sa-token-doc/more/common-questions.md
+++ b/sa-token-doc/more/common-questions.md
@@ -438,6 +438,25 @@ spring.mvc.throw-exception-if-no-handler-found=true
+### Q:开启了懒加载后,启动报“未能获取有效的上下文处理器”
+开启了`lazy-initialization=true`后,启动报异常 `InvalidContextException`: 未能获取有效的上下文处理器, 配置如下:
+``` yaml
+spring:
+ main:
+ lazy-initialization: true
+```
+原因是sa-token自动配置入口类SaTokenContextRegister被延迟加载了,只需要手动指定懒加载排除掉SaTokenContextRegister就可以了,实现代码如下:
+```java
+@Configuration
+class MyConfiguration {
+ @Bean
+ LazyInitializationExcludeFilter integrationLazyInitExcludeFilter() {
+ return LazyInitializationExcludeFilter.forBeanTypes(SaTokenContextRegister.class);
+ }
+}
+```
+[经验来源](https://gitee.com/dromara/sa-token/issues/I7EXIU)
+
diff --git a/sa-token-plugin/pom.xml b/sa-token-plugin/pom.xml
index 013c8d74..29999471 100644
--- a/sa-token-plugin/pom.xml
+++ b/sa-token-plugin/pom.xml
@@ -23,6 +23,7 @@
sa-token-redis-fastjson
sa-token-redis-fastjson2
sa-token-redisson-jackson
+ sa-token-redisson-jackson2
sa-token-redisx
sa-token-alone-redis
sa-token-dialect-thymeleaf
diff --git a/sa-token-plugin/sa-token-alone-redis/src/main/java/cn/dev33/satoken/dao/alone/SaAloneRedisInject.java b/sa-token-plugin/sa-token-alone-redis/src/main/java/cn/dev33/satoken/dao/alone/SaAloneRedisInject.java
index f97acc1f..abafe8c2 100644
--- a/sa-token-plugin/sa-token-alone-redis/src/main/java/cn/dev33/satoken/dao/alone/SaAloneRedisInject.java
+++ b/sa-token-plugin/sa-token-alone-redis/src/main/java/cn/dev33/satoken/dao/alone/SaAloneRedisInject.java
@@ -74,10 +74,6 @@ public class SaAloneRedisInject implements EnvironmentAware{
if(saTokenDao == null || saTokenDao instanceof SaTokenDaoDefaultImpl) {
return;
}
- // 如果配置文件不包含相关配置,则不进行任何操作
- if(environment.getProperty(ALONE_PREFIX + ".host") == null) {
- return;
- }
// ------------------- 开始注入
diff --git a/sa-token-plugin/sa-token-redisson-jackson2/README.md b/sa-token-plugin/sa-token-redisson-jackson2/README.md
new file mode 100644
index 00000000..b6dbf0db
--- /dev/null
+++ b/sa-token-plugin/sa-token-redisson-jackson2/README.md
@@ -0,0 +1,100 @@
+## sa-token-redisson-jackson2
+
+此扩展,不与生态绑定。可用于不同的生态。
+
+### 1、例 solon 集成
+
+添加关键依赖
+
+```xml
+
+
+ cn.dev33
+ sa-token-redisson-jackson2
+ ${sa-token.version}
+
+
+ org.noear
+ redisson-solon-plugin
+ ${solon.version}
+
+
+```
+
+添加 dao 配置
+
+```yaml
+sa-token-dao:
+ config: |
+ singleServerConfig:
+ password: "123456"
+ address: "redis://localhost:6379"
+ database: 0
+```
+
+开始组装
+
+```java
+@Configuration
+public class SaTokenConfigure {
+ /**
+ * 构造 RedissonClient
+ * */
+ @Bean
+ public RedissonClient saTokenDaoInit(@Inject("${sa-token-dao}") RedissonSupplier supplier) {
+ return supplier.get();
+ }
+
+ /**
+ * 构建 SaTokenDao
+ * */
+ @Bean
+ public SaTokenDao saTokenDaoInit(RedissonClient redissonClient) {
+ return new SaTokenDaoRedissonJackson(redissonClient);
+ }
+}
+```
+
+
+### 2、例 springboot 集成
+
+
+添加关键依赖
+
+```xml
+
+
+ cn.dev33
+ sa-token-redisson-jackson2
+ ${sa-token.version}
+
+
+ org.redisson
+ redisson-spring-boot-starter
+ ${redisson.version}
+
+
+```
+
+添加 dao 配置
+
+```yaml
+spring.redis:
+ redisson:
+ file: classpath:redisson.yml
+```
+
+开始组装
+
+```java
+@Configuration
+public class SaTokenConfigure {
+ /**
+ * 构建 SaTokenDao
+ * */
+ @Bean
+ public SaTokenDao saTokenDaoInit(RedissonClient redissonClient) {
+ return new SaTokenDaoRedissonJackson(redissonClient);
+ }
+}
+```
\ No newline at end of file
diff --git a/sa-token-plugin/sa-token-redisson-jackson2/pom.xml b/sa-token-plugin/sa-token-redisson-jackson2/pom.xml
new file mode 100644
index 00000000..007bb147
--- /dev/null
+++ b/sa-token-plugin/sa-token-redisson-jackson2/pom.xml
@@ -0,0 +1,48 @@
+
+
+ 4.0.0
+
+
+ cn.dev33
+ sa-token-plugin
+ ${revision}
+ ../pom.xml
+
+ jar
+
+ sa-token-redisson-jackson2
+ sa-token-redisson-jackson2
+ sa-token integrate redisson (to jackson)
+
+
+
+
+ cn.dev33
+ sa-token-core
+
+
+
+ org.redisson
+ redisson
+
+
+
+
+ com.fasterxml.jackson.core
+ jackson-databind
+ true
+
+
+
+ com.fasterxml.jackson.datatype
+ jackson-datatype-jsr310
+ true
+
+
+
+
+
+
+
diff --git a/sa-token-plugin/sa-token-redisson-jackson2/src/main/java/cn/dev33/satoken/dao/SaSessionForJacksonCustomized.java b/sa-token-plugin/sa-token-redisson-jackson2/src/main/java/cn/dev33/satoken/dao/SaSessionForJacksonCustomized.java
new file mode 100644
index 00000000..fe56ca4f
--- /dev/null
+++ b/sa-token-plugin/sa-token-redisson-jackson2/src/main/java/cn/dev33/satoken/dao/SaSessionForJacksonCustomized.java
@@ -0,0 +1,47 @@
+/*
+ * 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.dao;
+
+import cn.dev33.satoken.session.SaSession;
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+
+/**
+ * Jackson定制版SaSession,忽略 timeout 等属性的序列化
+ *
+ * @author click33
+ * @since 1.34.0
+ */
+@JsonIgnoreProperties({"timeout"})
+public class SaSessionForJacksonCustomized extends SaSession {
+
+ /**
+ *
+ */
+ private static final long serialVersionUID = -7600983549653130681L;
+
+ public SaSessionForJacksonCustomized() {
+ super();
+ }
+
+ /**
+ * 构建一个Session对象
+ * @param id Session的id
+ */
+ public SaSessionForJacksonCustomized(String id) {
+ super(id);
+ }
+
+}
diff --git a/sa-token-plugin/sa-token-redisson-jackson2/src/main/java/cn/dev33/satoken/dao/SaTokenDaoRedissonJackson.java b/sa-token-plugin/sa-token-redisson-jackson2/src/main/java/cn/dev33/satoken/dao/SaTokenDaoRedissonJackson.java
new file mode 100644
index 00000000..fa98f61c
--- /dev/null
+++ b/sa-token-plugin/sa-token-redisson-jackson2/src/main/java/cn/dev33/satoken/dao/SaTokenDaoRedissonJackson.java
@@ -0,0 +1,292 @@
+/*
+ * 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.dao;
+
+import java.time.Duration;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.time.LocalTime;
+import java.time.format.DateTimeFormatter;
+import java.util.List;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+import com.fasterxml.jackson.annotation.JsonTypeInfo;
+import org.redisson.api.RBatch;
+import org.redisson.api.RBucket;
+import org.redisson.api.RBucketAsync;
+import org.redisson.api.RedissonClient;
+import org.redisson.client.codec.Codec;
+import org.redisson.codec.JsonJacksonCodec;
+
+import com.fasterxml.jackson.databind.DeserializationFeature;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
+import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateDeserializer;
+import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer;
+import com.fasterxml.jackson.datatype.jsr310.deser.LocalTimeDeserializer;
+import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateSerializer;
+import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer;
+import com.fasterxml.jackson.datatype.jsr310.ser.LocalTimeSerializer;
+
+import cn.dev33.satoken.strategy.SaStrategy;
+import cn.dev33.satoken.util.SaFoxUtil;
+
+/**
+ * Sa-Token 持久层实现 [ Redisson客户端、Redis存储、Jackson序列化 ]
+ *
+ * @author 疯狂的狮子Li
+ * @author noear
+ * @since 1.34.0
+ */
+public class SaTokenDaoRedissonJackson implements SaTokenDao {
+
+ public static final String DATE_TIME_PATTERN = "yyyy-MM-dd HH:mm:ss";
+ public static final String DATE_PATTERN = "yyyy-MM-dd";
+ public static final String TIME_PATTERN = "HH:mm:ss";
+ public static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern(DATE_TIME_PATTERN);
+ public static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern(DATE_PATTERN);
+ public static final DateTimeFormatter TIME_FORMATTER = DateTimeFormatter.ofPattern(TIME_PATTERN);
+
+ /**
+ * ObjectMapper 对象 (以 public 作用域暴露出此对象,方便开发者二次更改配置)
+ *
+ * 例如:
+ *
+ * SaTokenDaoRedisJackson redisJackson = (SaTokenDaoRedisJackson) SaManager.getSaTokenDao();
+ * redisJackson.objectMapper.xxx = xxx;
+ *
+ *
+ */
+ public final ObjectMapper objectMapper;
+
+ /**
+ * 序列化方式
+ */
+ public final Codec codec;
+
+ /**
+ * redisson 客户端
+ */
+ public final RedissonClient redissonClient;
+
+ public SaTokenDaoRedissonJackson(RedissonClient redissonClient) {
+ this.objectMapper = new ObjectMapper();
+
+ this.objectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY);
+
+ // 配置[忽略未知字段]
+ this.objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
+
+ // 配置[时间类型转换]
+ JavaTimeModule timeModule = new JavaTimeModule();
+ // LocalDateTime序列化与反序列化
+ timeModule.addSerializer(new LocalDateTimeSerializer(DATE_TIME_FORMATTER));
+ timeModule.addDeserializer(LocalDateTime.class, new LocalDateTimeDeserializer(DATE_TIME_FORMATTER));
+ // LocalDate序列化与反序列化
+ timeModule.addSerializer(new LocalDateSerializer(DATE_FORMATTER));
+ timeModule.addDeserializer(LocalDate.class, new LocalDateDeserializer(DATE_FORMATTER));
+ // LocalTime序列化与反序列化
+ timeModule.addSerializer(new LocalTimeSerializer(TIME_FORMATTER));
+ timeModule.addDeserializer(LocalTime.class, new LocalTimeDeserializer(TIME_FORMATTER));
+ this.objectMapper.registerModule(timeModule);
+
+ // 重写 SaSession 生成策略
+ SaStrategy.instance.createSession = (sessionId) -> new SaSessionForJacksonCustomized(sessionId);
+
+
+ // 开始初始化相关组件
+ this.codec = new JsonJacksonCodec(objectMapper);
+ this.redissonClient = redissonClient;
+ }
+
+
+ /**
+ * 获取Value,如无返空
+ */
+ @Override
+ public String get(String key) {
+ RBucket rBucket = redissonClient.getBucket(key, codec);
+ return rBucket.get();
+ }
+
+ /**
+ * 写入Value,并设定存活时间 (单位: 秒)
+ */
+ @Override
+ public void set(String key, String value, long timeout) {
+ if(timeout == 0 || timeout <= SaTokenDao.NOT_VALUE_EXPIRE) {
+ return;
+ }
+ // 判断是否为永不过期
+ if(timeout == SaTokenDao.NEVER_EXPIRE) {
+ RBucket bucket = redissonClient.getBucket(key, codec);
+ bucket.set(value);
+ } else {
+ RBatch batch = redissonClient.createBatch();
+ RBucketAsync bucket = batch.getBucket(key, codec);
+ bucket.setAsync(value);
+ bucket.expireAsync(Duration.ofSeconds(timeout));
+ batch.execute();
+ }
+ }
+
+ /**
+ * 修修改指定key-value键值对 (过期时间不变)
+ */
+ @Override
+ public void update(String key, String value) {
+ long expire = getTimeout(key);
+ // -2 = 无此键
+ if(expire == SaTokenDao.NOT_VALUE_EXPIRE) {
+ return;
+ }
+ this.set(key, value, expire);
+ }
+
+ /**
+ * 删除Value
+ */
+ @Override
+ public void delete(String key) {
+ redissonClient.getBucket(key, codec).delete();
+ }
+
+ /**
+ * 获取Value的剩余存活时间 (单位: 秒)
+ */
+ @Override
+ public long getTimeout(String key) {
+ RBucket rBucket = redissonClient.getBucket(key, codec);
+ long timeout = rBucket.remainTimeToLive();
+ return timeout < 0 ? timeout : timeout / 1000;
+ }
+
+ /**
+ * 修改Value的剩余存活时间 (单位: 秒)
+ */
+ @Override
+ public void updateTimeout(String key, long timeout) {
+ // 判断是否想要设置为永久
+ if(timeout == SaTokenDao.NEVER_EXPIRE) {
+ long expire = getTimeout(key);
+ if(expire == SaTokenDao.NEVER_EXPIRE) {
+ // 如果其已经被设置为永久,则不作任何处理
+ } else {
+ // 如果尚未被设置为永久,那么再次set一次
+ this.set(key, this.get(key), timeout);
+ }
+ return;
+ }
+ RBucket rBucket = redissonClient.getBucket(key, codec);
+ rBucket.expire(Duration.ofSeconds(timeout));
+ }
+
+
+
+ /**
+ * 获取Object,如无返空
+ */
+ @Override
+ public Object getObject(String key) {
+ RBucket