後端在寫對外的API接口時,通常會對參數進行簽名來保證接口的安全性,在設計簽名算法的時候,主要考慮的是這幾個問題: 1. 請求的來源是否合法 2. 請求參數是否被篡改 3. 請求的惟一性 咱們的簽名加密也是主要針對這幾個問題來實現
基於上述的幾個問題,咱們來經過已下步驟來實現簽名加密: 1. 經過分配給APP對應的app_key和app_secret來驗證身份 2. 經過將請求的全部參數按照字母前後順序排序後拼接再MD5加密老保證請求參數不被篡改 3. 請求裏攜帶時間戳參數老保證請求的惟一和過時,重複的請求在指定時間(可配置)內有效
簽名生成:java
- 生成當前時間戳timestamp=now
- 按照請求參數名的字母升序排列非空請求參數(包含accessKey)
stringA="AccessKey=access&home=world&name=hello&work=java×tamp=now&nonce=random";
- 拼接密鑰accessSecret
stringSignTemp="AccessKey=access&home=world&name=hello&work=java×tamp=now&nonce=random&accessSecret=secret";
- MD5並轉換爲大寫生成簽名
sign=MD5(stringSignTemp).toUpperCase();
JAVA代碼以下:params是從request裏面獲取的全部參數map,accessSecret是加密密鑰web
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(); }
攔截器部分代碼算法
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; }
校驗簽名:spring
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); }
這裏經過攔截器來實現接口攔截,可自行替換apache
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(); } }