后端在写对外的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(); } }