簡單API介面簽名驗證

Kelin發表於2018-08-22

前言

後端在寫對外的API介面時,一般會對引數進行簽名來保證介面的安全性,在設計簽名演算法的時候,主要考慮的是這幾個問題:
1. 請求的來源是否合法
2. 請求引數是否被篡改
3. 請求的唯一性
我們的簽名加密也是主要針對這幾個問題來實現
複製程式碼

設計

基於上述的幾個問題,我們來通過已下步驟來實現簽名加密:
1. 通過分配給APP對應的app_key和app_secret來驗證身份
2. 通過將請求的所有引數按照字母先後順序排序後拼接再MD5加密老保證請求引數不被篡改
3. 請求裡攜帶時間戳引數老保證請求的唯一和過期,重複的請求在指定時間(可配置)內有效
複製程式碼

實現

  1. 簽名生成:

    1. 生成當前時間戳timestamp=now
    2. 按照請求引數名的字母升序排列非空請求引數(包含accessKey) stringA="AccessKey=access&home=world&name=hello&work=java&timestamp=now&nonce=random";
    3. 拼接金鑰accessSecret stringSignTemp="AccessKey=access&home=world&name=hello&work=java&timestamp=now&nonce=random&accessSecret=secret";
    4. MD5並轉換為大寫生成簽名 sign=MD5(stringSignTemp).toUpperCase();

JAVA程式碼如下:params是從request裡面獲取的所有引數map,accessSecret是加密金鑰

 private String createSign(Map<String, Object> params, String accessSecret) throws UnsupportedEncodingException {
        Set<String> keysSet = params.keySet();
        Object[] keys = keysSet.toArray();
        Arrays.sort(keys);
        StringBuilder temp = new StringBuilder();
        boolean first = true;
        for (Object key : keys) {
            if (first) {
                first = false;
            } else {
                temp.append("&");
            }
            temp.append(key).append("=");
            Object value = params.get(key);
            String valueString = "";
            if (null != value) {
                valueString = String.valueOf(value);
            }
            temp.append(valueString);
        }
        temp.append("&").append(ACCESS_SECRET).append("=").append(accessSecret);
        return MD5Util.MD52(temp.toString()).toUpperCase();
    }
複製程式碼
  1. 簽名校驗:

攔截器部分程式碼

  • 引數格式校驗
  • 超時校驗
  • 驗證簽名
  public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {

        Map<String, Object> result = new HashMap<String, Object>();
        String timestamp = request.getParameter(TIMESTAMP_KEY);
        String accessKey = request.getParameter(ACCESS_KEY);
        String accessSecret = map.get(accessKey);

        if (!org.apache.commons.lang.StringUtils.isNumeric(timestamp)) {
            result.put("code", 1000);
            result.put("msg", "請求時間戳不合法");
            WebUtils.writeJsonByObj(result, response, request);
            return false;
        }

        // 檢查KEY是否合理
        if (StringUtils.isEmpty(accessKey) || StringUtils.isEmpty(accessSecret)) {
            result.put("code", 1001);
            result.put("msg", "加密KEY不合法");
            WebUtils.writeJsonByObj(result, response, request);
            return false;
        }
        Long ts = Long.valueOf(timestamp);
        // 禁止超時簽名
        if (System.currentTimeMillis() - ts > SIGN_EXPIRED_TIME) {
            result.put("code", 1002);
            result.put("msg", "請求超時");
            WebUtils.writeJsonByObj(result, response, request);
            return false;
        }

        if (!verificationSign(request, accessKey, accessSecret)) {
            result.put("code", 1003);
            result.put("msg", "簽名錯誤");
            WebUtils.writeJsonByObj(result, response, request);
            return false;
        }
        return true;
    }
複製程式碼

校驗簽名

 private boolean verificationSign(HttpServletRequest request, String accessKey, String accessSecret) throws UnsupportedEncodingException {
        Enumeration<?> pNames = request.getParameterNames();
        Map<String, Object> params = new HashMap<String, Object>();
        while (pNames.hasMoreElements()) {
            String pName = (String) pNames.nextElement();
            if (SIGN_KEY.equals(pName)) continue;
            Object pValue = request.getParameter(pName);
            params.put(pName, pValue);
        }
        String originSign = request.getParameter(SIGN_KEY);
        String sign = createSign(params, accessSecret);
        return sign.equals(originSign);
    }
複製程式碼
  1. 完整程式碼:

