1. 程式人生 > 實用技巧 >spring boot:給介面增加簽名驗證(spring boot 2.3.1)

spring boot:給介面增加簽名驗證(spring boot 2.3.1)

一,為什麼要給介面做簽名驗證?

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/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)