diff --git a/README.md b/README.md index bb1bfc243bf95e49679c19075e0ebf546c0ec800..a5b85eabfef2227ec49550a2bcddae6fb1613d57 100644 --- a/README.md +++ b/README.md @@ -11,12 +11,13 @@ Sa-Token 三方插件合集,希望本仓库可以最大程度汇集社区的 #### 插件列表 目前已开发的插件列表: -| 插件 | 作者 | 介绍 | 是否已发布 | 使用文档 | -| :-------- | :-------- | :-------- | :-------- | :-------- | -| sa-token-three-example-plugin | 孔明 | 为第三方插件开发时提供的示例工程 | 未发布 | [详情](sa-token-three-example-plugin/README.md) | -| sa-token-three-redis-jackson-add-prefix | RockMan | 为 Sa-Token 在 Redis 中的 key 添加上指定前缀 | 未发布 | [详情](sa-token-three-redis-jackson-add-prefix/README.md) | -| sa-token-three-custom-check-permission | RockMan | 自定义 Sa-Token 的鉴权逻辑,不再一次性返回整个权限码集合给框架判断,而是根据自定义验证规则返回 true 或 false 给框架 | 未发布 | [详情](sa-token-three-custom-check-permission/README.md) | -| sa-token-three-token-prefix-compatible-cookie | 就剩一个桃 | 让 sa-token 在打开前缀模式时,Cookie 鉴权依然生效 | 未发布 | [详情](sa-token-three-token-prefix-compatible-cookie/README.md) | +| 插件 | 作者 | 介绍 | 是否已发布 | 使用文档 | +| :-------- |:---------|:-----------------------------------------------------------------------| :-------- |:---------------------------------------------------------------| +| sa-token-three-example-plugin | 孔明 | 为第三方插件开发时提供的示例工程 | 未发布 | [详情](sa-token-three-example-plugin/README.md) | +| sa-token-three-redis-jackson-add-prefix | RockMan | 为 Sa-Token 在 Redis 中的 key 添加上指定前缀 | 未发布 | [详情](sa-token-three-redis-jackson-add-prefix/README.md) | +| sa-token-three-custom-check-permission | RockMan | 自定义 Sa-Token 的鉴权逻辑,不再一次性返回整个权限码集合给框架判断,而是根据自定义验证规则返回 true 或 false 给框架 | 未发布 | [详情](sa-token-three-custom-check-permission/README.md) | +| sa-token-three-token-prefix-compatible-cookie | 就剩一个桃 | 让 sa-token 在打开前缀模式时,Cookie 鉴权依然生效 | 未发布 | [详情](sa-token-three-token-prefix-compatible-cookie/README.md) | +| sa-token-three-jdbc-fastjson2 | moon69 | 数据库持久化插件 | 未发布 | [详情](sa-token-three-jdbc-fastjson2/README.md) | #### 使用方式 diff --git a/pom.xml b/pom.xml index 186446e50549ef883ac89242e04dc9db3a451836..6a45e430e731d292d9e40b6af7e21769ca33ca8d 100644 --- a/pom.xml +++ b/pom.xml @@ -22,6 +22,7 @@ sa-token-three-redis-jackson-add-prefix sa-token-three-custom-check-permission sa-token-three-token-prefix-compatible-cookie + sa-token-three-jdbc-fastjson2 diff --git a/sa-token-three-jdbc-fastjson2/README.md b/sa-token-three-jdbc-fastjson2/README.md new file mode 100644 index 0000000000000000000000000000000000000000..86524f0d4c1a088a2810fc02ac57f093b8fc2a87 --- /dev/null +++ b/sa-token-three-jdbc-fastjson2/README.md @@ -0,0 +1,46 @@ +## sa-token-three-jdbc-fastjson2 + +### 插件介绍 +Sa-Token 数据库持久化插件。 + +### 使用方式 +1. 引入插件: +``` xml + + cn.dev33 + sa-token-three-jdbc-fastjson2 + ${sa-token.version} + +``` + +2. 在 application.yml 配置数据源: +``` yaml +spring: + datasource: + url: jdbc:mysql://localhost:3306/test + username: test + password: test +``` + +3. 在启动类配置注解: +``` java +@MapperScan(basePackages = "cn.dev33.satoken.dao.mapper") +``` + +4. 在数据库新增表结构,以下为 MySQL 的表结构: +``` mysql +# 本插件使用 mybatis-plus 作为 ORM 框架, mybatis-plus 支持的数据库本插件应该都支持 +# token_value 的长度可根据实际需求进行修改 + +CREATE TABLE `sa_token_data` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `token_key` varchar(255) NOT NULL COMMENT '键', + `token_value` varchar(1000) NOT NULL COMMENT '值', + `expire_time` bigint NOT NULL COMMENT '过期时间戳', + PRIMARY KEY (`id`), + UNIQUE KEY `uk_key` (`token_key`) +) ENGINE=InnoDB; +``` + +### 联系方式 +使用时如遇问题,请在 sa-token-three-plugin 中提交 issue 咨询 \ No newline at end of file diff --git a/sa-token-three-jdbc-fastjson2/pom.xml b/sa-token-three-jdbc-fastjson2/pom.xml new file mode 100644 index 0000000000000000000000000000000000000000..1210b1a51367ba974002844463447b6929853331 --- /dev/null +++ b/sa-token-three-jdbc-fastjson2/pom.xml @@ -0,0 +1,37 @@ + + + + sa-token-three-plugin + cn.dev33 + ${revision} + ../pom.xml + + 4.0.0 + + sa-token-three-jdbc-fastjson2 + + + 3.5.3.2 + + + + + + cn.dev33 + sa-token-core + + + + com.alibaba.fastjson2 + fastjson2 + + + + com.baomidou + mybatis-plus-boot-starter + ${mybatis-plus.version} + + + \ No newline at end of file diff --git a/sa-token-three-jdbc-fastjson2/src/main/java/cn/dev33/satoken/dao/SaSessionForJdbcCustomized.java b/sa-token-three-jdbc-fastjson2/src/main/java/cn/dev33/satoken/dao/SaSessionForJdbcCustomized.java new file mode 100644 index 0000000000000000000000000000000000000000..77c5f932f9bbc3f324437d99d04295b302b1cdda --- /dev/null +++ b/sa-token-three-jdbc-fastjson2/src/main/java/cn/dev33/satoken/dao/SaSessionForJdbcCustomized.java @@ -0,0 +1,74 @@ +/* + * 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 cn.dev33.satoken.util.SaFoxUtil; +import com.alibaba.fastjson2.JSON; +import com.alibaba.fastjson2.annotation.JSONField; + + +/** + * 定制版 SaSession + * + * @author moon69 + * @since 1.37.0 + */ +public class SaSessionForJdbcCustomized extends SaSession { + + private static final long serialVersionUID = 4855910561516650589L; + + public SaSessionForJdbcCustomized() { + super(); + } + + public SaSessionForJdbcCustomized(String id) { + super(id); + } + + @Override + public T getModel(String key, Class cs) { + if (SaFoxUtil.isBasicType(cs)) { + return SaFoxUtil.getValueByType(get(key), cs); + } + + return JSON.parseObject(getString(key), cs); + } + + @Override + @SuppressWarnings("unchecked") + public T getModel(String key, Class cs, Object defaultValue) { + Object value = get(key); + if (valueIsNull(value)) { + return (T) defaultValue; + } + + if (SaFoxUtil.isBasicType(cs)) { + return SaFoxUtil.getValueByType(get(key), cs); + } + + return JSON.parseObject(getString(key), cs); + } + + /** + * 忽略 timeout 字段的序列化 + */ + @Override + @JSONField(serialize = false) + public long getTimeout() { + return super.getTimeout(); + } +} diff --git a/sa-token-three-jdbc-fastjson2/src/main/java/cn/dev33/satoken/dao/SaTokenDaoJdbcImpl.java b/sa-token-three-jdbc-fastjson2/src/main/java/cn/dev33/satoken/dao/SaTokenDaoJdbcImpl.java new file mode 100644 index 0000000000000000000000000000000000000000..6deb92b0d4adc56a4e7452f23da826b2a6b4a111 --- /dev/null +++ b/sa-token-three-jdbc-fastjson2/src/main/java/cn/dev33/satoken/dao/SaTokenDaoJdbcImpl.java @@ -0,0 +1,307 @@ +/* + * 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.SaManager; +import cn.dev33.satoken.dao.entity.SaTokenData; +import cn.dev33.satoken.dao.mapper.SaTokenDataMapper; +import cn.dev33.satoken.strategy.SaStrategy; +import cn.dev33.satoken.util.SaFoxUtil; +import com.alibaba.fastjson2.JSON; +import com.alibaba.fastjson2.JSONReader; +import com.alibaba.fastjson2.JSONWriter; +import org.springframework.stereotype.Component; + +import java.util.List; + +/** + * Sa-Token 持久层实现 [ 数据库存储、FastJson2序列化 ] + * + * @author moon69 + * @since 1.37.0 + */ +@Component +public class SaTokenDaoJdbcImpl implements SaTokenDao { + + private SaTokenDataMapper saTokenDataMapper; + + public SaTokenDaoJdbcImpl(SaTokenDataMapper saTokenDataMapper) { + this.saTokenDataMapper = saTokenDataMapper; + + // 重写 SaSession 生成策略 + SaStrategy.instance.createSession = SaSessionForJdbcCustomized::new; + } + + // --------------------- 字符串读写 --------------------- + + @Override + public String get(String key) { + SaTokenData saTokenData = saTokenDataMapper.findByTokenKey(key); + if (clearKey(saTokenData)) { + return null; + } + + return saTokenData.getTokenValue(); + } + + @Override + public void set(String key, String value, long timeout) { + if (timeout == 0 || timeout <= SaTokenDao.NOT_VALUE_EXPIRE) { + return; + } + + SaTokenData saTokenData = saTokenDataMapper.findByTokenKey(key); + Long expireTime = (timeout == SaTokenDao.NEVER_EXPIRE) ? (SaTokenDao.NEVER_EXPIRE) : (System.currentTimeMillis() + timeout * 1000); + // 不存在则新增,存在则更新 + if (saTokenData == null) { + saTokenData = new SaTokenData(); + saTokenData.setTokenKey(key); + saTokenData.setTokenValue(value); + saTokenData.setExpireTime(expireTime); + saTokenDataMapper.insert(saTokenData); + } else { + saTokenData.setTokenValue(value); + saTokenData.setExpireTime(expireTime); + saTokenDataMapper.updateById(saTokenData); + } + } + + @Override + public void update(String key, String value) { + SaTokenData saTokenData = saTokenDataMapper.findByTokenKey(key); + if (getKeyTimeout(saTokenData) == SaTokenDao.NOT_VALUE_EXPIRE) { + return; + } + + saTokenData.setTokenValue(value); + saTokenDataMapper.updateById(saTokenData); + } + + @Override + public void delete(String key) { + saTokenDataMapper.deleteByTokenKey(key); + } + + @Override + public long getTimeout(String key) { + return getKeyTimeout(key); + } + + @Override + public void updateTimeout(String key, long timeout) { + saTokenDataMapper.updateExpireTime(key, (timeout == SaTokenDao.NEVER_EXPIRE) ? (SaTokenDao.NEVER_EXPIRE) : (System.currentTimeMillis() + timeout * 1000)); + } + + // --------------------- 对象读写 --------------------- + + @Override + public Object getObject(String key) { + SaTokenData saTokenData = saTokenDataMapper.findByTokenKey(key); + if (clearKey(saTokenData)) { + return null; + } + + return JSON.parseObject(saTokenData.getTokenValue(), Object.class, JSONReader.Feature.SupportAutoType); + } + + @Override + public void setObject(String key, Object object, long timeout) { + if (timeout == 0 || timeout <= SaTokenDao.NOT_VALUE_EXPIRE) { + return; + } + + SaTokenData saTokenData = saTokenDataMapper.findByTokenKey(key); + Long expireTime = (timeout == SaTokenDao.NEVER_EXPIRE) ? (SaTokenDao.NEVER_EXPIRE) : (System.currentTimeMillis() + timeout * 1000); + // 不存在则新增,存在则更新 + if (saTokenData == null) { + saTokenData = new SaTokenData(); + saTokenData.setTokenKey(key); + saTokenData.setTokenValue(JSON.toJSONString(object, JSONWriter.Feature.WriteClassName)); + saTokenData.setExpireTime(expireTime); + saTokenDataMapper.insert(saTokenData); + } else { + saTokenData.setTokenValue(JSON.toJSONString(object, JSONWriter.Feature.WriteClassName)); + saTokenData.setExpireTime(expireTime); + saTokenDataMapper.updateById(saTokenData); + } + } + + @Override + public void updateObject(String key, Object object) { + SaTokenData saTokenData = saTokenDataMapper.findByTokenKey(key); + if (getKeyTimeout(saTokenData) == SaTokenDao.NOT_VALUE_EXPIRE) { + return; + } + + saTokenData.setTokenValue(JSON.toJSONString(object, JSONWriter.Feature.WriteClassName)); + saTokenDataMapper.updateById(saTokenData); + } + + @Override + public void deleteObject(String key) { + saTokenDataMapper.deleteByTokenKey(key); + } + + @Override + public long getObjectTimeout(String key) { + return getKeyTimeout(key); + } + + @Override + public void updateObjectTimeout(String key, long timeout) { + saTokenDataMapper.updateExpireTime(key, (timeout == SaTokenDao.NEVER_EXPIRE) ? (SaTokenDao.NEVER_EXPIRE) : (System.currentTimeMillis() + timeout * 1000)); + } + + // --------------------- 会话管理 --------------------- + + @Override + public List searchData(String prefix, String keyword, int start, int size, boolean sortType) { + List keyList = saTokenDataMapper.findKeyList(prefix, keyword); + return SaFoxUtil.searchList(keyList, start, size, sortType); + } + + // --------------------- 数据过期 --------------------- + + /** + * @param saTokenData saToken数据 + * @return true-数据过期被清理 false-数据存活 + */ + public boolean clearKey(SaTokenData saTokenData) { + // 不存在则当作已被清理 + if (saTokenData == null) { + return true; + } + + // 删除过期数据 + Long expireTime = saTokenData.getExpireTime(); + if (expireTime != SaTokenDao.NEVER_EXPIRE && expireTime < System.currentTimeMillis()) { + saTokenDataMapper.deleteById(saTokenData); + return true; + } + + return false; + } + + /** + * 获取指定 key 的剩余存活时间 (单位:秒) + * + * @param key 指定 key + * @return 这个 key 的剩余存活时间 + */ + public long getKeyTimeout(String key) { + SaTokenData saTokenData = saTokenDataMapper.findByTokenKey(key); + return getKeyTimeout(saTokenData); + } + + /** + * 获取指定 key 的剩余存活时间 (单位:秒) + * + * @param saTokenData saToken数据 + * @return 这个 key 的剩余存活时间 + */ + public long getKeyTimeout(SaTokenData saTokenData) { + // 由于数据过期检测属于惰性扫描,很可能此时这个 key 已经是过期状态了,所以这里需要先检查一下 + if (clearKey(saTokenData)) { + return SaTokenDao.NOT_VALUE_EXPIRE; + } + + // 如果 expire 被标注为永不过期,则返回 NEVER_EXPIRE + if (saTokenData.getExpireTime() == SaTokenDao.NEVER_EXPIRE) { + return SaTokenDao.NEVER_EXPIRE; + } + + // 计算剩余时间并返回 (过期时间戳 - 当前时间戳) / 1000 转秒 + long timeout = (saTokenData.getExpireTime() - System.currentTimeMillis()) / 1000; + + // 小于零时,视为不存在 + if (timeout < 0) { + saTokenDataMapper.deleteByTokenKey(saTokenData.getTokenKey()); + return SaTokenDao.NOT_VALUE_EXPIRE; + } + + return timeout; + } + + // --------------------- 定时清理过期数据 --------------------- + + /** + * 执行数据清理的线程引用 + */ + public Thread refreshThread; + + /** + * 是否继续执行数据清理的线程标记 + */ + public volatile boolean refreshFlag; + + /** + * 初始化定时任务,定时清理过期数据 + */ + public void initRefreshThread() { + // 如果开发者配置了 <=0 的值,则不启动定时清理 + if (SaManager.getConfig().getDataRefreshPeriod() <= 0) { + return; + } + + refreshFlag = true; + refreshThread = new Thread(() -> { + for (; ; ) { + try { + try { + // 如果已经被标记为结束 + if (!refreshFlag) { + return; + } + + // 执行清理 + refreshDataMap(); + } catch (Exception e) { + e.printStackTrace(); + } + + // 休眠N秒 + int dataRefreshPeriod = SaManager.getConfig().getDataRefreshPeriod(); + if (dataRefreshPeriod <= 0) { + dataRefreshPeriod = 1; + } + Thread.sleep(dataRefreshPeriod * 1000L); + } catch (Exception e) { + e.printStackTrace(); + } + } + }); + refreshThread.start(); + } + + /** + * 清理所有已经过期的 key + */ + public void refreshDataMap() { + saTokenDataMapper.deleteByExpireTime(0L, System.currentTimeMillis()); + } + + @Override + public void init() { + // 定时清理过期数据 + initRefreshThread(); + } + + @Override + public void destroy() { + // 不再定时清理过期数据 + refreshFlag = false; + } +} diff --git a/sa-token-three-jdbc-fastjson2/src/main/java/cn/dev33/satoken/dao/entity/SaTokenData.java b/sa-token-three-jdbc-fastjson2/src/main/java/cn/dev33/satoken/dao/entity/SaTokenData.java new file mode 100644 index 0000000000000000000000000000000000000000..554090d7dcb032dbeb3420a0c778172e9444bc2e --- /dev/null +++ b/sa-token-three-jdbc-fastjson2/src/main/java/cn/dev33/satoken/dao/entity/SaTokenData.java @@ -0,0 +1,54 @@ +package cn.dev33.satoken.dao.entity; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; + +/** + * sa_token_data 实体类 + * + * @author moon69 + * @since 1.37.0 + */ +public class SaTokenData { + + @TableId(type = IdType.AUTO) + private Long id; + + private String tokenKey; + + private String tokenValue; + + private Long expireTime; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getTokenKey() { + return tokenKey; + } + + public void setTokenKey(String tokenKey) { + this.tokenKey = tokenKey; + } + + public String getTokenValue() { + return tokenValue; + } + + public void setTokenValue(String tokenValue) { + this.tokenValue = tokenValue; + } + + public Long getExpireTime() { + return expireTime; + } + + public void setExpireTime(Long expireTime) { + this.expireTime = expireTime; + } +} diff --git a/sa-token-three-jdbc-fastjson2/src/main/java/cn/dev33/satoken/dao/mapper/SaTokenDataMapper.java b/sa-token-three-jdbc-fastjson2/src/main/java/cn/dev33/satoken/dao/mapper/SaTokenDataMapper.java new file mode 100644 index 0000000000000000000000000000000000000000..1509922f9813efe09cc2f917640c47153c3ae83f --- /dev/null +++ b/sa-token-three-jdbc-fastjson2/src/main/java/cn/dev33/satoken/dao/mapper/SaTokenDataMapper.java @@ -0,0 +1,86 @@ +package cn.dev33.satoken.dao.mapper; + +import cn.dev33.satoken.dao.entity.SaTokenData; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; +import com.baomidou.mybatisplus.core.mapper.BaseMapper; + +import java.util.List; +import java.util.stream.Collectors; + +/** + * sa_token_data Mapper + * + * @author moon69 + * @since 1.37.0 + */ +public interface SaTokenDataMapper extends BaseMapper { + + /** + * select * from sa_token_data where token_key = tokenKey + * + * @param tokenKey 键 + * @return SaTokenData + */ + default SaTokenData findByTokenKey(String tokenKey) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper() + .eq(SaTokenData::getTokenKey, tokenKey); + return this.selectOne(wrapper); + } + + /** + * select token_key from where token_key like 'prefix%' and token_key like '%keyword%' + * + * @param prefix 前缀 + * @param keyword 关键字 + * @return 查询到的数据集合 + */ + default List findKeyList(String prefix, String keyword) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper() + .select(SaTokenData::getTokenKey) + .likeRight(SaTokenData::getTokenKey, prefix) + .like(SaTokenData::getTokenKey, keyword); + return this.selectList(wrapper) + .stream() + .map(SaTokenData::getTokenKey) + .collect(Collectors.toList()); + } + + /** + * delete from sa_token_data where token_key = tokenKey + * + * @param tokenKey 键 + */ + default void deleteByTokenKey(String tokenKey) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper() + .eq(SaTokenData::getTokenKey, tokenKey); + this.delete(wrapper); + } + + /** + * delete from sa_token_data where expire_time > leftTime and expire_time < rightTime + * + * @param leftTime 大于的时间戳 + * @param rightTime 小于的时间戳 + */ + default void deleteByExpireTime(Long leftTime, Long rightTime) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper() + .gt(SaTokenData::getExpireTime, leftTime) + .lt(SaTokenData::getExpireTime, rightTime); + this.delete(wrapper); + } + + /** + * update sa_token_data set expire_time = expireTime where token_key = tokenKey + * + * @param tokenKey 键 + * @param expireTime 过期时间 + */ + default void updateExpireTime(String tokenKey, Long expireTime) { + LambdaUpdateWrapper wrapper = new LambdaUpdateWrapper() + .set(SaTokenData::getExpireTime, expireTime) + .eq(SaTokenData::getTokenKey, tokenKey); + this.update(null, wrapper); + } + +} diff --git a/sa-token-three-jdbc-fastjson2/src/main/resources/META-INF/spring.factories b/sa-token-three-jdbc-fastjson2/src/main/resources/META-INF/spring.factories new file mode 100644 index 0000000000000000000000000000000000000000..828b9962c0e0ba1bbd1f84ec242e57e404887459 --- /dev/null +++ b/sa-token-three-jdbc-fastjson2/src/main/resources/META-INF/spring.factories @@ -0,0 +1 @@ +org.springframework.boot.autoconfigure.EnableAutoConfiguration=cn.dev33.satoken.dao.SaTokenDaoJdbcImpl \ No newline at end of file diff --git a/sa-token-three-jdbc-fastjson2/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/sa-token-three-jdbc-fastjson2/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 0000000000000000000000000000000000000000..16d02c951af3d9343567844002866474eee25787 --- /dev/null +++ b/sa-token-three-jdbc-fastjson2/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1 @@ +cn.dev33.satoken.dao.SaTokenDaoJdbcImpl \ No newline at end of file