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/

說明:作者:劉宏締 郵箱: 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)

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM