diff --git a/sa-token-core/src/main/java/cn/dev33/satoken/SaManager.java b/sa-token-core/src/main/java/cn/dev33/satoken/SaManager.java index 8d7d54b3..5b73c86e 100644 --- a/sa-token-core/src/main/java/cn/dev33/satoken/SaManager.java +++ b/sa-token-core/src/main/java/cn/dev33/satoken/SaManager.java @@ -41,8 +41,7 @@ import cn.dev33.satoken.stp.StpInterfaceDefaultImpl; import cn.dev33.satoken.stp.StpLogic; import cn.dev33.satoken.stp.StpUtil; import cn.dev33.satoken.strategy.SaStrategy; -import cn.dev33.satoken.temp.SaTempDefaultImpl; -import cn.dev33.satoken.temp.SaTempInterface; +import cn.dev33.satoken.temp.SaTempTemplate; import cn.dev33.satoken.util.SaFoxUtil; import java.util.LinkedHashMap; @@ -167,20 +166,20 @@ public class SaManager { /** * 临时 token 认证模块 */ - private volatile static SaTempInterface saTemp; - public static void setSaTemp(SaTempInterface saTemp) { - SaManager.saTemp = saTemp; - SaTokenEventCenter.doRegisterComponent("SaTempInterface", saTemp); + private volatile static SaTempTemplate saTempTemplate; + public static void setSaTempTemplate(SaTempTemplate saTempTemplate) { + SaManager.saTempTemplate = saTempTemplate; + SaTokenEventCenter.doRegisterComponent("SaTempInterface", saTempTemplate); } - public static SaTempInterface getSaTemp() { - if (saTemp == null) { + public static SaTempTemplate getSaTempTemplate() { + if (saTempTemplate == null) { synchronized (SaManager.class) { - if (saTemp == null) { - SaManager.saTemp = new SaTempDefaultImpl(); + if (saTempTemplate == null) { + SaManager.saTempTemplate = new SaTempTemplate(); } } } - return saTemp; + return saTempTemplate; } /** diff --git a/sa-token-core/src/main/java/cn/dev33/satoken/apikey/SaApiKeyTemplate.java b/sa-token-core/src/main/java/cn/dev33/satoken/apikey/SaApiKeyTemplate.java index 398232a0..64328b95 100644 --- a/sa-token-core/src/main/java/cn/dev33/satoken/apikey/SaApiKeyTemplate.java +++ b/sa-token-core/src/main/java/cn/dev33/satoken/apikey/SaApiKeyTemplate.java @@ -26,7 +26,7 @@ import cn.dev33.satoken.exception.ApiKeyException; import cn.dev33.satoken.exception.ApiKeyScopeException; import cn.dev33.satoken.httpauth.basic.SaHttpBasicUtil; import cn.dev33.satoken.session.SaSession; -import cn.dev33.satoken.session.SaSessionRawUtil; +import cn.dev33.satoken.session.raw.SaRawSessionDelegator; import cn.dev33.satoken.strategy.SaStrategy; import cn.dev33.satoken.util.SaFoxUtil; @@ -42,9 +42,9 @@ import java.util.List; public class SaApiKeyTemplate { /** - * ApiKey 的 raw-session 类型 + * Raw Session 读写委托 */ - public static final String SESSION_TYPE = "apikey"; + public SaRawSessionDelegator rawSessionDelegator = new SaRawSessionDelegator("apikey"); /** * 在 raw-session 中的保存索引列表使用的 key @@ -132,10 +132,10 @@ public class SaApiKeyTemplate { getSaTokenDao().setObject(saveKey, ak, ak.expiresIn()); } - // 调整索引 + // 记录索引 if (SaManager.getSaApiKeyDataLoader().getIsRecordIndex()) { - // 记录索引 - SaSession session = SaSessionRawUtil.getSessionById(SESSION_TYPE, ak.getLoginId()); + // 添加索引 + SaSession session = rawSessionDelegator.getSessionById(ak.getLoginId()); ArrayList apiKeyList = session.get(API_KEY_LIST, ArrayList::new); if(! apiKeyList.contains(ak.getApiKey())) { apiKeyList.add(ak.getApiKey()); @@ -172,7 +172,7 @@ public class SaApiKeyTemplate { // 删索引 if(SaManager.getSaApiKeyDataLoader().getIsRecordIndex()) { // RawSession 中不存在,提前退出 - SaSession session = SaSessionRawUtil.getSessionById(SESSION_TYPE, ak.getLoginId(), false); + SaSession session = rawSessionDelegator.getSessionById(ak.getLoginId(), false); if(session == null) { return; } @@ -184,7 +184,7 @@ public class SaApiKeyTemplate { // 如果只有一个 ApiKey,则整个 RawSession 删掉 if (apiKeyList.size() == 1) { - SaSessionRawUtil.deleteSessionById(SESSION_TYPE, ak.getLoginId()); + rawSessionDelegator.deleteSessionById(ak.getLoginId()); } else { // 否则移除此 ApiKey 并保存 apiKeyList.remove(apiKey); @@ -205,7 +205,7 @@ public class SaApiKeyTemplate { } // RawSession 中不存在,提前退出 - SaSession session = SaSessionRawUtil.getSessionById(SESSION_TYPE, loginId, false); + SaSession session = rawSessionDelegator.getSessionById(loginId, false); if(session == null) { return; } @@ -217,7 +217,7 @@ public class SaApiKeyTemplate { } // 再删索引 - SaSessionRawUtil.deleteSessionById(SESSION_TYPE, loginId); + rawSessionDelegator.deleteSessionById(loginId); } // ------- 创建 @@ -382,7 +382,7 @@ public class SaApiKeyTemplate { // 未提供则现场查询 if(session == null) { - session = SaSessionRawUtil.getSessionById(SESSION_TYPE, loginId, false); + session = rawSessionDelegator.getSessionById(loginId, false); if(session == null) { return; } @@ -400,6 +400,11 @@ public class SaApiKeyTemplate { apiKeyNewList.add(apikey); apiKeyModelList.add(ak); } + // 如果队列里已无有效值,则删除该 session + if(apiKeyNewList.isEmpty()) { + rawSessionDelegator.deleteSessionById(loginId); + return; + } session.set(API_KEY_LIST, apiKeyNewList); // 调整 SaSession TTL @@ -433,7 +438,7 @@ public class SaApiKeyTemplate { // 先查 RawSession List apiKeyModelList = new ArrayList<>(); - SaSession session = SaSessionRawUtil.getSessionById(SESSION_TYPE, loginId, false); + SaSession session = rawSessionDelegator.getSessionById(loginId, false); if(session == null) { return apiKeyModelList; } diff --git a/sa-token-core/src/main/java/cn/dev33/satoken/application/SaSetValueInterface.java b/sa-token-core/src/main/java/cn/dev33/satoken/application/SaSetValueInterface.java index 2e458f3c..d220d658 100644 --- a/sa-token-core/src/main/java/cn/dev33/satoken/application/SaSetValueInterface.java +++ b/sa-token-core/src/main/java/cn/dev33/satoken/application/SaSetValueInterface.java @@ -16,6 +16,7 @@ package cn.dev33.satoken.application; import cn.dev33.satoken.fun.SaRetFunction; +import cn.dev33.satoken.fun.SaRetGenericFunction; /** * 对写值的一组方法封装 @@ -55,7 +56,7 @@ public interface SaSetValueInterface extends SaGetValueInterface { * @return 值 */ @SuppressWarnings("unchecked") - default T get(String key, SaRetFunction fun) { + default T get(String key, SaRetGenericFunction fun) { Object value = get(key); if(value == null) { value = fun.run(); diff --git a/sa-token-core/src/main/java/cn/dev33/satoken/session/raw/SaRawSessionDelegator.java b/sa-token-core/src/main/java/cn/dev33/satoken/session/raw/SaRawSessionDelegator.java new file mode 100644 index 00000000..3be08733 --- /dev/null +++ b/sa-token-core/src/main/java/cn/dev33/satoken/session/raw/SaRawSessionDelegator.java @@ -0,0 +1,77 @@ +/* + * 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.session.raw; + +import cn.dev33.satoken.session.SaSession; + +/** + * SaSession 读写工具类 委托 + * + * @author click33 + * @since 1.42.0 + */ +public class SaRawSessionDelegator { + + /** + * raw session 类型 + */ + public String type; + + public SaRawSessionDelegator(String type) { + this.type = type; + } + + /** + * 判断:指定 SaSession 是否存在 + * + * @param valueId / + * @return 是否存在 + */ + public boolean isExists(Object valueId) { + return SaRawSessionUtil.isExists(type, valueId); + } + + /** + * 获取指定 SaSession 对象, 如果此 SaSession 尚未在 Cache 创建,isCreate 参数代表是否则新建并返回 + * + * @param valueId / + * @param isCreate 如果此 SaSession 尚未在 DB 创建,是否新建并返回 + * @return SaSession 对象 + */ + public SaSession getSessionById(Object valueId, boolean isCreate) { + return SaRawSessionUtil.getSessionById(type, valueId, isCreate); + } + + /** + * 获取指定 SaSession, 如果此 SaSession 尚未在 DB 创建,则新建并返回 + * + * @param valueId / + * @return SaSession 对象 + */ + public SaSession getSessionById(Object valueId) { + return SaRawSessionUtil.getSessionById(type, valueId); + } + + /** + * 删除指定 SaSession + * + * @param valueId / + */ + public void deleteSessionById(Object valueId) { + SaRawSessionUtil.deleteSessionById(type, valueId); + } + +} diff --git a/sa-token-core/src/main/java/cn/dev33/satoken/session/SaSessionRawUtil.java b/sa-token-core/src/main/java/cn/dev33/satoken/session/raw/SaRawSessionUtil.java similarity index 94% rename from sa-token-core/src/main/java/cn/dev33/satoken/session/SaSessionRawUtil.java rename to sa-token-core/src/main/java/cn/dev33/satoken/session/raw/SaRawSessionUtil.java index f323e5e8..00048b7d 100644 --- a/sa-token-core/src/main/java/cn/dev33/satoken/session/SaSessionRawUtil.java +++ b/sa-token-core/src/main/java/cn/dev33/satoken/session/raw/SaRawSessionUtil.java @@ -13,9 +13,10 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package cn.dev33.satoken.session; +package cn.dev33.satoken.session.raw; import cn.dev33.satoken.SaManager; +import cn.dev33.satoken.session.SaSession; import cn.dev33.satoken.strategy.SaStrategy; /** @@ -24,9 +25,9 @@ import cn.dev33.satoken.strategy.SaStrategy; * @author click33 * @since 1.42.0 */ -public class SaSessionRawUtil { +public class SaRawSessionUtil { - private SaSessionRawUtil() { + private SaRawSessionUtil() { } /** diff --git a/sa-token-core/src/main/java/cn/dev33/satoken/temp/SaTempDefaultImpl.java b/sa-token-core/src/main/java/cn/dev33/satoken/temp/SaTempDefaultImpl.java deleted file mode 100644 index 20f051d4..00000000 --- a/sa-token-core/src/main/java/cn/dev33/satoken/temp/SaTempDefaultImpl.java +++ /dev/null @@ -1,26 +0,0 @@ -/* - * Copyright 2020-2099 sa-token.cc - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package cn.dev33.satoken.temp; - -/** - * Sa-Token 临时令牌验证模块 默认实现类 - * - * @author click33 - * @since 1.20.0 - */ -public class SaTempDefaultImpl implements SaTempInterface { - -} diff --git a/sa-token-core/src/main/java/cn/dev33/satoken/temp/SaTempInterface.java b/sa-token-core/src/main/java/cn/dev33/satoken/temp/SaTempInterface.java deleted file mode 100644 index 0c081e54..00000000 --- a/sa-token-core/src/main/java/cn/dev33/satoken/temp/SaTempInterface.java +++ /dev/null @@ -1,173 +0,0 @@ -/* - * Copyright 2020-2099 sa-token.cc - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package cn.dev33.satoken.temp; - -import cn.dev33.satoken.SaManager; -import cn.dev33.satoken.strategy.SaStrategy; -import cn.dev33.satoken.util.SaFoxUtil; -import cn.dev33.satoken.util.SaTokenConsts; - -/** - * Sa-Token 临时 token 验证模块 - 接口 - * - *

- * 有效期很短的一种token,一般用于一次性接口防盗用、短时间资源访问等业务场景 - *

- * - * @author click33 - * @since 1.20.0 - */ -public interface SaTempInterface { - - // -------- 创建 - - /** - * 为指定 value 创建一个临时 Token - * @param value 指定值 - * @param timeout 有效时间,单位:秒,-1 代表永久有效 - * @return 生成的 token - */ - default String createToken(Object value, long timeout) { - return createToken(SaTokenConsts.DEFAULT_TEMP_TOKEN_SERVICE, value, timeout); - } - - /** - * 为指定 业务标识、指定 value 创建一个 Token - * @param service 业务标识 - * @param value 指定值 - * @param timeout 有效期,单位:秒,-1 代表永久有效 - * @return 生成的token - */ - default String createToken(String service, Object value, long timeout) { - - // 生成 token - String token = SaStrategy.instance.createToken.apply(null, null); - - // 持久化映射关系 - String key = splicingKeyTempToken(service, token); - SaManager.getSaTokenDao().setObject(key, value, timeout); - - // 返回 - return token; - } - - // -------- 解析 - - /** - * 解析 Token 获取 value - * @param token 指定 Token - * @return / - */ - default Object parseToken(String token) { - return parseToken(SaTokenConsts.DEFAULT_TEMP_TOKEN_SERVICE, token); - } - - /** - * 解析 Token 获取 value - * @param service 业务标识 - * @param token 指定 Token - * @return / - */ - default Object parseToken(String service, String token) { - String key = splicingKeyTempToken(service, token); - return SaManager.getSaTokenDao().getObject(key); - } - - /** - * 解析 Token 获取 value,并转换为指定类型 - * @param token 指定 Token - * @param cs 指定类型 - * @param 默认值的类型 - * @return / - */ - default T parseToken(String token, Class cs) { - return parseToken(SaTokenConsts.DEFAULT_TEMP_TOKEN_SERVICE, token, cs); - } - - /** - * 解析 Token 获取 value,并转换为指定类型 - * @param service 业务标识 - * @param token 指定 Token - * @param cs 指定类型 - * @param 默认值的类型 - * @return / - */ - default T parseToken(String service, String token, Class cs) { - return SaFoxUtil.getValueByType(parseToken(service, token), cs); - } - - /** - * 获取指定 Token 的剩余有效期,单位:秒 - *

返回值 -1 代表永久,-2 代表token无效 - * @param token 指定 Token - * @return / - */ - default long getTimeout(String token) { - return getTimeout(SaTokenConsts.DEFAULT_TEMP_TOKEN_SERVICE, token); - } - - /** - * 获取指定 业务标识、指定 Token 的剩余有效期,单位:秒 - *

返回值 -1 代表永久,-2 代表token无效 - * @param service 业务标识 - * @param token 指定 Token - * @return / - */ - default long getTimeout(String service, String token) { - String key = splicingKeyTempToken(service, token); - return SaManager.getSaTokenDao().getObjectTimeout(key); - } - - // -------- 删除 - - /** - * 删除一个 Token - * @param token 指定 Token - */ - default void deleteToken(String token) { - deleteToken(SaTokenConsts.DEFAULT_TEMP_TOKEN_SERVICE, token); - } - - /** - * 删除一个 Token - * @param service 业务标识 - * @param token 指定 Token - */ - default void deleteToken(String service, String token) { - String key = splicingKeyTempToken(service, token); - SaManager.getSaTokenDao().deleteObject(key); - } - - // -------- 其它 - - /** - * 获取:在存储临时 token 数据时,应该使用的 key - * @param service 业务标识 - * @param token token值 - * @return key - */ - default String splicingKeyTempToken(String service, String token) { - return SaManager.getConfig().getTokenName() + ":temp-token:" + service + ":" + token; - } - - /** - * @return jwt秘钥 (只有集成 sa-token-temp-jwt 模块时此参数才会生效) - */ - default String getJwtSecretKey() { - return null; - } - -} diff --git a/sa-token-core/src/main/java/cn/dev33/satoken/temp/SaTempTemplate.java b/sa-token-core/src/main/java/cn/dev33/satoken/temp/SaTempTemplate.java new file mode 100644 index 00000000..c1bd653d --- /dev/null +++ b/sa-token-core/src/main/java/cn/dev33/satoken/temp/SaTempTemplate.java @@ -0,0 +1,382 @@ +/* + * 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.temp; + +import cn.dev33.satoken.SaManager; +import cn.dev33.satoken.dao.SaTokenDao; +import cn.dev33.satoken.session.SaSession; +import cn.dev33.satoken.session.raw.SaRawSessionDelegator; +import cn.dev33.satoken.strategy.SaStrategy; +import cn.dev33.satoken.util.SaFoxUtil; + +import java.util.*; + +/** + * Sa-Token 临时 token 验证模块 + * + *

+ * 有效期很短的一种token,一般用于一次性接口防盗用、短时间资源访问等业务场景 + *

+ * + * @author click33 + * @since 1.42.0 + */ +public class SaTempTemplate { + + /** + * Raw Session 读写委托 + */ + public SaRawSessionDelegator rawSessionDelegator = new SaRawSessionDelegator("temp-token"); + + /** + * 在 raw-session 中的保存索引列表使用的 key + */ + public static final String TEMP_TOKEN_MAP = "__HD_TEMP_TOKEN_MAP"; + + + // -------- 创建 + + /** + * 为指定 value 创建一个临时 token (如果多条业务线均需要创建临时 token,请自行在 value 拼接不同前缀) + * + * @param value 指定值 + * @param timeout 有效时间,单位:秒,-1 代表永久有效 + * @return 生成的 token + */ + public String createToken(Object value, long timeout) { + return createToken(value, timeout, false); + } + + /** + * 为指定 业务标识、指定 value 创建一个 Token + * @param value 指定值 + * @param timeout 有效期,单位:秒,-1 代表永久有效 + * @param isRecordIndex 是否记录索引,以便后续使用 value 反查 token + * @return 生成的token + */ + public String createToken(Object value, long timeout, boolean isRecordIndex) { + + // 生成 temp-token + String tempToken = createTempTokenValue(value); + + // 持久化映射关系 + _saveToken(tempToken, value, timeout); + + // 记录索引 + if(isRecordIndex) { + SaSession session = rawSessionDelegator.getSessionById(value); + addTempTokenIndex(session, tempToken, timeout); + adjustIndex(value, session); + } + + // 返回 + return tempToken; + } + + /** + * 创建一个 temp-token 值 + * + * @return / + */ + public String createTempTokenValue(Object value) { + return SaStrategy.instance.generateUniqueToken.execute( + "Temp Token", + SaManager.getConfig().getMaxTryTimes(), + () -> randomTempToken(value), + _apiKey -> parseToken(_apiKey) == null + ); + } + + /** + * 随机一个 temp-token + * + * @return / + */ + public String randomTempToken(Object value) { + return UUID.randomUUID().toString().replace("-", ""); + } + + + // -------- 解析 + + /** + * 解析 Token 获取 value + * @param token 指定 Token + * @return / + */ + public Object parseToken(String token) { + return _getValue(token); + } + + /** + * 解析 Token 获取 value,并转换为指定类型 + * + * @param token 指定 Token + * @param cs 指定类型 + * @param 默认值的类型 + * @return / + */ + public T parseToken(String token, Class cs) { + return parseToken(token, null, cs); + } + + /** + * 解析 token 获取 value,并裁剪指定前缀,然后转换为指定类型 + * + * @param token 指定 Token + * @param cs 指定类型 + * @param 默认值的类型 + * @return / + */ + public T parseToken(String token, String cutPrefix, Class cs) { + // 解析值 + Object value = parseToken(token); + + // 如果未指定裁剪前缀,则直接返回 + if(SaFoxUtil.isEmpty(cutPrefix)) { + return SaFoxUtil.getValueByType(value, cs); + } + + // 如果符合前缀则裁剪并返回,如果不符合前缀则返回 null + String str = SaFoxUtil.valueToString(value); + if(str.startsWith(cutPrefix)) { + return SaFoxUtil.getValueByType(str.substring(cutPrefix.length()), cs); + } else { + return null; + } + } + + /** + * 获取指定指定 Token 的剩余有效期,单位:秒 + *

返回值 -1 代表永久,-2 代表 token 无效 + * + * @param token / + * @return / + */ + public long getTimeout(String token) { + return _getTimeout(token); + } + + + // -------- 删除 + + /** + * 删除一个 token + * @param token 指定 Token + */ + public void deleteToken(String token) { + // 如果无此数据,则直接返回 + Object value = parseToken(token); + if(SaFoxUtil.isEmpty(value)) { + return; + } + + // 删除 token 本身 + _deleteToken(token); + + // 调整索引 + SaSession session = rawSessionDelegator.getSessionById(value, false); + if(session != null) { + deleteTempTokenIndex(session, token); + adjustIndex(value, null); + } + } + + + + // ------------------- 索引操作 + + /** + * 调整索引 + * + * @param value 值 + * @param session 可填写 null,代表使用 value 现场查询 + * @return 调整后的索引列表 + */ + public Map adjustIndex(Object value, SaSession session) { + + // 未提供则现场查询 + if(session == null) { + session = rawSessionDelegator.getSessionById(value, false); + if(session == null) { + return newTempTokenMap(); + } + } + + // 重新整理索引列表 + Map tempTokenNewList = newTempTokenMap(); + ArrayList tempTokenTtlList = new ArrayList<>(); + Map tempTokenMap = session.get(TEMP_TOKEN_MAP, this::newTempTokenMap); + for (Map.Entry entry : tempTokenMap.entrySet()) { + long ttl = expireTimeToTtl(entry.getValue()); + if(ttl != SaTokenDao.NOT_VALUE_EXPIRE) { + tempTokenNewList.put(entry.getKey(), entry.getValue()); + tempTokenTtlList.add(ttl); + } + } + + // 有则保存,无则删除 + if( ! tempTokenNewList.isEmpty()) { + session.set(TEMP_TOKEN_MAP, tempTokenNewList); + } else { + rawSessionDelegator.deleteSessionById(value); + return tempTokenNewList; + } + + // 调整 SaSession TTL + long maxTtl = getMaxTtl(tempTokenTtlList); + if(maxTtl != 0) { + session.updateTimeout(maxTtl); + } + return tempTokenNewList; + } + + /** + * 获取指定 value 的 temp-token 列表记录 + * @param value / + * @return / + */ + public List getTempTokenList(Object value) { + // 先调增索引再获取,否则有可能获取到的不是最新有效数据 + Map tempTokenMap = adjustIndex(value, null); + return new ArrayList<>(tempTokenMap.keySet()); + } + + /** + * 在 SaSession 上添加临时 temp-token 索引 + * @param session / + * @param token / + * @param timeout / + */ + protected void addTempTokenIndex(SaSession session, String token, long timeout) { + Map tempTokenMap = session.get(TEMP_TOKEN_MAP, this::newTempTokenMap); + if(! tempTokenMap.containsKey(token)) { + tempTokenMap.put(token, ttlToExpireTime(timeout)); + session.set(TEMP_TOKEN_MAP, tempTokenMap); + } + } + + /** + * 在 SaSession 上删除临时 temp-token 索引 + * @param session / + * @param token / + */ + protected void deleteTempTokenIndex(SaSession session, String token) { + Map tempTokenMap = session.get(TEMP_TOKEN_MAP, this::newTempTokenMap); + if(tempTokenMap.containsKey(token)) { + tempTokenMap.remove(token); + session.set(TEMP_TOKEN_MAP, tempTokenMap); + } + } + + /** + * 获取一个新的 TempTokenMap 集合 + * @return / + */ + protected Map newTempTokenMap() { + return new LinkedHashMap<>(); + } + + + // -------- 元操作 + + protected Object _getValue(String token) { + String key = splicingTempTokenSaveKey(token); + return SaManager.getSaTokenDao().getObject(key); + } + protected void _saveToken(String token, Object value, long timeout) { + String key = splicingTempTokenSaveKey(token); + SaManager.getSaTokenDao().setObject(key, value, timeout); + } + protected void _deleteToken(String token) { + String key = splicingTempTokenSaveKey(token); + SaManager.getSaTokenDao().deleteObject(key); + } + protected long _getTimeout(String token) { + String key = splicingTempTokenSaveKey(token); + return SaManager.getSaTokenDao().getObjectTimeout(key); + } + + + + // -------- 其它 + + /** + * 过期时间转 ttl (秒) 获取最大 ttl 值 + * @param tempTokenTtlList / + * @return / + */ + protected long getMaxTtl(ArrayList tempTokenTtlList) { + long maxTtl = 0; + for (long ttl : tempTokenTtlList) { + if(ttl == SaTokenDao.NEVER_EXPIRE) { + maxTtl = SaTokenDao.NEVER_EXPIRE; + break; + } + if(ttl > maxTtl) { + maxTtl = ttl; + } + } + return maxTtl; + } + + /** + * 过期时间转 ttl (秒) + * @param expireTime / + * @return / + */ + protected long expireTimeToTtl(long expireTime) { + if(expireTime == SaTokenDao.NEVER_EXPIRE) { + return SaTokenDao.NEVER_EXPIRE; + } + if(expireTime == SaTokenDao.NOT_VALUE_EXPIRE) { + return SaTokenDao.NOT_VALUE_EXPIRE; + } + return (expireTime - System.currentTimeMillis()) / 1000; + } + + /** + * ttl (秒) 转 过期时间 + * @param ttl / + * @return / + */ + protected long ttlToExpireTime(long ttl) { + if(ttl == SaTokenDao.NEVER_EXPIRE) { + return SaTokenDao.NEVER_EXPIRE; + } + if(ttl == SaTokenDao.NOT_VALUE_EXPIRE) { + return SaTokenDao.NOT_VALUE_EXPIRE; + } + return ttl * 1000 + System.currentTimeMillis(); + } + + /** + * 获取:在存储临时 token 数据时,应该使用的 key + * @param token token值 + * @return key + */ + public String splicingTempTokenSaveKey(String token) { + return SaManager.getConfig().getTokenName() + ":temp-token:" + token; + } + + /** + * @return jwt秘钥 (只有集成 sa-token-temp-jwt 模块时此参数才会生效) + */ + public String getJwtSecretKey() { + return null; + } + +} diff --git a/sa-token-core/src/main/java/cn/dev33/satoken/temp/SaTempUtil.java b/sa-token-core/src/main/java/cn/dev33/satoken/temp/SaTempUtil.java index 71593cca..f8c51e54 100644 --- a/sa-token-core/src/main/java/cn/dev33/satoken/temp/SaTempUtil.java +++ b/sa-token-core/src/main/java/cn/dev33/satoken/temp/SaTempUtil.java @@ -17,6 +17,8 @@ package cn.dev33.satoken.temp; import cn.dev33.satoken.SaManager; +import java.util.List; + /** * Sa-Token 临时 token 验证模块 - 工具类 * @@ -35,108 +37,94 @@ public class SaTempUtil { // -------- 创建 /** - * 为指定 value 创建一个临时 Token + * 为指定 value 创建一个临时 token (如果多条业务线均需要创建临时 token,请自行在 value 拼接不同前缀) + * * @param value 指定值 - * @param timeout 有效期,单位:秒,-1 代表永久有效 - * @return 生成的token + * @param timeout 有效时间,单位:秒,-1 代表永久有效 + * @return 生成的 token */ public static String createToken(Object value, long timeout) { - return SaManager.getSaTemp().createToken(value, timeout); + return SaManager.getSaTempTemplate().createToken(value, timeout); } /** * 为指定 业务标识、指定 value 创建一个 Token - * @param service 业务标识 * @param value 指定值 * @param timeout 有效期,单位:秒,-1 代表永久有效 + * @param isRecordIndex 是否记录索引,以便后续使用 value 反查 token * @return 生成的token */ - public static String createToken(String service, Object value, long timeout) { - return SaManager.getSaTemp().createToken(service, value, timeout); + public static String createToken(Object value, long timeout, boolean isRecordIndex) { + return SaManager.getSaTempTemplate().createToken(value, timeout, isRecordIndex); } // -------- 解析 /** - * 解析 Token 获取 value - * @param token 指定 Token - * @return / + * 解析 Token 获取 value + * @param token 指定 Token + * @return / */ public static Object parseToken(String token) { - return SaManager.getSaTemp().parseToken(token); - } - - /** - * 解析 Token 获取 value - * @param service 业务标识 - * @param token 指定 Token - * @return / - */ - public static Object parseToken(String service, String token) { - return SaManager.getSaTemp().parseToken(service, token); - } - - /** - * 解析 Token 获取 value,并转换为指定类型 - * @param token 指定 Token - * @param cs 指定类型 - * @param 默认值的类型 - * @return / - */ - public static T parseToken(String token, Class cs) { - return SaManager.getSaTemp().parseToken(token, cs); + return SaManager.getSaTempTemplate().parseToken(token); } /** * 解析 Token 获取 value,并转换为指定类型 - * @param service 业务标识 - * @param token 指定 Token - * @param cs 指定类型 - * @param 默认值的类型 - * @return / - */ - public static T parseToken(String service, String token, Class cs) { - return SaManager.getSaTemp().parseToken(service, token, cs); - } - - /** - * 获取指定 Token 的剩余有效期,单位:秒 - *

返回值 -1 代表永久,-2 代表token无效 + * * @param token 指定 Token + * @param cs 指定类型 + * @param 默认值的类型 * @return / */ - public static long getTimeout(String token) { - return SaManager.getSaTemp().getTimeout(token); + public static T parseToken(String token, Class cs) { + return SaManager.getSaTempTemplate().parseToken(token, cs); } /** - * 获取指定 业务标识、指定 Token 的剩余有效期,单位:秒 - *

返回值 -1 代表永久,-2 代表token无效 - * @param service 业务标识 + * 解析 token 获取 value,并裁剪指定前缀,然后转换为指定类型 + * * @param token 指定 Token - * @return / + * @param cs 指定类型 + * @param 默认值的类型 + * @return / */ - public static long getTimeout(String service, String token) { - return SaManager.getSaTemp().getTimeout(service, token); + public static T parseToken(String token, String cutPrefix, Class cs) { + return SaManager.getSaTempTemplate().parseToken(token, cutPrefix, cs); } + /** + * 获取指定指定 Token 的剩余有效期,单位:秒 + *

返回值 -1 代表永久,-2 代表 token 无效 + * + * @param token / + * @return / + */ + public static long getTimeout(String token) { + return SaManager.getSaTempTemplate().getTimeout(token); + } + + // -------- 删除 /** - * 删除一个 Token - * @param token 指定 Token + * 删除一个 token + * @param token 指定 Token */ public static void deleteToken(String token) { - SaManager.getSaTemp().deleteToken(token); + SaManager.getSaTempTemplate().deleteToken(token); } - + + + // ------------------- 索引操作 + /** - * 删除一个 Token - * @param service 业务标识 - * @param token 指定 Token + * 获取指定 value 的 temp-token 列表记录 + * @param value / + * @return / */ - public static void deleteToken(String service, String token) { - SaManager.getSaTemp().deleteToken(service, token); + public static List getTempTokenList(Object value) { + return SaManager.getSaTempTemplate().getTempTokenList(value); } - + } diff --git a/sa-token-core/src/main/java/cn/dev33/satoken/util/SaFoxUtil.java b/sa-token-core/src/main/java/cn/dev33/satoken/util/SaFoxUtil.java index a9cf5f62..39b603f4 100644 --- a/sa-token-core/src/main/java/cn/dev33/satoken/util/SaFoxUtil.java +++ b/sa-token-core/src/main/java/cn/dev33/satoken/util/SaFoxUtil.java @@ -828,5 +828,17 @@ public class SaFoxUtil { return false; } + /** + * 将 value 转化为 String,如果 value 为 null,则返回空字符串 + * @param value / + * @return / + */ + public static String valueToString(Object value) { + if (value == null) { + return ""; + } + return value.toString(); + } + } diff --git a/sa-token-core/src/main/java/cn/dev33/satoken/util/SaTokenConsts.java b/sa-token-core/src/main/java/cn/dev33/satoken/util/SaTokenConsts.java index 1797da75..a41f396d 100644 --- a/sa-token-core/src/main/java/cn/dev33/satoken/util/SaTokenConsts.java +++ b/sa-token-core/src/main/java/cn/dev33/satoken/util/SaTokenConsts.java @@ -120,6 +120,7 @@ public class SaTokenConsts { /** * 常量 key 标记: 临时 Token 认证模块,默认的业务类型 */ + @Deprecated public static final String DEFAULT_TEMP_TOKEN_SERVICE = "record"; diff --git a/sa-token-demo/sa-token-demo-case/pom.xml b/sa-token-demo/sa-token-demo-case/pom.xml index 3ccf7872..b7ce1832 100644 --- a/sa-token-demo/sa-token-demo-case/pom.xml +++ b/sa-token-demo/sa-token-demo-case/pom.xml @@ -59,6 +59,13 @@ commons-pool2 + + + org.springframework.boot diff --git a/sa-token-demo/sa-token-demo-case/src/main/java/com/pj/cases/plugin/TempTokenController.java b/sa-token-demo/sa-token-demo-case/src/main/java/com/pj/cases/plugin/TempTokenController.java new file mode 100644 index 00000000..73e43e74 --- /dev/null +++ b/sa-token-demo/sa-token-demo-case/src/main/java/com/pj/cases/plugin/TempTokenController.java @@ -0,0 +1,69 @@ +package com.pj.cases.plugin; + +import cn.dev33.satoken.temp.SaTempUtil; +import cn.dev33.satoken.util.SaResult; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; + +/** + * 测试专用 Controller + * @author click33 + * + */ +@RestController +@RequestMapping("/temp-token/") +public class TempTokenController { + + // 创建 浏览器访问: http://localhost:8081/temp-token/create + @RequestMapping("create") + public SaResult create() { + String token = SaTempUtil.createToken(10001, 1200); + System.out.println("创建成功:" + token); + return SaResult.data(token); + } + + // 创建,获取时裁剪前缀 浏览器访问: http://localhost:8081/temp-token/create2 + @RequestMapping("create2") + public SaResult create2() { + String token = SaTempUtil.createToken("shop_" + 1001, 1200); + System.out.println("创建成功:" + token); + System.out.println("获取对应值:" + SaTempUtil.parseToken(token)); + System.out.println("获取对应值,并裁剪前缀:" + SaTempUtil.parseToken(token, "shop_", Long.class)); + System.out.println("指定错误前缀来获取:" + SaTempUtil.parseToken(token, "art_", Long.class)); + return SaResult.data(token); + } + + // 创建,回收 浏览器访问: http://localhost:8081/temp-token/create3 + @RequestMapping("create3") + public SaResult create3() { + String token = SaTempUtil.createToken(10003, 1200); + System.out.println("创建成功:" + token); + System.out.println("获取对应值:" + SaTempUtil.parseToken(token)); + SaTempUtil.deleteToken(token); + System.out.println("回收后再获取:" + SaTempUtil.parseToken(token)); + return SaResult.data(token); + } + + // 创建,记录索引 浏览器访问: http://localhost:8081/temp-token/create4 + @RequestMapping("create4") + public SaResult create4() { + String token1 = SaTempUtil.createToken(10004, 1200, true); + String token2 = SaTempUtil.createToken(10004, 1300, true); + String token3 = SaTempUtil.createToken(10004, -1, true); + + System.out.println("token1 剩余有效期:" + SaTempUtil.getTimeout(token1)); + System.out.println("token2 剩余有效期:" + SaTempUtil.getTimeout(token2)); + System.out.println("token3 剩余有效期:" + SaTempUtil.getTimeout(token3)); + + SaTempUtil.deleteToken(token3); + + // 获取已创建的 token 列表 + System.out.println("获取已创建的 token 列表 "); + List tempTokenList = SaTempUtil.getTempTokenList(10004); + System.out.println(tempTokenList); + return SaResult.data(token1); + } + +} diff --git a/sa-token-demo/sa-token-demo-case/src/main/java/com/pj/cases/up/MutexLoginController.java b/sa-token-demo/sa-token-demo-case/src/main/java/com/pj/cases/up/MutexLoginController.java index d22d7123..28322e2b 100644 --- a/sa-token-demo/sa-token-demo-case/src/main/java/com/pj/cases/up/MutexLoginController.java +++ b/sa-token-demo/sa-token-demo-case/src/main/java/com/pj/cases/up/MutexLoginController.java @@ -1,10 +1,9 @@ package com.pj.cases.up; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; - import cn.dev33.satoken.stp.StpUtil; import cn.dev33.satoken.util.SaResult; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; /** * Sa-Token 同端互斥登录示例 @@ -62,7 +61,7 @@ public class MutexLoginController { public SaResult isLogin() { // StpUtil.isLogin() 查询当前客户端是否登录,返回 true 或 false boolean isLogin = StpUtil.isLogin(); - return SaResult.ok("当前客户端是否登录:" + isLogin + ",登录的设备是:" + StpUtil.getLoginDevice()); + return SaResult.ok("当前客户端是否登录:" + isLogin + ",登录的设备是:" + StpUtil.getLoginDeviceType()); } } diff --git a/sa-token-demo/sa-token-demo-case/src/main/resources/application.yml b/sa-token-demo/sa-token-demo-case/src/main/resources/application.yml index ed89db69..cc4bedb0 100644 --- a/sa-token-demo/sa-token-demo-case/src/main/resources/application.yml +++ b/sa-token-demo/sa-token-demo-case/src/main/resources/application.yml @@ -18,6 +18,8 @@ sa-token: token-style: uuid # 是否输出操作日志 is-log: true + # jwt 秘钥 + jwt-secret-key: JfdDSgfCmPsDfmsAaQwnXk spring: # redis配置 diff --git a/sa-token-dependencies/pom.xml b/sa-token-dependencies/pom.xml index 400da9c5..40d9f840 100644 --- a/sa-token-dependencies/pom.xml +++ b/sa-token-dependencies/pom.xml @@ -34,7 +34,7 @@ 2.7.21 2.10.1.RELEASE 5.8.36 - 0.9.1 + 0.12.6 1.2.83 2.0.15 3.45.0 @@ -253,12 +253,12 @@ grpc-spring-boot-starter ${grpc-spring-boot-starter.version} - - + + io.jsonwebtoken jjwt ${jjwt.version} - + diff --git a/sa-token-plugin/sa-token-temp-jwt/pom.xml b/sa-token-plugin/sa-token-temp-jwt/pom.xml index 58c64275..80ed4532 100644 --- a/sa-token-plugin/sa-token-temp-jwt/pom.xml +++ b/sa-token-plugin/sa-token-temp-jwt/pom.xml @@ -22,10 +22,10 @@ cn.dev33 sa-token-core - + - io.jsonwebtoken - jjwt + io.jsonwebtoken + jjwt diff --git a/sa-token-plugin/sa-token-temp-jwt/src/main/java/cn/dev33/satoken/plugin/SaTokenPluginForTempForJwt.java b/sa-token-plugin/sa-token-temp-jwt/src/main/java/cn/dev33/satoken/plugin/SaTokenPluginForTempForJwt.java index d841508a..e3fe96fd 100644 --- a/sa-token-plugin/sa-token-temp-jwt/src/main/java/cn/dev33/satoken/plugin/SaTokenPluginForTempForJwt.java +++ b/sa-token-plugin/sa-token-temp-jwt/src/main/java/cn/dev33/satoken/plugin/SaTokenPluginForTempForJwt.java @@ -16,7 +16,7 @@ package cn.dev33.satoken.plugin; import cn.dev33.satoken.SaManager; -import cn.dev33.satoken.temp.jwt.SaTempForJwt; +import cn.dev33.satoken.temp.jwt.SaTempTemplateForJwt; /** * SaToken 插件安装:临时 token 生成器 - Jwt 版 @@ -28,7 +28,7 @@ public class SaTokenPluginForTempForJwt implements SaTokenPlugin { @Override public void install() { - SaManager.setSaTemp(new SaTempForJwt()); + SaManager.setSaTempTemplate(new SaTempTemplateForJwt()); } } \ No newline at end of file diff --git a/sa-token-plugin/sa-token-temp-jwt/src/main/java/cn/dev33/satoken/temp/jwt/SaJwtUtil.java b/sa-token-plugin/sa-token-temp-jwt/src/main/java/cn/dev33/satoken/temp/jwt/SaJwtUtil.java index 2866318b..dc631bef 100644 --- a/sa-token-plugin/sa-token-temp-jwt/src/main/java/cn/dev33/satoken/temp/jwt/SaJwtUtil.java +++ b/sa-token-plugin/sa-token-temp-jwt/src/main/java/cn/dev33/satoken/temp/jwt/SaJwtUtil.java @@ -17,11 +17,14 @@ package cn.dev33.satoken.temp.jwt; import cn.dev33.satoken.dao.SaTokenDao; import cn.dev33.satoken.exception.SaTokenException; +import cn.dev33.satoken.secure.SaSecureUtil; import cn.dev33.satoken.temp.jwt.error.SaTempJwtErrorCode; import io.jsonwebtoken.Claims; import io.jsonwebtoken.JwtBuilder; import io.jsonwebtoken.Jwts; -import io.jsonwebtoken.SignatureAlgorithm; +import io.jsonwebtoken.security.Keys; + +import javax.crypto.SecretKey; /** * jwt 相关操作工具类,封装一下 @@ -47,13 +50,12 @@ public class SaJwtUtil { /** * 根据指定值创建 jwt-token * - * @param key 存储value使用的key * @param value 要保存的值 * @param timeout token有效期 (单位 秒) * @param keyt 秘钥 * @return jwt-token */ - public static String createToken(String key, Object value, long timeout, String keyt) { + public static String createToken(Object value, long timeout, String keyt) { // 计算eff有效期: // 如果 timeout 指定为 -1,那么 eff 也为 -1,代表永不过期 // 如果 timeout 指定为一个具体的值,那么 eff 为 13 位时间戳,代表此数据到期的时间 @@ -62,12 +64,13 @@ public class SaJwtUtil { eff = timeout * 1000 + System.currentTimeMillis(); } - // 在这里你可以使用官方提供的claim方法构建载荷,也可以使用setPayload自定义载荷,但是两者不可一起使用 - JwtBuilder builder = Jwts.builder() - // .setHeaderParam("typ", "JWT") - .claim(KEY_VALUE + key, value) + // 在这里你可以使用官方提供的claim方法构建载荷,也可以使用setPayload自定义载荷,但是两者不可一起使用 + SecretKey key = Keys.hmacShaKeyFor(SaSecureUtil.md5(keyt).getBytes()); + JwtBuilder builder = Jwts.builder() + .header().add("typ", "JWT").and() + .claim(KEY_VALUE, value) .claim(KEY_EFF, eff) - .signWith(SignatureAlgorithm.HS256, keyt.getBytes()); + .signWith(key); // 生成jwt-token return builder.compact(); @@ -81,19 +84,20 @@ public class SaJwtUtil { */ public static Claims parseToken(String jwtToken, String keyt) { // 解析出载荷 + SecretKey key = Keys.hmacShaKeyFor(SaSecureUtil.md5(keyt).getBytes()); return Jwts.parser() - .setSigningKey(keyt.getBytes()) - .parseClaimsJws(jwtToken).getBody(); + .verifyWith(key) + .build() + .parseSignedClaims(jwtToken).getPayload(); } /** - * 从一个 jwt-token 解析出载荷, 并取出数据 - * @param key 存储value使用的key + * 从一个 jwt-token 解析出载荷, 并取出数据 * @param jwtToken JwtToken值 * @param keyt 秘钥 * @return 值 */ - public static Object getValue(String key, String jwtToken, String keyt) { + public static Object getValue(String jwtToken, String keyt) { // 取出数据 Claims claims = parseToken(jwtToken, keyt); @@ -104,25 +108,19 @@ public class SaJwtUtil { } // 获取数据 - return claims.get(KEY_VALUE + key); + return claims.get(KEY_VALUE); } /** * 从一个 jwt-token 解析出载荷, 并取出其剩余有效期 - * @param service 指定的服务类型 * @param jwtToken JwtToken值 * @param keyt 秘钥 * @return 值 */ - public static long getTimeout(String service, String jwtToken, String keyt) { + public static long getTimeout(String jwtToken, String keyt) { // 取出数据 Claims claims = parseToken(jwtToken, keyt); - - // 如果给定的 service 不对 - if(claims.get(KEY_VALUE + service) == null) { - return SaTokenDao.NOT_VALUE_EXPIRE; - } - + // 验证是否超时 Long eff = claims.get(KEY_EFF, Long.class); @@ -131,7 +129,7 @@ public class SaJwtUtil { return NEVER_EXPIRE; } // 已经超时 - if(eff == null || eff < System.currentTimeMillis()) { + if(eff < System.currentTimeMillis()) { return SaTokenDao.NOT_VALUE_EXPIRE; } diff --git a/sa-token-plugin/sa-token-temp-jwt/src/main/java/cn/dev33/satoken/temp/jwt/SaTempForJwt.java b/sa-token-plugin/sa-token-temp-jwt/src/main/java/cn/dev33/satoken/temp/jwt/SaTempTemplateForJwt.java similarity index 69% rename from sa-token-plugin/sa-token-temp-jwt/src/main/java/cn/dev33/satoken/temp/jwt/SaTempForJwt.java rename to sa-token-plugin/sa-token-temp-jwt/src/main/java/cn/dev33/satoken/temp/jwt/SaTempTemplateForJwt.java index 525529ad..24a98cb5 100644 --- a/sa-token-plugin/sa-token-temp-jwt/src/main/java/cn/dev33/satoken/temp/jwt/SaTempForJwt.java +++ b/sa-token-plugin/sa-token-temp-jwt/src/main/java/cn/dev33/satoken/temp/jwt/SaTempTemplateForJwt.java @@ -18,50 +18,61 @@ package cn.dev33.satoken.temp.jwt; import cn.dev33.satoken.SaManager; import cn.dev33.satoken.exception.ApiDisabledException; import cn.dev33.satoken.exception.SaTokenException; -import cn.dev33.satoken.temp.SaTempInterface; +import cn.dev33.satoken.temp.SaTempTemplate; import cn.dev33.satoken.temp.jwt.error.SaTempJwtErrorCode; import cn.dev33.satoken.util.SaFoxUtil; +import java.util.List; + /** * Sa-Token 临时令牌验证模块接口 JWT实现类,提供以 JWT 为逻辑内核的临时 token 验证功能 * * @author click33 * @since 1.20.0 */ -public class SaTempForJwt implements SaTempInterface { +public class SaTempTemplateForJwt extends SaTempTemplate { /** * 根据value创建一个token */ @Override - public String createToken(String service, Object value, long timeout) { - return SaJwtUtil.createToken(service, value, timeout, getJwtSecretKey()); + public String createToken(Object value, long timeout, boolean isRecordIndex) { + return SaJwtUtil.createToken(value, timeout, getJwtSecretKey()); } /** * 解析token获取value */ @Override - public Object parseToken(String service, String token) { - return SaJwtUtil.getValue(service, token, getJwtSecretKey()); + public Object parseToken(String token) { + return SaJwtUtil.getValue(token, getJwtSecretKey()); } /** * 返回指定token的剩余有效期,单位:秒 */ @Override - public long getTimeout(String service, String token) { - return SaJwtUtil.getTimeout(service, token, getJwtSecretKey()); + public long getTimeout(String token) { + return SaJwtUtil.getTimeout(token, getJwtSecretKey()); } /** * 删除一个token */ @Override - public void deleteToken(String service, String token) { + public void deleteToken(String token) { throw new ApiDisabledException("jwt cannot delete token").setCode(SaTempJwtErrorCode.CODE_30302); } - + + /** + * 获取指定 value 的 temp-token 列表记录 + * @param value / + * @return / + */ + public List getTempTokenList(Object value) { + throw new ApiDisabledException("jwt cannot get token list").setCode(SaTempJwtErrorCode.CODE_30304); + } + /** * 获取jwt秘钥 * @return jwt秘钥 diff --git a/sa-token-plugin/sa-token-temp-jwt/src/main/java/cn/dev33/satoken/temp/jwt/error/SaTempJwtErrorCode.java b/sa-token-plugin/sa-token-temp-jwt/src/main/java/cn/dev33/satoken/temp/jwt/error/SaTempJwtErrorCode.java index 59eb0ff3..0a7f6522 100644 --- a/sa-token-plugin/sa-token-temp-jwt/src/main/java/cn/dev33/satoken/temp/jwt/error/SaTempJwtErrorCode.java +++ b/sa-token-plugin/sa-token-temp-jwt/src/main/java/cn/dev33/satoken/temp/jwt/error/SaTempJwtErrorCode.java @@ -32,4 +32,7 @@ public interface SaTempJwtErrorCode { /** Token已超时 */ int CODE_30303 = 30303; + /** jwt 模式不可以查询旧 Token 列表 */ + int CODE_30304 = 30304; + } diff --git a/sa-token-starter/sa-token-solon-plugin/src/main/java/cn/dev33/satoken/solon/SaBeanInject.java b/sa-token-starter/sa-token-solon-plugin/src/main/java/cn/dev33/satoken/solon/SaBeanInject.java index af4528ef..f50b10e7 100644 --- a/sa-token-starter/sa-token-solon-plugin/src/main/java/cn/dev33/satoken/solon/SaBeanInject.java +++ b/sa-token-starter/sa-token-solon-plugin/src/main/java/cn/dev33/satoken/solon/SaBeanInject.java @@ -42,7 +42,7 @@ import cn.dev33.satoken.stp.StpUtil; import cn.dev33.satoken.strategy.SaAnnotationStrategy; import cn.dev33.satoken.strategy.SaFirewallStrategy; import cn.dev33.satoken.strategy.hooks.SaFirewallCheckHook; -import cn.dev33.satoken.temp.SaTempInterface; +import cn.dev33.satoken.temp.SaTempTemplate; import org.noear.solon.annotation.Bean; import org.noear.solon.annotation.Condition; import org.noear.solon.annotation.Configuration; @@ -145,12 +145,12 @@ public class SaBeanInject { /** * 注入临时令牌验证模块 Bean * - * @param saTemp saTemp对象 + * @param saTempTemplate / */ - @Condition(onBean = SaTempInterface.class) + @Condition(onBean = SaTempTemplate.class) @Bean - public void setSaTemp(SaTempInterface saTemp) { - SaManager.setSaTemp(saTemp); + public void setSaTempTemplate(SaTempTemplate saTempTemplate) { + SaManager.setSaTempTemplate(saTempTemplate); } /** diff --git a/sa-token-starter/sa-token-spring-boot-autoconfig/src/main/java/cn/dev33/satoken/spring/SaBeanInject.java b/sa-token-starter/sa-token-spring-boot-autoconfig/src/main/java/cn/dev33/satoken/spring/SaBeanInject.java index 281cbca5..ad22ff6c 100644 --- a/sa-token-starter/sa-token-spring-boot-autoconfig/src/main/java/cn/dev33/satoken/spring/SaBeanInject.java +++ b/sa-token-starter/sa-token-spring-boot-autoconfig/src/main/java/cn/dev33/satoken/spring/SaBeanInject.java @@ -43,7 +43,7 @@ import cn.dev33.satoken.stp.StpUtil; import cn.dev33.satoken.strategy.SaAnnotationStrategy; import cn.dev33.satoken.strategy.SaFirewallStrategy; import cn.dev33.satoken.strategy.hooks.SaFirewallCheckHook; -import cn.dev33.satoken.temp.SaTempInterface; +import cn.dev33.satoken.temp.SaTempTemplate; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.util.PathMatcher; @@ -139,11 +139,11 @@ public class SaBeanInject { /** * 注入临时令牌验证模块 Bean * - * @param saTemp saTemp对象 + * @param saTempTemplate / */ @Autowired(required = false) - public void setSaTemp(SaTempInterface saTemp) { - SaManager.setSaTemp(saTemp); + public void setSaTempTemplate(SaTempTemplate saTempTemplate) { + SaManager.setSaTempTemplate(saTempTemplate); } /** diff --git a/sa-token-test/pom.xml b/sa-token-test/pom.xml index fee39985..5b28fc38 100644 --- a/sa-token-test/pom.xml +++ b/sa-token-test/pom.xml @@ -24,6 +24,7 @@ sa-token-springboot-test sa-token-jwt-test + sa-token-temp-jwt-test sa-token-json-test sa-token-serializer-test diff --git a/sa-token-test/sa-token-springboot-test/src/test/java/cn/dev33/satoken/core/temp/SaTempTest.java b/sa-token-test/sa-token-springboot-test/src/test/java/cn/dev33/satoken/core/temp/SaTempTest.java deleted file mode 100644 index 47f9d34a..00000000 --- a/sa-token-test/sa-token-springboot-test/src/test/java/cn/dev33/satoken/core/temp/SaTempTest.java +++ /dev/null @@ -1,112 +0,0 @@ -/* - * Copyright 2020-2099 sa-token.cc - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package cn.dev33.satoken.core.temp; - -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.Test; - -import cn.dev33.satoken.SaManager; -import cn.dev33.satoken.dao.SaTokenDao; -import cn.dev33.satoken.temp.SaTempUtil; -import cn.dev33.satoken.util.SaTokenConsts; - -/** - * 临时Token模块测试 - * - * @author click33 - * @since 2022-9-1 - */ -public class SaTempTest { - - // 测试:临时Token认证模块 - @Test - public void testSaTemp() { - SaTokenDao dao = SaManager.getSaTokenDao(); - - // 生成token - String token = SaTempUtil.createToken("group-1014", 200); - Assertions.assertNotNull(token); - // System.out.println(((SaTokenDaoDefaultImpl)SaManager.getSaTokenDao()).dataMap); - - // 解析token - String value = SaTempUtil.parseToken(token, String.class); - Assertions.assertEquals(value, "group-1014"); - Assertions.assertEquals(dao.getObject("satoken:temp-token:" + SaTokenConsts.DEFAULT_TEMP_TOKEN_SERVICE + ":" + token), "group-1014"); - - // 默认类型 - Object value3 = SaTempUtil.parseToken(token); - Assertions.assertEquals(value3, "group-1014"); - - // 转换类型 - String value4 = SaTempUtil.parseToken(token, String.class); - Assertions.assertEquals(value4, "group-1014"); - - // 过期时间 - long timeout = SaTempUtil.getTimeout(token); - Assertions.assertTrue(timeout > 195); - - // 回收token - SaTempUtil.deleteToken(token); - String value2 = SaTempUtil.parseToken(token, String.class); - Assertions.assertEquals(value2, null); - Assertions.assertEquals(dao.getObject("satoken:temp-token:" + SaTokenConsts.DEFAULT_TEMP_TOKEN_SERVICE + ":" + token), null); - } - - // 测试:临时Token认证模块,带 Service 参数 - @Test - public void testSaTempService() { - SaTokenDao dao = SaManager.getSaTokenDao(); - - // 生成token - String token = SaTempUtil.createToken("shop", "1001", 200); - Assertions.assertNotNull(token); - // System.out.println(((SaTokenDaoDefaultImpl)SaManager.getSaTokenDao()).dataMap); - - // 解析token - String value = SaTempUtil.parseToken("shop", token, String.class); - Assertions.assertEquals(value, "1001"); - Assertions.assertEquals(dao.getObject("satoken:temp-token:" + "shop" + ":" + token), "1001"); - - // 默认类型 - Object value3 = SaTempUtil.parseToken("shop", token); - Assertions.assertEquals(value3, "1001"); - - // 转换类型 - String value4 = SaTempUtil.parseToken("shop", token, String.class); - Assertions.assertEquals(value4, "1001"); - - // service 参数不对的情况下,无法取出 - String value5 = SaTempUtil.parseToken("goods", token, String.class); - Assertions.assertNull(value5); - - // 过期时间 - long timeout = SaTempUtil.getTimeout("shop", token); - Assertions.assertTrue(timeout > 195); - - // 回收token - SaTempUtil.deleteToken("shop", token); - String value2 = SaTempUtil.parseToken("shop", token, String.class); - Assertions.assertEquals(value2, null); - Assertions.assertEquals(dao.getObject("satoken:temp-token:" + "shop" + ":" + token), null); - } - - @Test - public void testSaTemp2() { - // 秘钥默认为null - String jwtSecretKey = SaManager.getSaTemp().getJwtSecretKey(); - Assertions.assertEquals(jwtSecretKey, null); - } -} diff --git a/sa-token-test/sa-token-springboot-test/src/test/java/cn/dev33/satoken/core/temp/SaTempTokenTest.java b/sa-token-test/sa-token-springboot-test/src/test/java/cn/dev33/satoken/core/temp/SaTempTokenTest.java new file mode 100644 index 00000000..083c0286 --- /dev/null +++ b/sa-token-test/sa-token-springboot-test/src/test/java/cn/dev33/satoken/core/temp/SaTempTokenTest.java @@ -0,0 +1,142 @@ +/* + * 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.core.temp; + +import cn.dev33.satoken.SaManager; +import cn.dev33.satoken.dao.SaTokenDao; +import cn.dev33.satoken.temp.SaTempUtil; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import java.util.List; + +/** + * 临时Token模块测试 + * + * @author click33 + * @since 2022-9-1 + */ +public class SaTempTokenTest { + + // 测试:临时Token认证模块 + @Test + public void testSaTemp() { + SaTokenDao dao = SaManager.getSaTokenDao(); + + // 生成token + String token = SaTempUtil.createToken("group-1014", 200); +// System.out.println(((SaTokenDaoDefaultImpl)SaManager.getSaTokenDao()).timedCache.dataMap.keySet()); +// System.out.println("satoken:temp-token:" + ":" + token); + Assertions.assertNotNull(token); + Assertions.assertEquals(dao.getObject("satoken:temp-token:" + token), "group-1014"); + + // 解析token + String value = SaTempUtil.parseToken(token, String.class); + Assertions.assertEquals(value, "group-1014"); + + // 解析 token 并裁剪前缀 + long value2 = SaTempUtil.parseToken(token, "group-", Long.class); + Assertions.assertEquals(value2, 1014); + + // 默认类型 + Object value3 = SaTempUtil.parseToken(token); + Assertions.assertEquals(value3, "group-1014"); + + // 转换类型 + String value4 = SaTempUtil.parseToken(token, String.class); + Assertions.assertEquals(value4, "group-1014"); + + // 过期时间 + long timeout = SaTempUtil.getTimeout(token); + Assertions.assertTrue(timeout > 195); + Assertions.assertTrue(timeout < 201); + + // 回收token + SaTempUtil.deleteToken(token); + String value5 = SaTempUtil.parseToken(token, String.class); + Assertions.assertNull(value5); + Assertions.assertNull(dao.getObject("satoken:temp-token:" + ":" + token)); + } + + // 测试:临时Token认证模块索引 + @Test + public void testSaTempIndex() { + SaTokenDao dao = SaManager.getSaTokenDao(); + + // 生成token + String token1 = SaTempUtil.createToken("1001", 200, true); + String token2 = SaTempUtil.createToken("1001", 300, true); + String token3 = SaTempUtil.createToken("1001", 400, true); + + Assertions.assertNotNull(token1); + Assertions.assertNotNull(token2); + Assertions.assertNotNull(token3); + // System.out.println(((SaTokenDaoDefaultImpl)SaManager.getSaTokenDao()).dataMap); + + // 解析token + Assertions.assertEquals(SaTempUtil.parseToken(token1, String.class), "1001"); + Assertions.assertEquals(SaTempUtil.parseToken(token2, String.class), "1001"); + Assertions.assertEquals(SaTempUtil.parseToken(token3, String.class), "1001"); + + // 缓存数据比对 + Assertions.assertEquals(dao.getObject("satoken:temp-token:" + token1), "1001"); + Assertions.assertEquals(dao.getObject("satoken:temp-token:" + token2), "1001"); + Assertions.assertEquals(dao.getObject("satoken:temp-token:" + token3), "1001"); + + // 索引 + List tempTokenList = SaTempUtil.getTempTokenList("1001"); + Assertions.assertEquals(tempTokenList.size(), 3); + Assertions.assertTrue(tempTokenList.contains(token1)); + Assertions.assertTrue(tempTokenList.contains(token2)); + Assertions.assertTrue(tempTokenList.contains(token3)); + + long sessionTimeout = dao.getSessionTimeout("satoken:raw-session:temp-token:" + "1001"); + Assertions.assertTrue(sessionTimeout > 395); + Assertions.assertTrue(sessionTimeout < 401); + + // 移除一个 token + SaTempUtil.deleteToken(token3); + Assertions.assertNull(SaTempUtil.parseToken(token3, String.class)); + Assertions.assertNull(dao.getObject("satoken:temp-token:" + token3)); + + List tempTokenList2 = SaTempUtil.getTempTokenList("1001"); + Assertions.assertEquals(tempTokenList2.size(), 2); + Assertions.assertFalse(tempTokenList2.contains(token3)); + + long sessionTimeout2 = dao.getSessionTimeout("satoken:raw-session:temp-token:" + "1001"); + Assertions.assertTrue(sessionTimeout2 > 295); + Assertions.assertTrue(sessionTimeout2 < 301); + + // 新增一个 token + String token4 = SaTempUtil.createToken("1001", -1, true); + Assertions.assertEquals(SaTempUtil.parseToken(token4, String.class), "1001"); + + List tempTokenList3 = SaTempUtil.getTempTokenList("1001"); + Assertions.assertEquals(tempTokenList3.size(), 3); + Assertions.assertTrue(tempTokenList3.contains(token4)); + + long sessionTimeout4 = dao.getSessionTimeout("satoken:raw-session:temp-token:" + "1001"); + Assertions.assertEquals(-1, sessionTimeout4); + } + + @Test + public void testGetJwtSecretKey() { + // 秘钥默认为null + String jwtSecretKey = SaManager.getSaTempTemplate().getJwtSecretKey(); + Assertions.assertNull(jwtSecretKey); + } + +} diff --git a/sa-token-test/sa-token-springboot-test/src/test/java/cn/dev33/satoken/integrate/configure/inject/MySaTemp.java b/sa-token-test/sa-token-springboot-test/src/test/java/cn/dev33/satoken/integrate/configure/inject/MySaTempTemplate.java similarity index 87% rename from sa-token-test/sa-token-springboot-test/src/test/java/cn/dev33/satoken/integrate/configure/inject/MySaTemp.java rename to sa-token-test/sa-token-springboot-test/src/test/java/cn/dev33/satoken/integrate/configure/inject/MySaTempTemplate.java index fdf7d477..94f2535a 100644 --- a/sa-token-test/sa-token-springboot-test/src/test/java/cn/dev33/satoken/integrate/configure/inject/MySaTemp.java +++ b/sa-token-test/sa-token-springboot-test/src/test/java/cn/dev33/satoken/integrate/configure/inject/MySaTempTemplate.java @@ -15,11 +15,10 @@ */ package cn.dev33.satoken.integrate.configure.inject; +import cn.dev33.satoken.temp.SaTempTemplate; import org.springframework.stereotype.Component; -import cn.dev33.satoken.temp.SaTempDefaultImpl; - @Component -public class MySaTemp extends SaTempDefaultImpl { +public class MySaTempTemplate extends SaTempTemplate { } diff --git a/sa-token-test/sa-token-temp-jwt-test/pom.xml b/sa-token-test/sa-token-temp-jwt-test/pom.xml new file mode 100644 index 00000000..eaf0eff1 --- /dev/null +++ b/sa-token-test/sa-token-temp-jwt-test/pom.xml @@ -0,0 +1,30 @@ + + + 4.0.0 + + + cn.dev33 + sa-token-test + ${revision} + ../pom.xml + + jar + + sa-token-temp-jwt-test + sa-token-temp-jwt-test + sa-token-temp-jwt-test + + + + cn.dev33 + sa-token-spring-boot-starter + + + cn.dev33 + sa-token-temp-jwt + + + + diff --git a/sa-token-test/sa-token-temp-jwt-test/src/test/java/com/pj/test/SaTempTemplateForJwtTest.java b/sa-token-test/sa-token-temp-jwt-test/src/test/java/com/pj/test/SaTempTemplateForJwtTest.java new file mode 100644 index 00000000..daf1fe32 --- /dev/null +++ b/sa-token-test/sa-token-temp-jwt-test/src/test/java/com/pj/test/SaTempTemplateForJwtTest.java @@ -0,0 +1,105 @@ +package com.pj.test; + +import cn.dev33.satoken.SaManager; +import cn.dev33.satoken.dao.SaTokenDao; +import cn.dev33.satoken.exception.ApiDisabledException; +import cn.dev33.satoken.temp.SaTempUtil; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +/** + * Sa-Token 整合 temp jwt + * + * @author click33 + * @since 1.42.0 + */ +@SpringBootTest(classes = StartUpApplication.class) +public class SaTempTemplateForJwtTest { + + // 开始 + @BeforeAll + public static void beforeClass() { + System.out.println("\n\n------------------------ SaTempTemplateForJwtTest star ..."); + } + + // 结束 + @AfterAll + public static void afterClass() { + System.out.println("\n\n------------------------ SaTempTemplateForJwtTest end ... \n"); + } + + // 测试:临时Token认证模块 + @Test + public void testSaTemp() { + + // 生成token + String token = SaTempUtil.createToken("group-1014", 200); + // System.out.println(((SaTokenDaoDefaultImpl)SaManager.getSaTokenDao()).timedCache.dataMap.keySet()); + // System.out.println("satoken:temp-token:" + ":" + token); + Assertions.assertNotNull(token); + + // 解析token + String value = SaTempUtil.parseToken(token, String.class); + Assertions.assertEquals(value, "group-1014"); + + // 解析 token 并裁剪前缀 + long value2 = SaTempUtil.parseToken(token, "group-", Long.class); + Assertions.assertEquals(value2, 1014); + + // 默认类型 + Object value3 = SaTempUtil.parseToken(token); + Assertions.assertEquals(value3, "group-1014"); + + // 转换类型 + String value4 = SaTempUtil.parseToken(token, String.class); + Assertions.assertEquals(value4, "group-1014"); + + // 过期时间 + long timeout = SaTempUtil.getTimeout(token); + Assertions.assertTrue(timeout > 195); + Assertions.assertTrue(timeout < 201); + + // 回收token + Assertions.assertThrows(ApiDisabledException.class, () -> SaTempUtil.deleteToken(token) ); + } + + // 测试:临时Token认证模块索引 + @Test + public void testSaTempIndex() { + SaTokenDao dao = SaManager.getSaTokenDao(); + + // 生成token + String token1 = SaTempUtil.createToken("1001", 200, true); + String token2 = SaTempUtil.createToken("1001", 300, true); + String token3 = SaTempUtil.createToken("1001", 400, true); + + Assertions.assertNotNull(token1); + Assertions.assertNotNull(token2); + Assertions.assertNotNull(token3); + // System.out.println(((SaTokenDaoDefaultImpl)SaManager.getSaTokenDao()).dataMap); + + // 解析token + Assertions.assertEquals(SaTempUtil.parseToken(token1, String.class), "1001"); + Assertions.assertEquals(SaTempUtil.parseToken(token2, String.class), "1001"); + Assertions.assertEquals(SaTempUtil.parseToken(token3, String.class), "1001"); + + // 缓存数据比对 + Assertions.assertNull(dao.getObject("satoken:temp-token:" + token1)); + Assertions.assertNull(dao.getObject("satoken:temp-token:" + token2)); + Assertions.assertNull(dao.getObject("satoken:temp-token:" + token3)); + + // 索引 + Assertions.assertThrows(ApiDisabledException.class, () -> SaTempUtil.getTempTokenList("1001") ); + } + + @Test + public void testGetJwtSecretKey() { + // 秘钥默认为null + String jwtSecretKey = SaManager.getSaTempTemplate().getJwtSecretKey(); + Assertions.assertNotNull(jwtSecretKey); + } + +} diff --git a/sa-token-test/sa-token-temp-jwt-test/src/test/java/com/pj/test/StartUpApplication.java b/sa-token-test/sa-token-temp-jwt-test/src/test/java/com/pj/test/StartUpApplication.java new file mode 100644 index 00000000..2a174efd --- /dev/null +++ b/sa-token-test/sa-token-temp-jwt-test/src/test/java/com/pj/test/StartUpApplication.java @@ -0,0 +1,16 @@ +package com.pj.test; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +/** + * 启动类 + * @author Auster + * + */ +@SpringBootApplication +public class StartUpApplication { + public static void main(String[] args) { + SpringApplication.run(StartUpApplication.class, args); + } +} diff --git a/sa-token-test/sa-token-temp-jwt-test/src/test/resources/application.yml b/sa-token-test/sa-token-temp-jwt-test/src/test/resources/application.yml new file mode 100644 index 00000000..1b25a92c --- /dev/null +++ b/sa-token-test/sa-token-temp-jwt-test/src/test/resources/application.yml @@ -0,0 +1,46 @@ +# 端口 +server: + port: 8081 + +# sa-token 配置 +sa-token: + # token 名称 (同时也是 cookie 名称) + token-name: satoken + # token 有效期(单位:秒) 默认30天,-1 代表永久有效 + timeout: 2592000 + # token 最低活跃频率(单位:秒),如果 token 超过此时间没有访问系统就会被冻结,默认-1 代表不限制,永不冻结 + active-timeout: -1 + # 是否允许同一账号多地同时登录 (为 true 时允许一起登录, 为 false 时新登录挤掉旧登录) + is-concurrent: true + # 在多人登录同一账号时,是否共用一个 token (为 true 时所有登录共用一个 token, 为 false 时每次登录新建一个 token) + is-share: false + # token 风格(默认可取值:uuid、simple-uuid、random-32、random-64、random-128、tik) + token-style: uuid + # jwt秘钥 + jwt-secret-key: asdasdasifhueuiwyurfewbfjsdafjk + +spring: + # redis配置 + redis: + # Redis数据库索引(默认为0) + database: 0 + # Redis服务器地址 + host: 127.0.0.1 + # Redis服务器连接端口 + port: 6379 + # Redis服务器连接密码(默认为空) + password: + # 连接超时时间(毫秒) + timeout: 10000ms + lettuce: + pool: + # 连接池最大连接数 + max-active: 200 + # 连接池最大阻塞等待时间(使用负值表示没有限制) + max-wait: -1ms + # 连接池中的最大空闲连接 + max-idle: 10 + # 连接池中的最小空闲连接 + min-idle: 0 + + \ No newline at end of file