這裡通過攔截器來實現介面攔截,可自行替換

package com.mlcs.mop.common.web.interceptor;

import com.mlcs.core.conf.ZKClient;
import com.mlcs.mop.common.web.util.MD5Util;
import com.mlcs.mop.common.web.util.WebUtils;
import org.apache.zookeeper.KeeperException;
import org.springframework.util.StringUtils;
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.StringReader;
import java.io.UnsupportedEncodingException;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;

/**
 * Author: Kelin
 * Date:  2018/5/16
 * Description:
 */
@SuppressWarnings("SuspiciousMethodCalls")
public class SimpleApiSignInterceptor extends HandlerInterceptorAdapter {

    // 簽名超時時長,預設時間為5分鐘,ms
    private static final int SIGN_EXPIRED_TIME = 5 * 60 * 1000;

    private static final String API_SIGN_KEY_CONFIG_PATH = "/mop/common/system/api_sign_key_mapping.properties";

    private static final String SIGN_KEY = "sign";

    private static final String TIMESTAMP_KEY = "timestamp";

    private static final String ACCESS_KEY = "accessKey";

    private static final String ACCESS_SECRET = "accessSecret";

    private static Map<String, String> map = new ConcurrentHashMap<String, String>();


    static {
        // 從zk載入key對映到記憶體裡面
        try {
            String data = ZKClient.get().getStringData(API_SIGN_KEY_CONFIG_PATH);
            Properties properties = new Properties();
            properties.load(new StringReader(data));
            for (Object key : properties.keySet()) {
                map.put(String.valueOf(key), properties.getProperty(String.valueOf(key)));
            }
        } catch (KeeperException e) {
            e.printStackTrace();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }

    }

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {

        Map<String, Object> result = new HashMap<String, Object>();
        String timestamp = request.getParameter(TIMESTAMP_KEY);
        String accessKey = request.getParameter(ACCESS_KEY);
        String accessSecret = map.get(accessKey);

        if (!org.apache.commons.lang.StringUtils.isNumeric(timestamp)) {
            result.put("code", 1000);
            result.put("msg", "請求時間戳不合法");
            WebUtils.writeJsonByObj(result, response, request);
            return false;
        }

        // 檢查KEY是否合理
        if (StringUtils.isEmpty(accessKey) || StringUtils.isEmpty(accessSecret)) {
            result.put("code", 1001);
            result.put("msg", "加密KEY不合法");
            WebUtils.writeJsonByObj(result, response, request);
            return false;
        }
        Long ts = Long.valueOf(timestamp);
        // 禁止超時簽名
        if (System.currentTimeMillis() - ts > SIGN_EXPIRED_TIME) {
            result.put("code", 1002);
            result.put("msg", "請求超時");
            WebUtils.writeJsonByObj(result, response, request);
            return false;
        }

        if (!verificationSign(request, accessKey, accessSecret)) {
            result.put("code", 1003);
            result.put("msg", "簽名錯誤");
            WebUtils.writeJsonByObj(result, response, request);
            return false;
        }
        return true;
    }

    private boolean verificationSign(HttpServletRequest request, String accessKey, String accessSecret) throws UnsupportedEncodingException {
        Enumeration<?> pNames = request.getParameterNames();
        Map<String, Object> params = new HashMap<String, Object>();
        while (pNames.hasMoreElements()) {
            String pName = (String) pNames.nextElement();
            if (SIGN_KEY.equals(pName)) continue;
            Object pValue = request.getParameter(pName);
            params.put(pName, pValue);
        }
        String originSign = request.getParameter(SIGN_KEY);
        String sign = createSign(params, accessSecret);
        return sign.equals(originSign);
    }

    private String createSign(Map<String, Object> params, String accessSecret) throws UnsupportedEncodingException {
        Set<String> keysSet = params.keySet();
        Object[] keys = keysSet.toArray();
        Arrays.sort(keys);
        StringBuilder temp = new StringBuilder();
        boolean first = true;
        for (Object key : keys) {
            if (first) {
                first = false;
            } else {
                temp.append("&");
            }
            temp.append(key).append("=");
            Object value = params.get(key);
            String valueString = "";
            if (null != value) {
            
                valueString = String.valueOf(value);
            }
            temp.append(valueString);
        }
        temp.append("&").append(ACCESS_SECRET).append("=").append(accessSecret);
        return MD5Util.MD52(temp.toString()).toUpperCase();
    }
}

複製程式碼

相關文章