使用 Sa-Token 内置的 sign 模块,方便的完成 API 签名创建、校验等步骤:
<dependency>
<groupId>cn.dev33groupId>
<artifactId>sa-token-spring-boot-starterartifactId>
<version>1.35.0.RCversion>
dependency>
sa-token:
sign:
# API 接口签名秘钥 (随便乱摁几个字母即可)
secret-key: kQwIOrYvnXmSDkwEiFngrKidMcdrgKor
String url = "http://b.com/api/addMoney";
// 请求参数
Map<String, Object> paramMap = new LinkedHashMap<>();
paramMap.put("userId", 10001);
paramMap.put("money", 1000);
// 更多参数,不限制数量...
// 补全 timestamp、nonce、sign 参数,并序列化为 kv 字符串
String paramStr = SaSignUtil.addSignParamsAndJoin(paramMap);
// 将参数字符串拼接在请求地址后面
url += "?" + paramStr;
// 发送请求
String res = HttpUtil.request(url);
// 根据返回值做后续处理
System.out.println("server 端返回信息:" + res);
@RequestMapping("addMoney")
public SaResult addMoney(long userId, long money) {
// 1、校验请求中的签名
SaSignUtil.checkRequest(SaHolder.getRequest());
// 2、校验通过,处理业务
System.out.println("userId=" + userId);
System.out.println("money=" + money);
// 3、返回
return SaResult.ok();
}
SaSignUtil#addSignParamsAndJoin
public static String addSignParamsAndJoin(Map<String, Object> paramsMap) {
return SaManager.getSaSignTemplate().addSignParamsAndJoin(paramsMap);
}
SaSignTemplate#addSignParamsAndJoin
,新增签名参数
public String addSignParamsAndJoin(Map paramsMap) {
paramsMap = this.addSignParams(paramsMap);
return this.joinParams(paramsMap);
}
SaSignTemplate#addSignParams
,新增了timestamp
,nonce
随机数和sign
。
public Map<String, Object> addSignParams(Map<String, Object> paramsMap) {
paramsMap.put(timestamp, String.valueOf(System.currentTimeMillis()));
paramsMap.put(nonce, SaFoxUtil.getRandomString(32));
paramsMap.put(sign, this.createSign(paramsMap));
return paramsMap;
}
SaSignTemplate#createSign
,创建签名。按照Map中的自然排序进行拼接
public String createSign(Map<String, ?> paramsMap) {
String secretKey = this.getSecretKey();
SaSignException.throwByNull(secretKey, "参与参数签名的秘钥不可为空", 12201);
if (((Map)paramsMap).containsKey(sign)) {
paramsMap = new TreeMap((Map)paramsMap);
((Map)paramsMap).remove(sign);
}
String paramsStr = this.joinParamsDictSort((Map)paramsMap);
String fullStr = paramsStr + "&" + key + "=" + secretKey;
return this.abstractStr(fullStr);
}
public String joinParamsDictSort(Map<String, ?> paramsMap) {
if (!(paramsMap instanceof TreeMap)) {
paramsMap = new TreeMap((Map)paramsMap);
}
return this.joinParams((Map)paramsMap);
}
SaSignTemplate#abstractStr
,加密方式是md5
。
public String abstractStr(String fullStr) {
return SaSecureUtil.md5(fullStr);
}
SaSignUtil#checkRequest
,校验请求
public static void checkRequest(SaRequest request) {
SaManager.getSaSignTemplate().checkRequest(request);
}
SaSignTemplate#checkRequest
,校验请求中的参数
public void checkRequest(SaRequest request) {
this.checkParamMap(request.getParamMap());
}
SaSignTemplate#checkParamMap
,校验请求中的参数timestampValue
,nonceValue
,signValue
public void checkParamMap(Map paramMap) {
String timestampValue = (String)paramMap.get(timestamp);
String nonceValue = (String)paramMap.get(nonce);
String signValue = (String)paramMap.get(sign);
this.checkTimestamp(Long.parseLong(timestampValue));
if (this.getSignConfigOrGlobal().getIsCheckNonce()) {
this.checkNonce(nonceValue);
}
this.checkSign(paramMap, signValue);
}
SaSignTemplate#checkTimestamp
,判断在一定范围内
public void checkTimestamp(long timestamp) {
if (!this.isValidTimestamp(timestamp)) {
throw (new SaSignException("timestamp 超出允许的范围:" + timestamp)).setCode(12203);
}
}
public boolean isValidTimestamp(long timestamp) {
long allowDisparity = this.getSignConfigOrGlobal().getTimestampDisparity();
long disparity = Math.abs(System.currentTimeMillis() - timestamp);
return allowDisparity == -1L || disparity <= allowDisparity;
}
SaSignTemplate#checkNonce
,校验随机数
public void checkNonce(String nonce) {
if (SaFoxUtil.isEmpty(nonce)) {
throw new SaSignException("nonce 为空,无效");
} else {
String key = this.splicingNonceSaveKey(nonce);
if (SaManager.getSaTokenDao().get(key) != null) {
throw new SaSignException("此 nonce 已被使用过,不可重复使用:" + nonce);
} else {
SaManager.getSaTokenDao().set(key, nonce, this.getSignConfigOrGlobal().getSaveNonceExpire() * 2L + 2L);
}
}
}
SaSignTemplate#checkSign
,根据请求中的参数数据创建签名,判断和原有签名是否相同。
public void checkSign(Map<String, ?> paramsMap, String sign) {
if (!this.isValidSign(paramsMap, sign)) {
throw (new SaSignException("无效签名:" + sign)).setCode(12202);
}
}
public boolean isValidSign(Map<String, ?> paramsMap, String sign) {
String theSign = this.createSign(paramsMap);
return theSign.equals(sign);
}
SaTokenDao
是存储接口。
默认实现是用的是SaTokenDaoDefaultImpl
。SaTokenDaoDefaultImpl
存储数据,主要是通过ConcurrentHashMap
存放在本地内存中。
public class SaTokenDaoDefaultImpl implements SaTokenDao {
/**
* 数据集合
*/
public Map<String, Object> dataMap = new ConcurrentHashMap<String, Object>();
/**
* 过期时间集合 (单位: 毫秒) , 记录所有key的到期时间 [注意不是剩余存活时间]
*/
public Map<String, Long> expireMap = new ConcurrentHashMap<String, Long>();
/**
* 构造函数
*/
public SaTokenDaoDefaultImpl() {
initRefreshThread();
}
// ------------------------ String 读写操作
@Override
public String get(String key) {
clearKeyByTimeout(key);
return (String)dataMap.get(key);
}
@Override
public void set(String key, String value, long timeout) {
if(timeout == 0 || timeout <= SaTokenDao.NOT_VALUE_EXPIRE) {
return;
}
dataMap.put(key, value);
expireMap.put(key, (timeout == SaTokenDao.NEVER_EXPIRE) ? (SaTokenDao.NEVER_EXPIRE) : (System.currentTimeMillis() + timeout * 1000));
}
}
SaTokenDaoDefaultImpl#initRefreshThread
,对过期数据要定时清除
/**
* 初始化定时任务
*/
public void initRefreshThread() {
// 如果配置了<=0的值,则不启动定时清理
if(SaManager.getConfig().getDataRefreshPeriod() <= 0) {
return;
}
// 启动定时刷新
this.refreshFlag = true;
this.refreshThread = new Thread(() -> {
for (;;) {
try {
try {
// 如果已经被标记为结束
if(refreshFlag == false) {
return;
}
// 执行清理
refreshDataMap();
} catch (Exception e) {
e.printStackTrace();
}
// 休眠N秒
int dataRefreshPeriod = SaManager.getConfig().getDataRefreshPeriod();
if(dataRefreshPeriod <= 0) {
dataRefreshPeriod = 1;
}
Thread.sleep(dataRefreshPeriod * 1000);
} catch (Exception e) {
e.printStackTrace();
}
}
});
this.refreshThread.start();
}
/**
* 清理所有已经过期的key
*/
public void refreshDataMap() {
Iterator<String> keys = expireMap.keySet().iterator();
while (keys.hasNext()) {
clearKeyByTimeout(keys.next());
}
}
如果仅仅存放在本地内存中,涉及到多个项目,可能数据无法共享。
引入仓库sa-token-dao-redis-jackson
。
<dependency>
<groupId>cn.dev33groupId>
<artifactId>sa-token-dao-redis-jacksonartifactId>
<version>1.33.0version>
dependency>
SaTokenDaoRedisJackson
使用Redis作为存储数据的地方。
@Component
public class SaTokenDaoRedisJackson implements SaTokenDao {
public static final String DATE_TIME_PATTERN = "yyyy-MM-dd HH:mm:ss";
public static final String DATE_PATTERN = "yyyy-MM-dd";
public static final String TIME_PATTERN = "HH:mm:ss";
public static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
public static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd");
public static final DateTimeFormatter TIME_FORMATTER = DateTimeFormatter.ofPattern("HH:mm:ss");
public ObjectMapper objectMapper;
public StringRedisTemplate stringRedisTemplate;
public RedisTemplate<String, Object> objectRedisTemplate;
public boolean isInit;
public SaTokenDaoRedisJackson() {
}
@Autowired
public void init(RedisConnectionFactory connectionFactory) {
if (!this.isInit) {
StringRedisSerializer keySerializer = new StringRedisSerializer();
GenericJackson2JsonRedisSerializer valueSerializer = new GenericJackson2JsonRedisSerializer();
try {
Field field = GenericJackson2JsonRedisSerializer.class.getDeclaredField("mapper");
field.setAccessible(true);
ObjectMapper objectMapper = (ObjectMapper)field.get(valueSerializer);
this.objectMapper = objectMapper;
this.objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
JavaTimeModule timeModule = new JavaTimeModule();
timeModule.addSerializer(new LocalDateTimeSerializer(DATE_TIME_FORMATTER));
timeModule.addDeserializer(LocalDateTime.class, new LocalDateTimeDeserializer(DATE_TIME_FORMATTER));
timeModule.addSerializer(new LocalDateSerializer(DATE_FORMATTER));
timeModule.addDeserializer(LocalDate.class, new LocalDateDeserializer(DATE_FORMATTER));
timeModule.addSerializer(new LocalTimeSerializer(TIME_FORMATTER));
timeModule.addDeserializer(LocalTime.class, new LocalTimeDeserializer(TIME_FORMATTER));
this.objectMapper.registerModule(timeModule);
SaStrategy.me.createSession = (sessionId) -> {
return new SaSessionForJacksonCustomized(sessionId);
};
} catch (Exception var7) {
System.err.println(var7.getMessage());
}
StringRedisTemplate stringTemplate = new StringRedisTemplate();
stringTemplate.setConnectionFactory(connectionFactory);
stringTemplate.afterPropertiesSet();
RedisTemplate<String, Object> template = new RedisTemplate();
template.setConnectionFactory(connectionFactory);
template.setKeySerializer(keySerializer);
template.setHashKeySerializer(keySerializer);
template.setValueSerializer(valueSerializer);
template.setHashValueSerializer(valueSerializer);
template.afterPropertiesSet();
this.stringRedisTemplate = stringTemplate;
this.objectRedisTemplate = template;
this.isInit = true;
}
}
public String get(String key) {
return (String)this.stringRedisTemplate.opsForValue().get(key);
}
public void set(String key, String value, long timeout) {
if (timeout != 0L && timeout > -2L) {
if (timeout == -1L) {
this.stringRedisTemplate.opsForValue().set(key, value);
} else {
this.stringRedisTemplate.opsForValue().set(key, value, timeout, TimeUnit.SECONDS);
}
}
}
}
SaTokenDao
SaBeanInject#setSaTokenDao
,SaBeanInject
是自动配置的。当系统中存在SaTokenDao
的Bean实例,则设置SaTokenDao
实例。
@Autowired(
required = false
)
public void setSaTokenDao(SaTokenDao saTokenDao) {
SaManager.setSaTokenDao(saTokenDao);
}
SaManager#setSaTokenDao
,设置SaTokenDao
实例
public static void setSaTokenDao(SaTokenDao saTokenDao) {
setSaTokenDaoMethod(saTokenDao);
SaTokenEventCenter.doRegisterComponent("SaTokenDao", saTokenDao);
}
private static void setSaTokenDaoMethod(SaTokenDao saTokenDao) {
if((SaManager.saTokenDao instanceof SaTokenDaoDefaultImpl)) {
((SaTokenDaoDefaultImpl)SaManager.saTokenDao).endRefreshThread();
}
SaManager.saTokenDao = saTokenDao;
}
SaManager#getSaTokenDao
,如果saTokenDao
没有设置,则获取默认的实现SaTokenDaoDefaultImpl
public static SaTokenDao getSaTokenDao() {
if (saTokenDao == null) {
synchronized (SaManager.class) {
if (saTokenDao == null) {
setSaTokenDaoMethod(new SaTokenDaoDefaultImpl());
}
}
}
return saTokenDao;
}