升级临时 Token 认证模块,可指定 service 参数。

This commit is contained in:
click33 2022-12-07 09:59:23 +08:00
parent 57d2843f90
commit 385cf3b2e4
12 changed files with 339 additions and 62 deletions

View File

@ -155,4 +155,15 @@ public interface SaErrorCode {
/** RSA 私钥解密异常 */
public static final int CODE_12119 = 12119;
// ------------
/** 参与参数签名的秘钥不可为空 */
public static final int CODE_12201 = 12201;
/** 给定的签名无效 */
public static final int CODE_12202 = 12202;
/** timestamp 超出允许的范围 */
public static final int CODE_12203 = 12203;
}

View File

@ -3,6 +3,8 @@ package cn.dev33.satoken.sign;
import java.util.Map;
import java.util.TreeMap;
import cn.dev33.satoken.error.SaErrorCode;
import cn.dev33.satoken.exception.SaTokenException;
import cn.dev33.satoken.secure.SaSecureUtil;
import cn.dev33.satoken.util.SaFoxUtil;
@ -15,15 +17,11 @@ import cn.dev33.satoken.util.SaFoxUtil;
public interface SaSignTemplate {
/**
* 将所有参数连接成一个字符串
* 将所有参数连接成一个字符串(不排序)形如b=2&a=1&c=3
* @param paramsMap 参数列表
* @return 字符串
* @return 拼接出的参数字符串
*/
public default String joinParams(Map<String, Object> paramsMap) {
// 保证字段按照字典顺序排列
if(paramsMap instanceof TreeMap == false) {
paramsMap = new TreeMap<>(paramsMap);
}
// 按照 k1=v1&k2=v2&k3=v3 排列
StringBuilder sb = new StringBuilder();
@ -43,6 +41,21 @@ public interface SaSignTemplate {
return sb.toString();
}
/**
* 将所有参数按照字典顺序连接成一个字符串形如a=1&b=2&c=3
* @param paramsMap 参数列表
* @return 拼接出的参数字符串
*/
public default String joinParamsDictSort(Map<String, Object> paramsMap) {
// 保证字段按照字典顺序排列
if(paramsMap instanceof TreeMap == false) {
paramsMap = new TreeMap<>(paramsMap);
}
// 拼接
return joinParams(paramsMap);
}
/**
* 创建签名md5(paramsStr + keyStr)
* @param paramsMap 参数列表
@ -50,10 +63,85 @@ public interface SaSignTemplate {
* @return 签名
*/
public default String createSign(Map<String, Object> paramsMap, String key) {
String paramsStr = joinParams(paramsMap);
SaTokenException.throwByNull(key, "参与参数签名的秘钥不可为空", SaErrorCode.CODE_12201);
String paramsStr = joinParamsDictSort(paramsMap);
String fullStr = paramsStr + "&key=" + key;
return SaSecureUtil.md5(fullStr);
}
/**
* 判断给定的参数 + 秘钥 生成的签名是否为有效签名
* @param paramsMap 参数列表
* @param key 秘钥
* @param sign 待验证的签名
* @return 签名是否有效
*/
public default boolean isValidSign(Map<String, Object> paramsMap, String key, String sign) {
String theSign = createSign(paramsMap, key);
return theSign.equals(sign);
}
/**
* 校验给定的参数 + 秘钥 生成的签名是否为有效签名如果签名无效则抛出异常
* @param paramsMap 参数列表
* @param key 秘钥
* @param sign 待验证的签名
*/
public default void checkSign(Map<String, Object> paramsMap, String key, String sign) {
if(isValidSign(paramsMap, key, sign) == false) {
throw new SaTokenException("无效签名:" + sign).setCode(SaErrorCode.CODE_12202);
}
}
/**
* paramsMap 追加 timestampnoncesign 三个参数
* @param paramsMap 参数列表
* @param key 秘钥
* @return 加工后的参数列表
*/
public default Map<String, Object> addSignParams(Map<String, Object> paramsMap, String key) {
paramsMap.put("timestamp", String.valueOf(System.currentTimeMillis()));
paramsMap.put("nonce", SaFoxUtil.getRandomString(32));
paramsMap.put("sign", createSign(paramsMap, key));
return paramsMap;
}
/**
* paramsMap 追加 timestampnoncesign 三个参数并转换为参数字符串形如
* <code>data=xxx&nonce=xxx&timestamp=xxx&sign=xxx</code>
* @param paramsMap 参数列表
* @param key 秘钥
* @return 加工后的参数列表 转化为的参数字符串
*/
public default String addSignParamsToString(Map<String, Object> paramsMap, String key) {
// 追加参数
paramsMap = addSignParams(paramsMap, key);
// .
return joinParams(paramsMap);
}
/**
* 判断指定时间戳与当前时间戳的差距是否在允许的范围内
* @param timestamp 待校验的时间戳
* @param allowDisparity 允许的最大时间差单位ms-1 代表不限制
* @return 是否在允许的范围内
*/
public default boolean isValidTimestamp(long timestamp, long allowDisparity) {
long disparity = Math.abs(System.currentTimeMillis() - timestamp);
return allowDisparity == -1 || disparity <= allowDisparity;
}
/**
* 校验指定时间戳与当前时间戳的差距是否在允许的范围内如果超出则抛出异常
* @param timestamp 待校验的时间戳
* @param allowDisparity 允许的最大时间差单位ms-1 代表不限制
*/
public default void checkTimestamp(long timestamp, long allowDisparity) {
if(isValidTimestamp(timestamp, allowDisparity) == false) {
throw new SaTokenException("timestamp 超出允许的范围:" + timestamp).setCode(SaErrorCode.CODE_12203);
}
}
}

View File

@ -3,6 +3,7 @@ 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 临时令牌验证模块接口
@ -12,18 +13,29 @@ import cn.dev33.satoken.util.SaFoxUtil;
public interface SaTempInterface {
/**
* 根据value创建一个token
* 指定值 创建一个临时 Token
* @param value 指定值
* @param timeout 有效期单位
* @param timeout 有效期单位-1代表永久有效
* @return 生成的token
*/
public default String createToken(Object value, long timeout) {
return createToken(SaTokenConsts.DEFAULT_TEMP_TOKEN_SERVICE, value, timeout);
}
/**
* 指定服务 指定值 创建一个 Token
* @param service 服务标识
* @param value 指定值
* @param timeout 有效期单位-1代表永久有效
* @return 生成的token
*/
public default String createToken(String service, Object value, long timeout) {
// 生成 token
String token = SaStrategy.me.createToken.apply(null, null);
// 持久化映射关系
String key = splicingKeyTempToken(token);
String key = splicingKeyTempToken(service, token);
SaManager.getSaTokenDao().setObject(key, value, timeout);
// 返回
@ -31,43 +43,85 @@ public interface SaTempInterface {
}
/**
* 解析token获取value
* @param token 指定token
* @return See Note
* 解析 Token 获取 value
* @param token 指定 Token
* @return /
*/
public default Object parseToken(String token) {
String key = splicingKeyTempToken(token);
return parseToken(SaTokenConsts.DEFAULT_TEMP_TOKEN_SERVICE, token);
}
/**
* 解析 Token 获取 value
* @param service 服务标识
* @param token 指定 Token
* @return /
*/
public default Object parseToken(String service, String token) {
String key = splicingKeyTempToken(service, token);
return SaManager.getSaTokenDao().getObject(key);
}
/**
* 解析token获取value并转换为指定类型
* @param token 指定token
* 解析 Token 获取 value并转换为指定类型
* @param token 指定 Token
* @param cs 指定类型
* @param <T> 默认值的类型
* @return See Note
* @return /
*/
public default<T> T parseToken(String token, Class<T> cs) {
return SaFoxUtil.getValueByType(parseToken(token), cs);
return parseToken(SaTokenConsts.DEFAULT_TEMP_TOKEN_SERVICE, token, cs);
}
/**
* 解析 Token 获取 value并转换为指定类型
* @param service 服务标识
* @param token 指定 Token
* @param cs 指定类型
* @param <T> 默认值的类型
* @return /
*/
public default<T> T parseToken(String service, String token, Class<T> cs) {
return SaFoxUtil.getValueByType(parseToken(service, token), cs);
}
/**
* 获取指定 token 的剩余有效期单位
* 获取指定 Token 的剩余有效期单位
* <p> 返回值 -1 代表永久-2 代表token无效
* @param token see note
* @return see note
* @param token 指定 Token
* @return /
*/
public default long getTimeout(String token) {
String key = splicingKeyTempToken(token);
return getTimeout(SaTokenConsts.DEFAULT_TEMP_TOKEN_SERVICE, token);
}
/**
* 获取指定服务指定 Token 的剩余有效期单位
* <p> 返回值 -1 代表永久-2 代表token无效
* @param service 服务标识
* @param token 指定 Token
* @return /
*/
public default long getTimeout(String service, String token) {
String key = splicingKeyTempToken(service, token);
return SaManager.getSaTokenDao().getObjectTimeout(key);
}
/**
* 删除一个 token
* @param token 指定token
* 删除一个 Token
* @param token 指定 Token
*/
public default void deleteToken(String token) {
String key = splicingKeyTempToken(token);
deleteToken(SaTokenConsts.DEFAULT_TEMP_TOKEN_SERVICE, token);
}
/**
* 删除一个 Token
* @param service 服务标识
* @param token 指定 Token
*/
public default void deleteToken(String service, String token) {
String key = splicingKeyTempToken(service, token);
SaManager.getSaTokenDao().deleteObject(key);
}
@ -76,8 +130,8 @@ public interface SaTempInterface {
* @param token token值
* @return key
*/
public default String splicingKeyTempToken(String token) {
return SaManager.getConfig().getTokenName() + ":temp-token:" + token;
public default String splicingKeyTempToken(String service, String token) {
return SaManager.getConfig().getTokenName() + ":temp-token:" + service + ":" + token;
}
/**

View File

@ -11,53 +11,106 @@ public class SaTempUtil {
private SaTempUtil() {
}
/**
* 根据value创建一个token
* 指定值 创建一个临时 Token
* @param value 指定值
* @param timeout 有效期单位
* @param timeout 有效期单位-1代表永久有效
* @return 生成的token
*/
public static String createToken(Object value, long timeout) {
return SaManager.getSaTemp().createToken(value, timeout);
}
/**
* 解析token获取value
* @param token 指定token
* @return See Note
* 指定服务 指定值 创建一个 Token
* @param service 服务标识
* @param value 指定值
* @param timeout 有效期单位-1代表永久有效
* @return 生成的token
*/
public static String createToken(String service, Object value, long timeout) {
return SaManager.getSaTemp().createToken(service, value, timeout);
}
/**
* 解析 Token 获取 value
* @param token 指定 Token
* @return /
*/
public static Object parseToken(String token) {
return SaManager.getSaTemp().parseToken(token);
}
/**
* 解析token获取value并转换为指定类型
* @param token 指定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 <T> 默认值的类型
* @return See Note
* @return /
*/
public static<T> T parseToken(String token, Class<T> cs) {
return SaManager.getSaTemp().parseToken(token, cs);
}
/**
* 解析 Token 获取 value并转换为指定类型
* @param service 服务标识
* @param token 指定 Token
* @param cs 指定类型
* @param <T> 默认值的类型
* @return /
*/
public static<T> T parseToken(String service, String token, Class<T> cs) {
return SaManager.getSaTemp().parseToken(service, token, cs);
}
/**
* 获取指定 token 的剩余有效期单位
* 获取指定 Token 的剩余有效期单位
* <p> 返回值 -1 代表永久-2 代表token无效
* @param token see note
* @return see note
* @param token 指定 Token
* @return /
*/
public static long getTimeout(String token) {
return SaManager.getSaTemp().getTimeout(token);
}
/**
* 删除一个 token
* @param token 指定token
* 获取指定服务指定 Token 的剩余有效期单位
* <p> 返回值 -1 代表永久-2 代表token无效
* @param service 服务标识
* @param token 指定 Token
* @return /
*/
public static long getTimeout(String service, String token) {
return SaManager.getSaTemp().getTimeout(service, token);
}
/**
* 删除一个 Token
* @param token 指定 Token
*/
public static void deleteToken(String token) {
SaManager.getSaTemp().deleteToken(token);
}
/**
* 删除一个 Token
* @param service 服务标识
* @param token 指定 Token
*/
public static void deleteToken(String service, String token) {
SaManager.getSaTemp().deleteToken(service, token);
}
}

View File

@ -103,6 +103,16 @@ public class SaFoxUtil {
public static boolean equals(Object a, Object b) {
return (a == b) || (a != null && a.equals(b));
}
/**
* 比较两个对象是否不相等
* @param a 第一个对象
* @param b 第二个对象
* @return 两个对象是否不相等
*/
public static boolean notEquals(Object a, Object b) {
return !equals(a, b);
}
/**
* 以当前时间戳和随机int数字拼接一个随机字符串

View File

@ -95,6 +95,11 @@ public class SaTokenConsts {
*/
public static final String DEFAULT_SAFE_AUTH_SERVICE = "important";
/**
* 常量key标记: 临时 Token 认证模块默认的业务类型
*/
public static final String DEFAULT_TEMP_TOKEN_SERVICE = "record";
// =================== token-style 相关 ===================

View File

@ -104,6 +104,9 @@ SaToken 中的所有异常都是继承于 `SaTokenException` 的,也就是说
| 12117 | RSA 私钥加密异常 |
| 12118 | RSA 公钥解密异常 |
| 12119 | RSA 私钥解密异常 |
| 12201 | 参与参数签名的秘钥不可为空 |
| 12202 | 给定的签名无效 |
| 12203 | timestamp 超出允许的范围 |
#### sa-token-servlet

View File

@ -16,9 +16,9 @@ import io.jsonwebtoken.SignatureAlgorithm;
public class SaJwtUtil {
/**
* key: value
* key: value 前缀
*/
public static final String KEY_VALUE = "value";
public static final String KEY_VALUE = "value_";
/**
* key: 有效期 (时间戳)
@ -30,12 +30,13 @@ public class SaJwtUtil {
/**
* 根据指定值创建 jwt-token
* @param key 存储value使用的key
* @param value 要保存的值
* @param timeout token有效期 (单位 )
* @param keyt 秘钥
* @return jwt-token
*/
public static String createToken(Object value, long timeout, String keyt) {
public static String createToken(String key, Object value, long timeout, String keyt) {
// 计算eff有效期
long eff = timeout;
if(timeout != NEVER_EXPIRE) {
@ -44,7 +45,7 @@ public class SaJwtUtil {
// 在这里你可以使用官方提供的claim方法构建载荷也可以使用setPayload自定义载荷但是两者不可一起使用
JwtBuilder builder = Jwts.builder()
// .setHeaderParam("typ", "JWT")
.claim(KEY_VALUE, value)
.claim(KEY_VALUE + key, value)
.claim(KEY_EFF, eff)
.signWith(SignatureAlgorithm.HS256, keyt.getBytes());
// 生成jwt-token
@ -68,11 +69,12 @@ public class SaJwtUtil {
/**
* 从一个 jwt-token 解析出载荷, 并取出数据
* @param key 存储value使用的key
* @param jwtToken JwtToken值
* @param keyt 秘钥
* @return
*/
public static Object getValue(String jwtToken, String keyt) {
public static Object getValue(String key, String jwtToken, String keyt) {
// 取出数据
Claims claims = parseToken(jwtToken, keyt);
@ -83,7 +85,7 @@ public class SaJwtUtil {
}
// 获取数据
return claims.get(KEY_VALUE);
return claims.get(KEY_VALUE + key);
}
/**
@ -92,10 +94,15 @@ public class SaJwtUtil {
* @param keyt 秘钥
* @return
*/
public static long getTimeout(String jwtToken, String keyt) {
public static long getTimeout(String key, String jwtToken, String keyt) {
// 取出数据
Claims claims = parseToken(jwtToken, keyt);
// 如果给定的key不对
if(claims.get(KEY_VALUE + key) == null) {
return SaTokenDao.NOT_VALUE_EXPIRE;
}
// 验证是否超时
Long eff = claims.get(KEY_EFF, Long.class);

View File

@ -18,8 +18,8 @@ public class SaTempForJwt implements SaTempInterface {
* 根据value创建一个token
*/
@Override
public String createToken(Object value, long timeout) {
String token = SaJwtUtil.createToken(value, timeout, getJwtSecretKey());
public String createToken(String service, Object value, long timeout) {
String token = SaJwtUtil.createToken(service, value, timeout, getJwtSecretKey());
return token;
}
@ -27,8 +27,8 @@ public class SaTempForJwt implements SaTempInterface {
* 解析token获取value
*/
@Override
public Object parseToken(String token) {
Object value = SaJwtUtil.getValue(token, getJwtSecretKey());
public Object parseToken(String service, String token) {
Object value = SaJwtUtil.getValue(service, token, getJwtSecretKey());
return value;
}
@ -36,8 +36,8 @@ public class SaTempForJwt implements SaTempInterface {
* 返回指定token的剩余有效期单位
*/
@Override
public long getTimeout(String token) {
long timeout = SaJwtUtil.getTimeout(token, getJwtSecretKey());
public long getTimeout(String service, String token) {
long timeout = SaJwtUtil.getTimeout(service, token, getJwtSecretKey());
return timeout;
}
@ -45,7 +45,7 @@ public class SaTempForJwt implements SaTempInterface {
* 删除一个token
*/
@Override
public void deleteToken(String token) {
public void deleteToken(String service, String token) {
throw new ApiDisabledException("jwt cannot delete token").setCode(SaTempJwtErrorCode.CODE_30302);
}

View File

@ -28,6 +28,7 @@ public class SaJsonTemplateForJackson implements SaJsonTemplate {
* @param obj 对象
* @return 转换后的 json 字符串
*/
@Override
public String toJsonString(Object obj) {
try {
return objectMapper.writeValueAsString(obj);

View File

@ -18,12 +18,12 @@ public class SaSignTemplateTest {
// 连接参数列表
@Test
public void testJoinParams() {
public void testJoinParamsDictSort() {
SoMap map = SoMap.getSoMap()
.set("name", "zhang")
.set("age", 18)
.set("sex", "");
String str = SaManager.getSaSignTemplate().joinParams(map);
String str = SaManager.getSaSignTemplate().joinParamsDictSort(map);
// 按照音序排列
Assertions.assertEquals(str, "age=18&name=zhang&sex=女");

View File

@ -6,6 +6,7 @@ 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模块测试
@ -23,15 +24,21 @@ public class SaTempTest {
// 生成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:" + token), "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);
@ -40,7 +47,45 @@ public class SaTempTest {
SaTempUtil.deleteToken(token);
String value2 = SaTempUtil.parseToken(token, String.class);
Assertions.assertEquals(value2, null);
Assertions.assertEquals(dao.getObject("satoken:temp-token:" + token), 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