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 + 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 rBucket = redissonClient.getBucket(key, codec); + return rBucket.get(); + } + + /** + * 写入Object,并设定存活时间 (单位: 秒) + */ + @Override + public void setObject(String key, Object object, long timeout) { + if(timeout == 0 || timeout <= SaTokenDao.NOT_VALUE_EXPIRE) { + return; + } + // 判断是否为永不过期 + if(timeout == SaTokenDao.NEVER_EXPIRE) { + RBucket bucket = redissonClient.getBucket(key, codec); + bucket.set(object); + } else { + RBatch batch = redissonClient.createBatch(); + RBucketAsync bucket = batch.getBucket(key, codec); + bucket.setAsync(object); + bucket.expireAsync(Duration.ofSeconds(timeout)); + batch.execute(); + } + + } + + /** + * 更新Object (过期时间不变) + */ + @Override + public void updateObject(String key, Object object) { + long expire = getObjectTimeout(key); + // -2 = 无此键 + if(expire == SaTokenDao.NOT_VALUE_EXPIRE) { + return; + } + this.setObject(key, object, expire); + } + + /** + * 删除Object + */ + @Override + public void deleteObject(String key) { + redissonClient.getBucket(key, codec).delete(); + } + + /** + * 获取Object的剩余存活时间 (单位: 秒) + */ + @Override + public long getObjectTimeout(String key) { + RBucket rBucket = redissonClient.getBucket(key, codec); + long timeout = rBucket.remainTimeToLive(); + return timeout < 0 ? timeout : timeout / 1000; + } + + /** + * 修改Object的剩余存活时间 (单位: 秒) + */ + @Override + public void updateObjectTimeout(String key, long timeout) { + // 判断是否想要设置为永久 + if(timeout == SaTokenDao.NEVER_EXPIRE) { + long expire = getObjectTimeout(key); + if(expire == SaTokenDao.NEVER_EXPIRE) { + // 如果其已经被设置为永久,则不作任何处理 + } else { + // 如果尚未被设置为永久,那么再次set一次 + this.setObject(key, this.getObject(key), timeout); + } + return; + } + RBucket rBucket = redissonClient.getBucket(key, codec); + rBucket.expire(Duration.ofSeconds(timeout)); + } + + + /** + * 搜索数据 + */ + @Override + public List searchData(String prefix, String keyword, int start, int size, boolean sortType) { + Stream stream = redissonClient.getKeys().getKeysStreamByPattern(prefix + "*" + keyword + "*"); + List list = stream.collect(Collectors.toList()); + return SaFoxUtil.searchList(list, start, size, sortType); + } +}