Spring Boot實(shí)現(xiàn)接口簽名驗(yàn)證的過(guò)程
項(xiàng)目場(chǎng)景:
開(kāi)放接口是指不需要登錄憑證就允許被第三方系統(tǒng)調(diào)用的接口。為了防止開(kāi)放接口被惡意調(diào)用,開(kāi)放接口一般都需要驗(yàn)簽才能被調(diào)用。
在Spring Boot中實(shí)現(xiàn)接口校驗(yàn)簽名通常是為了保證接口請(qǐng)求的安全性和數(shù)據(jù)的完整性。簽名校驗(yàn)通常涉及對(duì)請(qǐng)求參數(shù)的簽名計(jì)算和驗(yàn)證,以確保請(qǐng)求是由可信的發(fā)送方發(fā)送,并且在傳輸過(guò)程中沒(méi)有被篡改。下面,我將詳細(xì)介紹如何在Spring Boot應(yīng)用中實(shí)現(xiàn)接口校驗(yàn)簽名的過(guò)程。
解決方案:
1.配置簽名密鑰
簽名密鑰是用于生成和驗(yàn)證簽名的秘密信息,生成規(guī)則自己定義,需要把生成的密鑰提供給第三方。
比如:
appId:360aa3a3ba074da6a7bb17ae55e72d26
appSecret:81343DC5-6E80-483A-A427-E3DF5FA4E5F3
/** * 生成應(yīng)用id和密鑰 */ @RequestMapping(value = "/getSecret", method = RequestMethod.GET) public Map<String, String> getSecret() { //生成應(yīng)用id和密鑰提供給第三方使用,具體生成規(guī)則自己定 String appId = UUID.randomUUID().toString().replace("-", "").toLowerCase(); String appSecret = UUID.randomUUID().toString().toUpperCase(); System.out.println("appId:"+appId); System.out.println("appSecret:"+appSecret); Map<String, String> map = new HashMap<>(); map.put("appId", appId); map.put("appSecret", appSecret); return map; }
2.定義簽名算法
一個(gè)用于生成簽名,另一個(gè)用于驗(yàn)證簽名。生成簽名的方法通常將請(qǐng)求參數(shù)按照特定規(guī)則計(jì)算出一個(gè)簽名值。常見(jiàn)的簽名算法有HMAC-SHA1、HMAC-SHA256等。
驗(yàn)證簽名的方法則是對(duì)接收到的請(qǐng)求參數(shù)進(jìn)行同樣的處理,并計(jì)算出一個(gè)簽名值,然后與請(qǐng)求中攜帶的簽名值進(jìn)行比對(duì)。
package com.test.utils; import javax.crypto.Mac; import javax.crypto.spec.SecretKeySpec; import java.io.UnsupportedEncodingException; import java.security.InvalidKeyException; import java.security.NoSuchAlgorithmException; import java.util.Base64; public class Signature { /** * 獲取簽名 * @param secretKey 密鑰 * @param data 需要簽名的數(shù)據(jù) * @return 簽名 */ public static String signWithHmacSha1(String secretKey, String data) { try { SecretKeySpec signingKey = new SecretKeySpec(secretKey.getBytes("UTF-8"), "HmacSHA1"); Mac mac = Mac.getInstance("HmacSHA1"); mac.init(signingKey); return Base64.getEncoder().encodeToString(mac.doFinal(data.getBytes("UTF-8"))); } catch (NoSuchAlgorithmException | InvalidKeyException | UnsupportedEncodingException e) { e.printStackTrace(); } return null; } /** * 驗(yàn)證簽名 * @param secretKey 密鑰 * @param data 需要簽名的數(shù)據(jù) * @param hmac 已經(jīng)簽名的數(shù)據(jù) * @return true:簽名一致 */ public static boolean verify(String secretKey, String data, String hmac) { String calculatedHmac = signWithHmacSha1(secretKey, data); return calculatedHmac.equals(hmac); } }
3.攔截器或過(guò)濾器實(shí)現(xiàn)
使用Spring的攔截器(Interceptor)或過(guò)濾器(Filter)來(lái)實(shí)現(xiàn)對(duì)接口請(qǐng)求的簽名校驗(yàn)。在攔截器或過(guò)濾器中,你可以獲取到請(qǐng)求的參數(shù),并調(diào)用簽名驗(yàn)證方法來(lái)校驗(yàn)簽名的有效性。
package com.test.aop; import com.test.utils.Signature; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; import org.springframework.stereotype.Component; import org.springframework.web.servlet.HandlerInterceptor; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; /** * 簽名攔截器 */ @Component @Slf4j public class SignInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { response.setCharacterEncoding("UTF-8"); response.setContentType("text/html;charset=UTF-8"); //分配的應(yīng)用id String appId = request.getHeader("appId"); //時(shí)間戳 String timestampStr = request.getHeader("timestamp"); //簽名 String signature = request.getHeader("signature"); if(StringUtils.isBlank(appId) || StringUtils.isBlank(timestampStr) || StringUtils.isBlank(signature)){ response.setStatus(500); response.getWriter().println("參數(shù)錯(cuò)誤!"); return false; } //這個(gè)密鑰實(shí)際應(yīng)該根據(jù)appId到數(shù)據(jù)庫(kù)里查出來(lái) String appSecret = "81343DC5-6E80-483A-A427-E3DF5FA4E5F3"; //如果密鑰沒(méi)查到 // if(flag){ // response.setStatus(500); // response.getWriter().println("密鑰不存在!"); // } //拼接數(shù)據(jù) String origin = appId + "\n" + appSecret + "\n" + timestampStr; if(!Signature.verify(appSecret, origin, signature)){ response.setStatus(500); response.getWriter().println("簽名錯(cuò)誤!"); return false; } //業(yè)務(wù)的時(shí)間戳 long timestamp = Long.parseLong(timestampStr); //當(dāng)前時(shí)間戳 long currentTimestamp = System.currentTimeMillis() / 1000; //10分鐘內(nèi)有效 long timeDifference = 10 * 60; if(Math.abs(timestamp - currentTimestamp) > timeDifference){ response.setStatus(500); response.getWriter().println("簽名過(guò)期!"); return false; } //放行 return true; } }
配置攔截器
package com.test.config; import com.test.aop.SignInterceptor; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport; import javax.annotation.Resource; import java.util.ArrayList; import java.util.List; /** * WebMvc配置 */ @Configuration public class WebMvcConfig extends WebMvcConfigurationSupport { @Resource private SignInterceptor signInterceptor; @Override public void addInterceptors(InterceptorRegistry registry) { //不攔截的地址 List<String> excludedList = new ArrayList<>(); //swagger地址 excludedList.add("/swagger-ui.html"); excludedList.add("/swagger-ui.html/**"); excludedList.add("/webjars/**"); excludedList.add("/swagger/**"); excludedList.add("/doc.html"); excludedList.add("/doc.html/**"); excludedList.add("/swagger-resources/**"); excludedList.add("/v2/**"); excludedList.add("/favicon.ico"); //生成應(yīng)用id和密鑰接口不攔截 excludedList.add("/getSecret"); registry.addInterceptor(signInterceptor) .addPathPatterns("/**")//攔截所有請(qǐng)求 .excludePathPatterns(excludedList);//排除的請(qǐng)求 super.addInterceptors(registry); } }
4.測(cè)試接口
controller定義一個(gè)測(cè)試接口
/** * 測(cè)試簽名 */ @RequestMapping(value = "/sign", method = RequestMethod.GET) public String sign() { return "success"; }
模擬第三方調(diào)用:
我們需要把接口請(qǐng)求頭所需參數(shù)和簽名的方法告知第三方。
接口請(qǐng)求頭參數(shù)如下:
參數(shù)名稱 | 中文 | 參數(shù)值 |
---|---|---|
appId | 應(yīng)用id | 360aa3a3ba074da6a7bb17ae55e72d26 |
timestamp | 當(dāng)前時(shí)間戳,精確到秒 | 1713838208 |
signature | 簽名 | bjvXebFiHi2+I93BNs+8+Tl2I7k= |
簽名方法:
import javax.crypto.Mac; import javax.crypto.spec.SecretKeySpec; import java.io.UnsupportedEncodingException; import java.security.InvalidKeyException; import java.security.NoSuchAlgorithmException; import java.util.Base64; public class Signature { /** * 獲取簽名 * @param secretKey 密鑰 * @param data 需要簽名的數(shù)據(jù) * @return 簽名 */ public static String signWithHmacSha1(String secretKey, String data) { try { SecretKeySpec signingKey = new SecretKeySpec(secretKey.getBytes("UTF-8"), "HmacSHA1"); Mac mac = Mac.getInstance("HmacSHA1"); mac.init(signingKey); return Base64.getEncoder().encodeToString(mac.doFinal(data.getBytes("UTF-8"))); } catch (NoSuchAlgorithmException | InvalidKeyException | UnsupportedEncodingException e) { e.printStackTrace(); } return null; } }
生成簽名示例:
public static void main(String[] args) { //這里的應(yīng)用id和密鑰已實(shí)際分配的為準(zhǔn) String appId = "360aa3a3ba074da6a7bb17ae55e72d26"; String appSecret = "81343DC5-6E80-483A-A427-E3DF5FA4E5F3"; //當(dāng)前時(shí)間戳,精確到秒,示例:1713838208 long timestamp = System.currentTimeMillis() / 1000; //拼接數(shù)據(jù):appId、appSecret、timestamp String origin = appId + "\n" + appSecret + "\n" + timestamp; String signature = Signature.signWithHmacSha1(appSecret, origin); //需要加到請(qǐng)求頭的參數(shù) System.out.println("appId:"+appId); System.out.println("timestamp:"+timestamp); System.out.println("signature:"+signature); }
當(dāng)?shù)谌街澜涌谡?qǐng)求頭所需參數(shù)和簽名的方法后,就可以調(diào)用接口了
curl調(diào)用:
curl -X GET \ http://localhost:8080/testservice/test/sign \ -H 'appId: 360aa3a3ba074da6a7bb17ae55e72d26' \ -H 'signature: bjvXebFiHi2+I93BNs+8+Tl2I7k=' \ -H 'timestamp: 1713843128'
總結(jié) 這里簽名由appId + "\n" + appSecret + "\n" + timestamp,生成簽名字串時(shí)間戳用于保證簽名的有效性,即使簽名被盜用,也只能在有效時(shí)間內(nèi)使用appId、appSecret自己定義生成規(guī)則,保存到數(shù)據(jù)庫(kù)中
源碼百度網(wǎng)盤(pán)下載地址:
鏈接: https://pan.baidu.com/s/1dhtbspb3AC3M_sWBG2D-Xg?pwd=k57m
提取碼: k57m
到此這篇關(guān)于Spring Boot實(shí)現(xiàn)接口簽名驗(yàn)證的文章就介紹到這了,更多相關(guān)Spring Boot接口簽名驗(yàn)證內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
java serialVersionUID解決序列化類版本不一致問(wèn)題面試精講
這篇文章主要為大家介紹了serialVersionUID解決序列化類版本不一致問(wèn)題的面試精講,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-10-10如何在攔截器中獲取url路徑里面@PathVariable的參數(shù)值
這篇文章主要介紹了如何在攔截器中獲取url路徑里面@PathVariable的參數(shù)值,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-08-08