spring boot:給介面增加簽名驗證(spring boot 2.3.1)
阿新 • • 發佈:2020-07-01
一,為什麼要給介面做簽名驗證?
1,app客戶端在與服務端通訊時,通常都是以介面的形式實現, 這種形式的安全方面有可能出現以下問題: 被非法訪問(例如:發簡訊的介面通常會被利用來垃圾簡訊) 被重複訪問 (例如:在提交訂單時多點了幾次提交按鈕) 而客戶端存在的弱點是:對介面站的地址不能輕易修改, 所以我們需要針對從app到介面的介面做簽名驗證, 介面不能隨便app之外的應用訪問 2,要注意的地方: 我們給app分配一個app_id和一個app_secret app對app_secret的儲存要做到不會被輕易的反編譯出來, 否則安全就沒有了保障 android平臺建議儲存到二進位制的so檔案中說明:劉巨集締的架構森林是一個專注架構的部落格,地址: https://www.cnblogs.com/architectforest
對應的原始碼可以訪問這裡獲取:https://github.com/liuhongdi/
說明:作者:劉巨集締 郵箱: [email protected]
二,演示專案的相關資訊
1,專案的地址https://github.com/liuhongdi/apisign2,專案的原理: 給客戶端分發:appId,appSecret,version三個字串 appId:分配給客戶端的id appSecret:金鑰字串,客戶端要安全儲存 version:服務端的介面版本 客戶端在傳送請求前, 用appId + appSecret + timestamp + nonce + version做md5,生成sign字串, 這個字串和appId/timestamp/nonce一起傳送到服務端 服務端驗證sign是否正確, 如果有誤則攔截請求 3,專案的結構 如圖:
三,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這個介面
{"status":0,"msg":"操作成功","data":null}
報錯時返回:
{"msg":"sign簽名校驗失敗","status":10007}
五,檢視spring boot的版本:
. ____ _ __ _ _ /\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \ ( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \ \\/ ___)| |_)| | | | | || (_| | ) ) ) ) ' |____| .__|_| |_|_| |_\__, | / / / / =========|_|==============|___/=/_/_/_/ :: Spring Boot :: (v2.3.1.RELEASE)