一,為什么要給接口做簽名驗證?
1,app客戶端在與服務端通信時,通常都是以接口的形式實現,
這種形式的安全方面有可能出現以下問題:
被非法訪問(例如:發短信的接口通常會被利用來垃圾短信)
被重復訪問 (例如:在提交訂單時多點了幾次提交按鈕)
而客戶端存在的弱點是:對接口站的地址不能輕易修改,
所以我們需要針對從app到接口的接口做簽名驗證,
接口不能隨便app之外的應用訪問
2,要注意的地方:
我們給app分配一個app_id和一個app_secret
app對app_secret的保存要做到不會被輕易的反編譯出來,
否則安全就沒有了保障
android平台建議保存到二進制的so文件中
說明:劉宏締的架構森林是一個專注架構的博客,地址:https://www.cnblogs.com/architectforest
對應的源碼可以訪問這里獲取: https://github.com/liuhongdi/
說明:作者:劉宏締 郵箱: 371125307@qq.com
二,演示項目的相關信息
1,項目的地址
https://github.com/liuhongdi/apisign
2,項目的原理:
給客戶端分發:appId,appSecret,version三個字串
appId:分配給客戶端的id
appSecret:密鑰字串,客戶端要安全保存
version:服務端的接口版本
客戶端在發送請求前,
用appId + appSecret + timestamp + nonce + version做md5,生成sign字串,
這個字串和appId/timestamp/nonce一起發送到服務端
服務端驗證sign是否正確,
如果有誤則攔截請求
3,項目的結構
如圖:

三, java代碼說明:
1,SignInterceptor.java
@Component public class SignInterceptor implements HandlerInterceptor { private static final String SIGN_KEY = "apisign_"; private static final Logger logger = LogManager.getLogger("bussniesslog"); @Resource private RedisStringUtil redisStringUtil; /* *@author:liuhongdi *@date:2020/7/1 下午4:00 *@description: * @param request:請求對象 * @param response:響應對象 * @param handler:處理對象:controller中的信息 * * *@return:true表示正常,false表示被攔截 */ @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { //依次檢查各變量是否存在? String appId = request.getHeader("appId"); if (StringUtils.isBlank(appId)) { ServletUtil.renderString(response, JSON.toJSONString(ResultUtil.error(ResponseCode.SIGN_NO_APPID))); return false; } String timestampStr = request.getHeader("timestamp"); if (StringUtils.isBlank(timestampStr)) { ServletUtil.renderString(response, JSON.toJSONString(ResultUtil.error(ResponseCode.SIGN_NO_TIMESTAMP))); return false; } String sign = request.getHeader("sign"); if (StringUtils.isBlank(sign)) { ServletUtil.renderString(response, JSON.toJSONString(ResultUtil.error(ResponseCode.SIGN_NO_SIGN))); return false; } String nonce = request.getHeader("nonce"); if (StringUtils.isBlank(nonce)) { ServletUtil.renderString(response, JSON.toJSONString(ResultUtil.error(ResponseCode.SIGN_NO_NONCE))); return false; } //得到正確的sign供檢驗用 String origin = appId + Constants.APP_SECRET + timestampStr + nonce + Constants.APP_API_VERSION; String signEcrypt = MD5Util.md5(origin); long timestamp = 0; try { timestamp = Long.parseLong(timestampStr); } catch (Exception e) { logger.error("發生異常",e); } //前端的時間戳與服務器當前時間戳相差如果大於180,判定當前請求的timestamp無效 if (Math.abs(timestamp - System.currentTimeMillis() / 1000) > 180) { ServletUtil.renderString(response, JSON.toJSONString(ResultUtil.error(ResponseCode.SIGN_TIMESTAMP_INVALID))); return false; } //nonce是否存在於redis中,檢查當前請求是否是重復請求 boolean nonceExists = redisStringUtil.hasStringkey(SIGN_KEY+timestampStr+nonce); if (nonceExists) { ServletUtil.renderString(response, JSON.toJSONString(ResultUtil.error(ResponseCode.SIGN_DUPLICATION))); return false; } //后端MD5簽名校驗與前端簽名sign值比對 if (!(sign.equalsIgnoreCase(signEcrypt))) { ServletUtil.renderString(response, JSON.toJSONString(ResultUtil.error(ResponseCode.SIGN_VERIFY_FAIL))); return false; } //將timestampstr+nonce存進redis redisStringUtil.setStringValue(SIGN_KEY+timestampStr+nonce, nonce, 180L); //sign校驗無問題,放行 return true; } @Override public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception { } @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { } }
說明:如果客戶端請求的數據缺少會被攔截
與服務端的appSecret等參數md5生成的sign不一致也會被攔截
時間超時/重復請求也會被攔截
2,DefaultMvcConfig.java
@Configuration @ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET) public class DefaultMvcConfig implements WebMvcConfigurer { @Resource private SignInterceptor signInterceptor; /** * 添加Interceptor
* liuhongdi */ @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(signInterceptor) .addPathPatterns("/**") //所有請求都需要進行報文簽名sign .excludePathPatterns("/html/*","/js/*"); //排除html/js目錄 } }
說明:用來添加interceptor
四,效果驗證:
1,js代碼實現:
說明:我們在這里使用js代碼供僅演示使用,app_secret作為密鑰不能使用js保存:
<body> <a href="javascript:login('right')">login(right)</a><br/> <a href="javascript:login('error')">login(error)</a><br/> <script> //vars var appId="wap"; var version="1.0"; //得到sign function getsign(appSecret,timestamp,nonce) { var origin = appId + appSecret + timestamp + nonce + version; console.log("origin:"+origin); var sign = hex_md5(origin); return sign; } //訪問login這個api //說明:這里僅僅是舉例子,在ios/android開發中,appSecret要以二進制的形式編譯保存 function login(isright) { //right secret var appSecret_right="30c722c6acc64306a88dd93a814c9f0a"; //error secret var appSecret_error="aabbccdd"; var timestamp = parseInt((new Date()).getTime()/1000); var nonce = Math.floor(Math.random()*8999)+1000; var sign = ""; if (isright == 'right') { sign = getsign(appSecret_right,timestamp,nonce); } else { sign = getsign(appSecret_error,timestamp,nonce); }
var postdata = { username:"a", password:"b" } $.ajax({ type:"POST", url:"/user/login", data:postdata, //返回數據的格式 datatype: "json", //在請求之前調用的函數 beforeSend: function(request) { request.setRequestHeader("appId", appId); request.setRequestHeader("timestamp", timestamp); request.setRequestHeader("sign", sign); request.setRequestHeader("nonce", nonce); }, //成功返回之后調用的函數 success:function(data){ if (data.status == 0) { alert('success:'+data.msg); } else { alert("failed:"+data.msg); } }, //調用執行后調用的函數 complete: function(XMLHttpRequest, textStatus){ //complete }, //調用出錯執行的函數 error: function(){ //請求出錯處理 } }); } </script> </body>
如圖:
說明:
login(right):使用正確的appSecret訪問login這個接口
login(error):使用錯誤的appSecret訪問login這個接口
2,查看效果:
成功時返回:
{"status":0,"msg":"操作成功","data":null}
報錯時返回:
{"msg":"sign簽名校驗失敗","status":10007}
五,查看spring boot的版本:
. ____ _ __ _ _ /\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \ ( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \ \\/ ___)| |_)| | | | | || (_| | ) ) ) ) ' |____| .__|_| |_|_| |_\__, | / / / / =========|_|==============|___/=/_/_/_/ :: Spring Boot :: (v2.3.1.RELEASE)