前言
後端在寫對外的API介面時,一般會對引數進行簽名來保證介面的安全性,在設計簽名演算法的時候,主要考慮的是這幾個問題:
1. 請求的來源是否合法
2. 請求引數是否被篡改
3. 請求的唯一性
我們的簽名加密也是主要針對這幾個問題來實現
複製程式碼
設計
基於上述的幾個問題,我們來通過已下步驟來實現簽名加密:
1. 通過分配給APP對應的app_key和app_secret來驗證身份
2. 通過將請求的所有引數按照字母先後順序排序後拼接再MD5加密老保證請求引數不被篡改
3. 請求裡攜帶時間戳引數老保證請求的唯一和過期,重複的請求在指定時間(可配置)內有效
複製程式碼
實現
-
簽名生成:
- 生成當前時間戳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是加密金鑰
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;
}
複製程式碼
校驗簽名:
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);
}
複製程式碼
- 完整程式碼:
這裡通過攔截器來實現介面攔截,可自行替換
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();
}
}
複製程式碼