一,為什么要給接口做簽名驗證?
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)
