SpringBoot整合Sa-Token實(shí)現(xiàn)?API?接口簽名安全校驗(yàn)功能
在涉及跨系統(tǒng)接口調(diào)用時(shí),我們?nèi)菀着龅揭韵掳踩珕?wèn)題:
- 請(qǐng)求身份被偽造
- 請(qǐng)求參數(shù)被篡改
- 請(qǐng)求被抓包,然后重放攻擊
sa-token api-sign
模塊將幫你輕松解決以上難題。(此插件是內(nèi)嵌到 sa-token-core 核心包中的模塊,開(kāi)發(fā)者無(wú)需再次引入其它依賴,插件直接可用)
假設(shè)我們有如下業(yè)務(wù)需求:
用戶在 A 系統(tǒng)參與活動(dòng)成功后,活動(dòng)獎(jiǎng)勵(lì)以余額的形式下發(fā)到 B 系統(tǒng)。
1. 初始方案:直接裸奔
在不考慮安全問(wèn)題的情況下,我們很容易完成這個(gè)需求:
1、在 B 系統(tǒng)開(kāi)放一個(gè)接口
@RestController @RequestMapping("/sign") public class SignController { @PostMapping("/addMoney") public String addMoney(Long userId, Long money) { // TODO 處理業(yè)務(wù)... return "ADD SUCCESS"; } }
2、在 A 系統(tǒng)使用 http 工具類調(diào)用這個(gè)接口
@RestController @RequestMapping("/activity") public class ActivityController { @PostMapping("/join") public String join() { // 參加完活動(dòng)后,發(fā)送余額 Long userId = 1L; Long money = 100L; Map<String, Object> params = new HashMap<>(); params.put("userId", userId); params.put("money", money); String url = "http://localhost:8079/sign/addMoney"; String result = HttpUtil.post(url, params); return "join"; } }
上述代碼簡(jiǎn)單的完成了需求,但是很明顯它有一個(gè)安全問(wèn)題:B 系統(tǒng)開(kāi)放的接口不僅可以被 A 系統(tǒng)調(diào)用,還可以被其它任何人調(diào)用,甚至別人可以本地跑一個(gè) for 循環(huán)調(diào)用這個(gè)接口,為自己無(wú)限充值金額
2. 方案升級(jí):增加 secretKey 校驗(yàn)
為防止 B 系統(tǒng)開(kāi)放的接口被陌生人任意調(diào)用,我們?cè)黾右粋€(gè) secretKey 參數(shù)
@PostMapping("/addMoney") public String addMoney(Long userId, Long money, String secretKey) { // 校驗(yàn) secretKey if (!check(secretKey)) { throw new RuntimeException("無(wú)效 secretKey,無(wú)法響應(yīng)請(qǐng)求"); } // TODO 處理業(yè)務(wù)... return "ADD SUCCESS"; }
由于 A 系統(tǒng)是我們 “自己人”,所以它可以拿著 secretKey 進(jìn)行合法請(qǐng)求:
@PostMapping("/join") public String join() { // 參加完活動(dòng)后,發(fā)送余額 Map<String, Object> params = new HashMap<>(); params.put("userId", userId); params.put("money", money); params.put("secretKey", "×××××××××××"); String url = "http://localhost:8079/sign/addMoney"; String result = HttpUtil.post(url, params); return "join"; }
現(xiàn)在,即使 B 系統(tǒng)的接口被暴露了,也不會(huì)被陌生人任意調(diào)用了,安全性得到了一定的保證,但是仍然存在一些問(wèn)題:
- 如果請(qǐng)求被抓包,secretKey 就會(huì)泄露,因?yàn)槊看握?qǐng)求都在 url 中明文傳輸了 secretKey 參數(shù)。
- 如果請(qǐng)求被抓包,請(qǐng)求的其它參數(shù)就可以被任意修改,例如可以將 money 參數(shù)修改為 9999999,B系統(tǒng)無(wú)法確定參數(shù)是否被修改過(guò)。
3.方案再升級(jí):使用摘要算法生成參數(shù)簽名
首先,在 A 系統(tǒng)不要直接發(fā)起請(qǐng)求,而是先計(jì)算一個(gè) sign 參數(shù):
@PostMapping("/join") public String join() { // 參加完活動(dòng)后,發(fā)送余額 Long userId = 1L; Long money = 100L; String secretKey = "×××××××××××"; // 計(jì)算 sign String sign = md5("money=" + money + "&userId=" + userId + "&key=" + secretKey); Map<String, Object> params = new HashMap<>(); params.put("userId", userId); params.put("money", money); params.put("sign", sign); String url = "http://localhost:8079/sign/addMoney"; String result = HttpUtil.post(url, params); return "join"; }
注意:此處計(jì)算簽名時(shí),需要將所有參數(shù)按照字典順序依次排列(key除外,掛在最后面)
然后在 B 系統(tǒng)接收請(qǐng)求時(shí),使用同樣的算法、同樣的秘鑰,生成 sign 字符串,與參數(shù)中 sign 值進(jìn)行比較:
@PostMapping("/addMoney") public String addMoney(Long userId, Long money, String sign) { // 在 B 系統(tǒng),使用同樣的算法、同樣的密鑰,計(jì)算出 sign2,與傳入的 sign 進(jìn)行比對(duì) String sign2 = md5("money=" + money + "&userId=" + userId + "&key=" + secretKey); if (!sign2.equals(sign)) { return "無(wú)效 sign,無(wú)法響應(yīng)請(qǐng)求"; } // TODO 處理業(yè)務(wù)... return "ADD SUCCESS"; }
因?yàn)?sign 的值是由 userId、money、secretKey 三個(gè)參數(shù)共同決定的,所以只要有一個(gè)參數(shù)不一致,就會(huì)造成最終生成 sign 也是不一致的,所以,根據(jù)比對(duì)結(jié)果:
- 如果 sign 一致,說(shuō)明這是個(gè)合法請(qǐng)求。
- 如果 sign 不一致,說(shuō)明發(fā)起請(qǐng)求的客戶端秘鑰不正確,或者請(qǐng)求參數(shù)被篡改過(guò),是個(gè)不合法請(qǐng)求。
此方案優(yōu)點(diǎn):
- 不在 url 中直接傳遞 secretKey 參數(shù)了,避免了泄露風(fēng)險(xiǎn)。
- 由于 sign 參數(shù)的限制,請(qǐng)求中的參數(shù)也不可被篡改,B 系統(tǒng)可放心的使用這些參數(shù)。
此方案仍然存在以下缺陷:
- 被抓包后,請(qǐng)求可以被無(wú)限重放,B 系統(tǒng)無(wú)法判斷請(qǐng)求是真正來(lái)自于 A 系統(tǒng)發(fā)出的,還是被抓包后重放的。
@PostMapping("/join") public String join() { // 參加完活動(dòng)后,發(fā)送余額 Long userId = 1L; Long money = 100L; String nonce = SaFoxUtil.getRandomString(32); // 隨機(jī)32位字符串 String secretKey = "×××××××××××"; // 計(jì)算 sign String sign = md5("money=" + money + "&nonce=" + nonce + "&userId=" + userId + "&key=" + secretKey); Map<String, Object> params = new HashMap<>(); params.put("userId", userId); params.put("money", money); params.put("nonce", nonce); params.put("sign", sign); String url = "http://localhost:8079/sign/addMoney"; String result = HttpUtil.post(url, params); return "join"; }
4. 方案再再升級(jí):追加 nonce 隨機(jī)字符串
首先,在 A 系統(tǒng)發(fā)起調(diào)用前,追加一個(gè) nonce 參數(shù),一起參與到簽名中:
public String join() { // 參加完活動(dòng)后,發(fā)送余額 Long userId = 1L; Long money = 100L; String nonce = SaFoxUtil.getRandomString(32); // 隨機(jī)32位字符串 String secretKey = "×××××××××××"; // 計(jì)算 sign String sign = md5("money=" + money + "&nonce=" + nonce + "&userId=" + userId + "&key=" + secretKey); Map<String, Object> params = new HashMap<>(); params.put("userId", userId); params.put("money", money); params.put("nonce", nonce); params.put("sign", sign); String url = "http://localhost:8079/sign/addMoney"; String result = HttpUtil.post(url, params); return "join"; }
然后在 B 系統(tǒng)接收請(qǐng)求時(shí),也把 nonce 參數(shù)加進(jìn)去生成 sign 字符串,進(jìn)行比較:
public String addMoney(Long userId, Long money, String nonce,String sign) { // 檢查此 nonce 是否已被使用過(guò)了 if (Objects.nonNull(CacheUtil.get("nonce_" + nonce))) { return "此 nonce 已被使用過(guò)了,請(qǐng)求無(wú)效"; } // 在 B 系統(tǒng),使用同樣的算法、同樣的密鑰,計(jì)算出 sign2,與傳入的 sign 進(jìn)行比對(duì) String sign2 = md5("money=" + money + "&nonce=" + nonce + "&userId=" + userId + "&key=" + secretKey); if (!sign2.equals(sign)) { return "無(wú)效 sign,無(wú)法響應(yīng)請(qǐng)求"; } // 存入緩存 CacheUtil.set("nonce_" + nonce, "1"); // TODO 處理業(yè)務(wù)... return "ADD SUCCESS"; }
代碼分析:
- 為方便理解,我們先看第 3 步:此處在校驗(yàn)簽名成功后,將 nonce 隨機(jī)字符串記入緩存中。
- 再看第 1 步:每次請(qǐng)求進(jìn)來(lái),先查看一下緩存中是否已經(jīng)記錄了這個(gè)隨機(jī)字符串,如果是,則立即返回:無(wú)效請(qǐng)求。
這兩步的組合,保證了一個(gè) nonce 隨機(jī)字符串只能被使用一次,如果請(qǐng)求被抓包后重放,是無(wú)法通過(guò) nonce 校驗(yàn)的。
至此,問(wèn)題似乎已被解決了 …… 嗎?
別急,我們還有一個(gè)問(wèn)題沒(méi)有考慮:這個(gè) nonce 在字符串在緩存應(yīng)該被保存多久呢?
- 保存 15 分鐘?那抓包的人只需要等待 15 分鐘,你的 nonce 記錄在緩存中消失,請(qǐng)求就可以被重放了。
- 那保存 24 小時(shí)?保存一周?保存半個(gè)月?好像無(wú)論保存多久,都無(wú)法從根本上解決這個(gè)問(wèn)題。
你可能會(huì)想到,那我永久保存吧。這樣確實(shí)能解決問(wèn)題,但顯然服務(wù)器承載不了這么做,即使再微小的數(shù)據(jù)量,在時(shí)間的累加下,也總一天會(huì)超出服務(wù)器能夠承載的上限。
5. 方案再再再升級(jí):追加 timestamp 時(shí)間戳
我們可以再追加一個(gè) timestamp 時(shí)間戳參數(shù),將請(qǐng)求的有效性限定在一個(gè)有限時(shí)間范圍內(nèi),例如 15分鐘。
首先,在 A 系統(tǒng)追加 timestamp 參數(shù):
public String join() { // 參加完活動(dòng)后,發(fā)送余額 Long userId = 1L; Long money = 100L; Long timestamp = System.currentTimeMillis(); String nonce = SaFoxUtil.getRandomString(32); // 隨機(jī)32位字符串 String secretKey = "×××××××××××"; // 計(jì)算 sign String sign = md5("money=" + money + "&nonce=" + nonce + "×tamp=" + timestamp + "&userId=" + userId + "&key=" + secretKey); Map<String, Object> params = new HashMap<>(); params.put("userId", userId); params.put("money", money); params.put("nonce", nonce); params.put("timestamp", timestamp); params.put("sign", sign); String url = "http://localhost:8079/sign/addMoney"; String result = HttpUtil.post(url, params); return "join"; }
在 B 系統(tǒng)檢測(cè)這個(gè) timestamp 是否超出了允許的范圍
public String addMoney(Long userId, Long money, Long timestamp, String nonce,String sign) { // 1、檢查 timestamp 是否超出允許的范圍(此處假定最大允許15分鐘差距) long timestampDisparity = System.currentTimeMillis() - timestamp; // 實(shí)際的時(shí)間差 if(timestampDisparity > 1000 * 60 * 15) { return "timestamp 時(shí)間差超出允許的范圍,請(qǐng)求無(wú)效"; } // 檢查此 nonce 是否已被使用過(guò)了 if (Objects.nonNull(CacheUtil.get("nonce_" + nonce))) { return "此 nonce 已被使用過(guò)了,請(qǐng)求無(wú)效"; } // 在 B 系統(tǒng),使用同樣的算法、同樣的密鑰,計(jì)算出 sign2,與傳入的 sign 進(jìn)行比對(duì) String sign2 = md5("money=" + money + "&nonce=" + nonce + "&userId=" + userId + "&key=" + secretKey); if (!sign2.equals(sign)) { return "無(wú)效 sign,無(wú)法響應(yīng)請(qǐng)求"; } // 將 nonce 記入緩存,ttl 有效期和 allowDisparity 允許時(shí)間差一致 CacheUtil.set("nonce_" + nonce, "1", 1000 * 60 * 15); // TODO 處理業(yè)務(wù)... return "ADD SUCCESS"; }
至此,抓包者:
- 如果在 15 分鐘內(nèi)重放攻擊,nonce 參數(shù)不答應(yīng):緩存中可以查出 nonce 值,直接拒絕響應(yīng)請(qǐng)求。
- 如果在 15 分鐘后重放攻擊,timestamp 參數(shù)不答應(yīng):超出了允許的 timestamp 時(shí)間差,直接拒絕響應(yīng)請(qǐng)求。
6. 服務(wù)器的時(shí)鐘差異造成安全問(wèn)題
以上的代碼,均假設(shè) A 系統(tǒng)服務(wù)器與 B 系統(tǒng)服務(wù)器的時(shí)鐘一致,才可以正常完成安全校驗(yàn),但在實(shí)際的開(kāi)發(fā)場(chǎng)景中,有些服務(wù)器會(huì)存在時(shí)鐘不準(zhǔn)確的問(wèn)題。
假設(shè) A 服務(wù)器與 B 服務(wù)器的時(shí)鐘差異為 10 分鐘,即:在 A 服務(wù)器為 8:00 的時(shí)候,B 服務(wù)器為 7:50。
- A 系統(tǒng)發(fā)起請(qǐng)求,其生成的時(shí)間戳也是代表 8:00。
- B 系統(tǒng)接受到請(qǐng)求后,完成業(yè)務(wù)處理,此時(shí) nonce 的 ttl 為 15分鐘,到期時(shí)間為 7:50 + 15分 = 8:05。
- 8.05 后,nonce 緩存消失,抓包者重放請(qǐng)求攻擊:
- timestamp 校驗(yàn)通過(guò):因?yàn)闀r(shí)間戳差距僅有 8.05 - 8.00 = 5分鐘,小于 15 分鐘,校驗(yàn)通過(guò)。
- -nonce 校驗(yàn)通過(guò):因?yàn)榇藭r(shí) nonce 緩存已經(jīng)消失,可以通過(guò)校驗(yàn)。
- sign 校驗(yàn)通過(guò):因?yàn)檫@本來(lái)就是由 A 系統(tǒng)構(gòu)建的一個(gè)合法簽名。
攻擊完成。
要解決上述問(wèn)題,有兩種方案:
- 方案一:修改服務(wù)器時(shí)鐘,使兩個(gè)服務(wù)器時(shí)鐘保持一致。
- 方案二:在代碼層面兼容時(shí)鐘不一致的場(chǎng)景。
要采用方案一的同學(xué)可自行搜索一下同步時(shí)鐘的方法,在此暫不贅述,此處詳細(xì)闡述一下方案二。
我們只需簡(jiǎn)單修改一下,B 系統(tǒng)校驗(yàn)參數(shù)的代碼即可:
public String addMoney(Long userId, Long money, Long timestamp, String nonce,String sign) { // 1、檢查 timestamp 是否超出允許的范圍 (重點(diǎn)一:此處需要取絕對(duì)值) long timestampDisparity = Math.abs(System.currentTimeMillis() - timestamp); if(timestampDisparity > 1000 * 60 * 15) { return "timestamp 時(shí)間差超出允許的范圍,請(qǐng)求無(wú)效"; } // 檢查此 nonce 是否已被使用過(guò)了 if (Objects.nonNull(CacheUtil.get("nonce_" + nonce))) { return "此 nonce 已被使用過(guò)了,請(qǐng)求無(wú)效"; } // 在 B 系統(tǒng),使用同樣的算法、同樣的密鑰,計(jì)算出 sign2,與傳入的 sign 進(jìn)行比對(duì) String sign2 = md5("money=" + money + "&nonce=" + nonce + "&userId=" + userId + "&key=" + secretKey); if (!sign2.equals(sign)) { return "無(wú)效 sign,無(wú)法響應(yīng)請(qǐng)求"; } // 將 nonce 記入緩存,防止重復(fù)使用(重點(diǎn)二:此處需要將 ttl 設(shè)定為允許 timestamp 時(shí)間差的值 x 2 ) CacheUtil.set("nonce_" + nonce, "1", (1000 * 60 * 15) * 2; // TODO 處理業(yè)務(wù)... return "ADD SUCCESS"; }
7. 使用 Sa-Token 框架完成 API 參數(shù)簽名
接下來(lái)步入正題,使用 Sa-Token 內(nèi)置的 sign 模塊,方便的完成 API 簽名創(chuàng)建、校驗(yàn)等步驟:
- 不限制請(qǐng)求的參數(shù)數(shù)量,方便組織業(yè)務(wù)需求代碼。
- 自動(dòng)補(bǔ)全 nonce、timestamp 參數(shù),省時(shí)省力。
- 自動(dòng)構(gòu)建簽名,并序列化參數(shù)為字符串。
- 一句代碼完成 nonce、timestamp、sign 的校驗(yàn),防偽造請(qǐng)求調(diào)用、防參數(shù)篡改、防重放攻擊。
7.1 引入依賴
api-sign 模塊已內(nèi)嵌到核心包,只需要引入 sa-token 本身依賴即可:(請(qǐng)求發(fā)起端和接收端都需要引入)
<dependency> <groupId>cn.dev33</groupId> <artifactId>sa-token-spring-boot-starter</artifactId> <version>1.35.0.RC</version> </dependency>
7.2 配置密鑰
請(qǐng)求發(fā)起端和接收端需要配置一個(gè)相同的秘鑰,在 application.yml 中配置:
sa-token: sign: # API 接口簽名秘鑰 (隨便亂摁幾個(gè)字母即可) secret-key: kQwIOrYvnXmSDkwEiFngrKidMcdrgKor
7.3 請(qǐng)求發(fā)起端構(gòu)建簽名
public String join() { // 參加完活動(dòng)后,發(fā)送余額 Long userId = 1L; Long money = 100L; Map<String, Object> params = new HashMap<>(); params.put("userId", userId); params.put("money", money); SaSignUtil.addSignParamsAndJoin(params); String url = "http://localhost:8079/sign/addMoney"; return HttpUtil.post(url, params); }
7.4 請(qǐng)求接受端校驗(yàn)簽名
public String addMoney(Long userId, Long money) { // 1、校驗(yàn)請(qǐng)求中的簽名 SaSignUtil.checkRequest(SaHolder.getRequest()); // 2、校驗(yàn)通過(guò),處理業(yè)務(wù) System.out.println("userId=" + userId); System.out.println("money=" + money); return "ADD SUCCESS"; }
如上代碼便可簡(jiǎn)單方便的完成 API 接口參數(shù)簽名校驗(yàn),當(dāng)請(qǐng)求端的秘鑰不對(duì),或者請(qǐng)求參數(shù)被篡改、請(qǐng)求被重放時(shí),均無(wú)法通過(guò) SaSignUtil.checkRequest 校驗(yàn)
7.5 原理分析
7.5.1 構(gòu)建簽名
SaSignUtil#addSignParamsAndJoin(params);
:
public static String addSignParamsAndJoin(Map<String, Object> paramsMap) { return SaManager.getSaSignTemplate().addSignParamsAndJoin(paramsMap); }
會(huì)調(diào)用 SaSignTemplate
類中的方法
SaSignTemplate#addSignParamsAndJoin() 方法
public String addSignParamsAndJoin(Map<String, Object> paramsMap) { // 1.添加參數(shù):timestamp、nonce、sign paramsMap = this.addSignParams(paramsMap); // 2.將 map 使用 & 轉(zhuǎn)化為String return this.joinParams(paramsMap); }
這個(gè)方法有兩個(gè)邏輯:
- 添加參數(shù):timestamp、nonce、sign
- 將 map 使用 & 轉(zhuǎn)化為String
SaSignTemplate#addSignParams() 方法
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() 方法
:生成簽名
public String createSign(Map<String, ?> paramsMap) { String secretKey = this.getSecretKey(); SaSignException.throwByNull(secretKey, "參與參數(shù)簽名的秘鑰不可為空", 12201); if (((Map)paramsMap).containsKey(sign)) { paramsMap = new TreeMap((Map)paramsMap); ((Map)paramsMap).remove(sign); } // 按照數(shù)據(jù)字典進(jìn)行排序,并將 map 使用 & 轉(zhuǎn)化為String String paramsStr = this.joinParamsDictSort((Map)paramsMap); String fullStr = paramsStr + "&" + key + "=" + secretKey; // md5 return this.abstractStr(fullStr); } public String abstractStr(String fullStr) { return SaSecureUtil.md5(fullStr); }
這個(gè)方法有兩個(gè)邏輯:
- 按照數(shù)據(jù)字典進(jìn)行排序,并將 map 使用 & 轉(zhuǎn)化為String
- 使用 md5 摘要算法
7.5.2 驗(yàn)證簽名
SaSignUtil.checkRequest(SaHolder.getRequest());
:
public static void checkRequest(SaRequest request) { SaManager.getSaSignTemplate().checkRequest(request); }
還是會(huì)調(diào)用 SaSignTemplate
類中的方法
SaSignTemplate#checkParamMap() 方法
:校驗(yàn)請(qǐng)求參數(shù)
public void checkRequest(SaRequest request) { this.checkParamMap(request.getParamMap()); } public void checkParamMap(Map<String, String> paramMap) { String timestampValue = (String)paramMap.get(timestamp); String nonceValue = (String)paramMap.get(nonce); String signValue = (String)paramMap.get(sign); // 1.校驗(yàn)時(shí)間戳 this.checkTimestamp(Long.parseLong(timestampValue)); // 2.校驗(yàn)隨機(jī)數(shù) if (this.getSignConfigOrGlobal().getIsCheckNonce()) { this.checkNonce(nonceValue); } // 3.校驗(yàn)簽名 this.checkSign(paramMap, signValue); }
這個(gè)方法有三個(gè)邏輯:
- 校驗(yàn)時(shí)間戳:判斷是否在時(shí)間差范圍內(nèi)
- 校驗(yàn)隨機(jī)數(shù):判斷此隨機(jī)數(shù)是否已使用
- 校驗(yàn)簽名:判斷原簽名和現(xiàn)在生成的簽名是否一致
SaSignTemplate#checkNonce() 方法:
校驗(yàn)隨機(jī)數(shù)
public void checkNonce(String nonce) { if (SaFoxUtil.isEmpty(nonce)) { throw new SaSignException("nonce 為空,無(wú)效"); } else { String key = this.splicingNonceSaveKey(nonce); if (SaManager.getSaTokenDao().get(key) != null) { throw new SaSignException("此 nonce 已被使用過(guò),不可重復(fù)使用:" + nonce); } else { SaManager.getSaTokenDao().set(key, nonce, this.getSignConfigOrGlobal().getSaveNonceExpire() * 2L + 2L); } } }
SaToken 存儲(chǔ)
SaTokenDao
是存儲(chǔ)接口,默認(rèn)實(shí)現(xiàn)是用的是 SaTokenDaoDefaultImpl
。SaTokenDaoDefaultImpl
存儲(chǔ)數(shù)據(jù),主要是通過(guò) ConcurrentHashMap
存放在本地內(nèi)存中。
SaManager#getSaTokenDao() 方法:
public static SaTokenDao getSaTokenDao() { if (saTokenDao == null) { Class var0 = SaManager.class; synchronized(SaManager.class) { if (saTokenDao == null) { setSaTokenDaoMethod(new SaTokenDaoDefaultImpl()); } } } return saTokenDao; }
SaTokenDaoDefaultImpl
:
public class SaTokenDaoDefaultImpl implements SaTokenDao { // 數(shù)據(jù)集合 public Map<String, Object> dataMap = new ConcurrentHashMap(); // 過(guò)期時(shí)間集合 (單位: 毫秒) , 記錄所有key的到期時(shí)間 [注意不是剩余存活時(shí)間] public Map<String, Long> expireMap = new ConcurrentHashMap(); public Thread refreshThread; public volatile boolean refreshFlag; public SaTokenDaoDefaultImpl() { // 定時(shí)清理過(guò)期數(shù)據(jù) this.initRefreshThread(); } public String get(String key) { this.clearKeyByTimeout(key); return (String)this.dataMap.get(key); } public void set(String key, String value, long timeout) { if (timeout != 0L && timeout > -2L) { this.dataMap.put(key, value); this.expireMap.put(key, timeout == -1L ? -1L : System.currentTimeMillis() + timeout * 1000L); } } public void initRefreshThread() { if (SaManager.getConfig().getDataRefreshPeriod() > 0) { this.refreshFlag = true; this.refreshThread = new Thread(() -> { while(true) { try { try { if (!this.refreshFlag) { return; } this.refreshDataMap(); } catch (Exception var2) { var2.printStackTrace(); } int dataRefreshPeriod = SaManager.getConfig().getDataRefreshPeriod(); if (dataRefreshPeriod <= 0) { dataRefreshPeriod = 1; } Thread.sleep((long)dataRefreshPeriod * 1000L); } catch (Exception var3) { var3.printStackTrace(); } } }); this.refreshThread.start(); } } }
如果僅僅存放在本地內(nèi)存中,涉及到多個(gè)項(xiàng)目,可能數(shù)據(jù)無(wú)法共享。
引入倉(cāng)庫(kù) sa-token-dao-redis-jackson
<!-- Sa-Token 整合 Redis (使用 jackson 序列化方式) --> <dependency> <groupId>cn.dev33</groupId> <artifactId>sa-token-redis-jackson</artifactId> <version>1.35.0.RC</version> </dependency> <!-- 提供Redis連接池 --> <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-pool2</artifactId> </dependency>
SaTokenDaoRedisJackson
使用 Redis 作為存儲(chǔ)數(shù)據(jù)的地方
SaBeanInject#setSaTokenDao
,SaBeanInject
是自動(dòng)配置的。當(dāng)系統(tǒng)中存在 SaTokenDao
的 Bean 實(shí)例,則設(shè)置SaTokenDao
實(shí)例
public class SaBeanInject { @Autowired( required = false ) public void setSaTokenDao(SaTokenDao saTokenDao) { SaManager.setSaTokenDao(saTokenDao); } }
參考:
【開(kāi)源項(xiàng)目】使用Sa-Token框架完成API參數(shù)簽名
到此這篇關(guān)于SpringBoot整合Sa-Token 快速實(shí)現(xiàn) API 接口簽名安全校驗(yàn)的文章就介紹到這了,更多相關(guān)SpringBoot API 接口簽名安全校驗(yàn)內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
- SpringBoot3整合SpringDoc OpenAPI生成接口文檔的詳細(xì)過(guò)程
- 關(guān)于springboot忽略接口,參數(shù)注解的使用ApiIgnore
- Springboot+Redis實(shí)現(xiàn)API接口防刷限流的項(xiàng)目實(shí)踐
- SpringBoot?快速實(shí)現(xiàn)?api?接口加解密功能
- 詳解Springboot快速搭建跨域API接口的步驟(idea社區(qū)版2023.1.4+apache-maven-3.9.3-bin)
- SpringBoot如何根據(jù)目錄結(jié)構(gòu)生成API接口前綴
- SpringBoot可視化接口開(kāi)發(fā)工具magic-api的簡(jiǎn)單使用教程
- SpringBoot實(shí)現(xiàn)API接口的完整代碼
- springboot接入方式對(duì)接股票數(shù)據(jù)源API接口的操作方法
相關(guān)文章
SpringMVC4+MyBatis+SQL Server2014實(shí)現(xiàn)數(shù)據(jù)庫(kù)讀寫分離
這篇文章主要介紹了SpringMVC4+MyBatis+SQL Server2014實(shí)現(xiàn)讀寫分離,需要的朋友可以參考下2017-04-04java hasNextInt判斷是否為數(shù)字的方法
今天小編就為大家分享一篇java hasNextInt判斷是否為數(shù)字的方法,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2018-07-07關(guān)于Java中String創(chuàng)建的字符串對(duì)象內(nèi)存分配測(cè)試問(wèn)題
這篇文章主要介紹了Java中String創(chuàng)建的字符串對(duì)象內(nèi)存分配測(cè)試,給大家詳細(xì)介紹了在創(chuàng)建String對(duì)象的兩種常用方法比較,通過(guò)示例代碼給大家介紹的非常詳細(xì),需要的朋友可以參考下2021-07-07java向es中寫入數(shù)據(jù)報(bào)錯(cuò)org.elasticsearch.action.ActionReque問(wèn)題
這篇文章主要介紹了java向es中寫入數(shù)據(jù)報(bào)錯(cuò)org.elasticsearch.action.ActionReque問(wèn)題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-11-11JavaWeb開(kāi)發(fā)入門第二篇Tomcat服務(wù)器配置講解
JavaWeb開(kāi)發(fā)入門第二篇主要介紹了Tomcat服務(wù)器配置的方法教大家如何使用Tomcat服務(wù)器,感興趣的小伙伴們可以參考一下2016-04-04Java日常練習(xí)題,每天進(jìn)步一點(diǎn)點(diǎn)(24)
下面小編就為大家?guī)?lái)一篇Java基礎(chǔ)的幾道練習(xí)題(分享)。小編覺(jué)得挺不錯(cuò)的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧,希望可以幫到你2021-07-07