- 接口安全問題
- 防止篡改
- 防止重放
- timestamp+nonce方案
- 簽名流程
- 簽名規(guī)則
- 簽名生成
- 請求參數的拼接
- 請求頭的拼接
- 生成簽名
- 實現
- Spring Boot單項目的簽名實現
- 過濾器中替換HttpServletRequest
- 簽名攔截
- 微服務架構中Zuul中實現簽名實現
- Spring Boot單項目的簽名實現
- 參考文章
接口安全問題
在為第三方系統(tǒng)提供接口的時候铛只,肯定要考慮接口數據的安全問題夯膀,比如數據是否被篡改童叠,數據是否已經過時祝高,請求是否唯一捣域,數據是否可以重復提交等問題沿侈。其中數據是否被篡改相對重要衣形。
防止篡改
請求攜帶參數appid和sign翎苫,只有擁有合法的身份appid和正確的簽名sign才能放行嘉抒。這樣就解決了身份驗證和參數篡改問題零聚,即使請求參數被劫持,由于獲取不到secret(僅作本地加密使用,不參與網絡傳輸)隶症,無法偽造合法的請求政模。
防止重放
只使用appid和sign,雖然解決了請求參數被篡改的隱患蚂会,但是還存在著重復使用請求參數偽造二次請求的隱患淋样。
timestamp+nonce方案
nonce指唯一的隨機字符串,用來標識每個被簽名的請求胁住。通過為每個請求提供一個唯一的標識符趁猴,服務器能夠防止請求被多次使用(記錄所有用過的nonce以阻止它們被二次使用)。
然而彪见,對服務器來說永久存儲所有接收到的nonce的代價是非常大的儡司。可以使用timestamp來優(yōu)化nonce的存儲余指。
假設允許客戶端和服務端最多能存在10分鐘的時間差捕犬,同時追蹤記錄在服務端的nonce集合。當有新的請求進入時酵镜,首先檢查攜帶的timestamp是否在10分鐘內碉碉,如超出時間范圍,則拒絕淮韭,然后查詢攜帶的nonce垢粮,如存在(說明該請求是第二次請求),則拒絕缸濒。否則足丢,記錄該nonce,并刪除nonce集合內時間戳大于10分鐘的nonce(可以使用redis的expire庇配,新增nonce的同時設置它的超時失效時間為10分鐘)斩跌。
簽名流程
對服務端而言,攔截請求用AOP切面或者用攔截器都行捞慌,如果要對所有請求進行攔截耀鸦,可以直接攔截器處理(攔截器在切面之前,過濾器之后啸澡,具體在springmvc的dispather分發(fā)之后)袖订。
過濾器→攔截器→切面的順序:
簽名規(guī)則
線下分配appid和appsecret,針對不同的調用方分配不同的appid和appsecret
加入timestamp(時間戳)嗅虏,2分鐘內數據有效
加入流水號nonce(防止重復提交)洛姑,至少為10位。針對查詢接口皮服,流水號只用于日志落地楞艾,便于后期日志核查参咙。 針對辦理類接口需校驗流水號在有效期內的唯一性,以避免重復請求硫眯。
加入signature蕴侧,所有數據的簽名信息。
其中两入,需要放在請求頭的字段:appid 净宵、timestamp 、nonce 裹纳、signature 择葡。
簽名生成
請求參數的拼接
對各種類型的請求參數,先做如下拼接處理:
Path:按照path中的順序將所有value進行拼接
Query:按照key字典序排序剃氧,將所有key=value進行拼接
Form:按照key字典序排序刁岸,將所有key=value進行拼接
-
Body:
- Json: 按照key字典序排序,將所有key=value進行拼接(例如{"a":"a","c":"c","b":{"e":"e"}} => a=ab=e=ec=c)
- String: 整個字符串作為一個拼接
如果存在多種數據形式她我,則按照path、query迫横、form番舆、body的順序進行再拼接,得到所有數據的拼接值矾踱。
上述拼接的值記作 Y恨狈。
請求頭的拼接
X=”appid=xxxnonce=xxxtimestamp=xxx”
生成簽名
最終拼接值=XY。最后將最終拼接值按照一個加密算法得到簽名呛讲。
雖然散列算法會有推薦使用 SHA-256禾怠、SHA-384、SHA-512贝搁,禁止使用 MD5吗氏。但其實簽名這里用MD5加密沒多大問題,不推薦MD5主要是因為雷逆,網絡有大量的MD5解密庫弦讽。
實現
Spring Boot單項目的簽名實現
實現可以分以下幾步:
- 過濾器中替換自定義的緩存有body參數的HttpServletRequest
- 切面或者攔截器中,實現簽名攔截
過濾器中替換HttpServletRequest
自定義的緩存有body參數的HttpServletRequest:
/*
* copyright(c) ?2003-2020 Young. All Rights Reserved.
*/
import javax.servlet.ReadListener;
import javax.servlet.ServletInputStream;
import javax.servlet.ServletRequest;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import java.io.*;
import java.nio.charset.Charset;
/**
* 在使用HTTP協(xié)議實現應用間接口通信時膀哲,服務端讀取客戶端請求過來的數據往产,會用到request.getInputStream(),
* 第一次讀取的時候可以讀取到數據某宪,但是接下來的讀取操作都讀取不到數據仿村。
*
* 原因:
* 1. 一個InputStream對象在被讀取完成后,將無法被再次讀取兴喂,始終返回-1蔼囊;
* 2. InputStream并沒有實現reset方法(可以重置首次讀取的位置)焚志,無法實現重置操作;
*
* 解決方法(緩存讀取到的數據):
* 1.使用request压真、session等來緩存讀取到的數據,這種方式很容易實現娩嚼,只要setAttribute和getAttribute就行;
* 2.使用HttpServletRequestWrapper來包裝HttpServletRequest滴肿,在HttpServletRequestWrapper中初始化讀取request的InputStream數據岳悟,以byte[]形式緩存在其中,然后在Filter中將request轉換為包裝過的request泼差; *
*
* @author young
* @version v1.0
*/
public class BufferedHttpServletRequest extends HttpServletRequestWrapper {
private final byte[] body;
/**
* 將body取出存儲起來然后再放回去贵少,但是在request.getParameter()時數據就會丟失
* 調用getParameterMap(),目的將參數Map從body中取出堆缘,這樣后續(xù)的任何request.getParamter()都會有值
* @param request request
* @throws IOException io異常
*/
public BufferedHttpServletRequest(HttpServletRequest request) throws IOException {
super(request);
// request.getParameterMap();//此處將body中的parameter取出來滔灶,,這樣后續(xù)的任何request.getParamter()都會有值
this.body = this.getBodyString(request).getBytes(Charset.forName("UTF-8"));
}
private String getBodyString(ServletRequest request) {
StringBuilder sb = new StringBuilder();
InputStream inputStream = null;
BufferedReader reader = null;
try {
inputStream = request.getInputStream();
reader = new BufferedReader(new InputStreamReader(inputStream, Charset.forName("UTF-8")));
String line;
while ((line = reader.readLine()) != null) {
sb.append(line);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
if (inputStream != null) {
try {
inputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (reader != null) {
try {
reader.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
return sb.toString();
}
@Override
public BufferedReader getReader() throws IOException {
return new BufferedReader(new InputStreamReader(this.getInputStream()));
}
@Override
public ServletInputStream getInputStream() throws IOException {
final ByteArrayInputStream newIS = new ByteArrayInputStream(this.body);
return new ServletInputStream() {
@Override
public boolean isFinished() {
return false;
}
@Override
public boolean isReady() {
return false;
}
@Override
public void setReadListener(ReadListener readListener) {
}
@Override
public int read() throws IOException {
return newIS.read();
}
};
}
}
過濾器中替換自定義的RequestServlet:
/*
* copyright(c) ?2003-2020 Young. All Rights Reserved.
*/
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.alibaba.fastjson.serializer.SerializerFeature;
import lombok.extern.log4j.Log4j2;
import org.apache.commons.io.IOUtils;
import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.util.Collections;
/**
* request中的body緩存起來的過濾器(即替換ServletRequest為自定義的緩存body的Request)吼肥。
*
* @author young
* @version v1.0
*/
@Log4j2
public class BodyCachingFilter implements Filter {
@Override
public void destroy() {
}
@Override
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {
ServletRequest requestWrapper = null;
if(request instanceof HttpServletRequest) {
requestWrapper = new BufferedHttpServletRequest((HttpServletRequest) request);
}
if(requestWrapper == null) {
chain.doFilter(request, response);
} else {
chain.doFilter(requestWrapper, response);
}
}
@Override
public void init(FilterConfig arg0) throws ServletException {
}
}
添加過濾器的配置以及注意順序:
/*
* copyright(c) ?2003-2020 Young. All Rights Reserved.
*/
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* 過濾器配置录平。
*
* @author young
* @version v1.0
*/
@Configuration
public class FilterConfig {
@Bean
public BodyCachingFilter requestCachingFilter() {
return new BodyCachingFilter();
}
@Bean
public FilterRegistrationBean requestCachingFilterRegistration(BodyCachingFilter bodyCachingFilter) {
FilterRegistrationBean bean = new FilterRegistrationBean(bodyCachingFilter);
bean.setOrder(1);
return bean;
}
}
切面或者攔截器中,實現簽名攔截
/*
* copyright(c) ?2003-2020 Young. All Rights Reserved.
*/
import lombok.extern.log4j.Log4j2;
import org.apache.commons.codec.digest.HmacUtils;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.StringUtils;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import org.springframework.web.context.request.ServletWebRequest;
import org.springframework.web.servlet.HandlerMapping;
import javax.servlet.http.HttpServletRequest;
import java.util.Arrays;
import java.util.Map;
import java.util.concurrent.TimeUnit;
/**
* 簽名切面缀皱。
*
* @author young
* @version v1.0
*/
@Order(1)
@Aspect
@Component
@Log4j2
public class SignatureAspect {
private static final String HEADER_APPID = "appid";
private static final String HEADER_TIMESTAMP = "timestamp";
private static final String HEADER_NONCE = "nonce";
private static final String HEADER_SIGNATURE = "signature";
/**
* APP_ID + SECRET 開放平臺的話斗这,理應用線下分配,線上存儲的方式啤斗,但作為一個定向服務表箭,可以直接定義一個固定值。
*/
private static final String SIGN_APPID = "xxx";
private static final String SIGN_SECRET = "xxxxxxx";
/**
* 同一個請求多長時間內有效(2min)钮莲。
*/
private static final Long EXPIRE_TIME = 60 * 1000 * 2L;
/**
* 同一個nonce 請求多長時間內不允許重復請求(2min)免钻。
*/
private static final Long RESUBMIT_DURATION = 60 * 1000 * 2L;
@Autowired
RedisService redisService;
@Before("execution(* com.xxx.controller..*.*(..)) ")
public void doBefore(JoinPoint joinPoint) throws Throwable {
try {
HttpServletRequest request = ((ServletRequestAttributes) (RequestContextHolder.currentRequestAttributes())).getRequest();
String nonce = request.getHeader(HEADER_NONCE);
String timestamp =request.getHeader(HEADER_TIMESTAMP);
String sign = request.getHeader(HEADER_SIGNATURE);
//其他合法性校驗
Long now = System.currentTimeMillis();
Long requestTimestamp = Long.parseLong(timestamp);
if ((now - requestTimestamp) > EXPIRE_TIME) {
String errMsg = "請求時間超過規(guī)定范圍時間2分鐘, signature=" + sign;
log.error(errMsg);
throw new RequestException("請求時間超過規(guī)定范圍時間2分鐘");
}
if (nonce.length() < 10) {
String errMsg = "nonce長度最少為10位, nonce=" + nonce;
log.error(errMsg);
throw new RequestException("nonce長度最少為10位");
}
//redis 存儲管理nonce
String key = "NONCE_"+nonce;
if (this.redisService.hasKey(key)) {
String errMsg = "不允許重復請求, nonce=" + nonce;
log.error(errMsg);
throw new RequestException("不允許重復請求");
} else {
this.redisService.set(key, nonce);
this.redisService.expire(key, (int) TimeUnit.MILLISECONDS.toSeconds(RESUBMIT_DURATION));
}
this.checkSign(request);
} catch (Throwable e) {
log.error("SignatureAspect>>>>>>>>", e);
throw e;
}
}
private void checkSign(HttpServletRequest request) throws Exception {
String nonce = request.getHeader(HEADER_NONCE);
String timestamp =request.getHeader(HEADER_TIMESTAMP);
String oldSign = request.getHeader(HEADER_SIGNATURE);
String headerSplice = HEADER_APPID+"="+SIGN_APPID+HEADER_NONCE+"="+nonce+HEADER_TIMESTAMP+"="+timestamp;
if (StringUtils.isBlank(oldSign)) {
throw new RequestException("無簽名Header[SIGN]信息");
}
//獲取body(對應@RequestBody)
String body = null;
if (request instanceof BufferedHttpServletRequest) {
body = IOUtils.toString(request.getInputStream(), "UTF-8");
}
//獲取parameters(對應@RequestParam)
Map<String, String[]> params = null;
if (!CollectionUtils.isEmpty(request.getParameterMap())) {
params = request.getParameterMap();
}
//獲取path variable(對應@PathVariable)
String[] paths = null;
ServletWebRequest webRequest = new ServletWebRequest(request, null);
Map<String, String> uriTemplateVars = (Map<String, String>) webRequest.getAttribute(
HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE, RequestAttributes.SCOPE_REQUEST);
if (!CollectionUtils.isEmpty(uriTemplateVars)) {
paths = uriTemplateVars.values().toArray(new String[]{});
}
String newSign = this.generateSign(headerSplice,paths, params, body);
log.debug(request.getRequestURI()+"生成的簽名:"+newSign);
if (!newSign.equals(oldSign)) {
throw new RequestException("簽名不一致...");
}
}
/**
* 生成簽名。
* 請求參數的拼接:
* 對各種類型的請求參數崔拥,先做如下拼接處理:
* - Path:按照path中的順序將所有value進行拼接
* - Query:按照key字典序排序极舔,將所有key=value進行拼接
* - Form:按照key字典序排序,將所有key=value進行拼接
* - Body:
* - Json: 按照key字典序排序握童,將所有key=value進行拼接(例如{"a":"a","c":"c","b":{"e":"e"}} => a=ab=e=ec=c)
* - String: 整個字符串作為一個拼接
* 如果存在多種數據形式姆怪,則按照path、query澡绩、form稽揭、body的順序進行再拼接,得到所有數據的拼接值肥卡。
* 上述拼接的值記作 Y溪掀。
*
* 請求頭的拼接:
* X=”appid=xxxnonce=xxxtimestamp=xxx”
*
* 生成簽名:
* 最終拼接值=XY。最后將最終拼接值按照一個加密算法得到簽名(這里使用MD5算法)步鉴。
* 雖然散列算法會有推薦使用 SHA-256揪胃、SHA-384璃哟、SHA-512,禁止使用 MD5喊递。但其實簽名這里用MD5加密沒多大問題随闪,不推薦MD5主要是因為,網絡有大量的MD5解密庫骚勘。
* @param body request中的body參數
* @param params request中的param參數
* @param paths request中的path參數
* @return 簽名信息
*/
private String generateSign(String headerSplice,String[] paths,Map<String, String[]> params,String body ) {
StringBuilder sb = new StringBuilder();
sb.append(headerSplice);
if (ArrayUtils.isNotEmpty(paths)) {
// String pathValues = String.join(",", Arrays.stream(paths).sorted().toArray(String[]::new));
String pathValues = String.join("", Arrays.stream(paths).toArray(String[]::new));
sb.append(pathValues);
}
if (!CollectionUtils.isEmpty(params)) {
params.entrySet()
.stream()
.sorted(Map.Entry.comparingByKey())
.forEach(paramEntry -> {
String paramValue = String.join(",", Arrays.stream(paramEntry.getValue()).sorted().toArray(String[]::new));
sb.append(paramEntry.getKey()).append("=").append(paramValue);
});
}
if (StringUtils.isNotBlank(body)) {
sb.append(body);
}
sb.append('#');
log.debug("參數拼接:"+sb.toString());
return HmacUtils.hmacSha256Hex(SIGN_SECRET, sb.toString());
}
}
微服務架構中Zuul中實現簽名實現
由于Zuul自帶默認的過濾中铐伴,有已經對body處理過的(FormBodyWrapperFilter),所以在Zuul中處理簽名俏讹,只需添加一個過濾器即可如下当宴。
類型 | 順序 | 過濾器 | 功能 |
---|---|---|---|
pre | -3 | ServletDetectionFilter | 標記處理Servlet的類型 |
pre | -2 | Servlet30WrapperFilter | 包裝HttpServletRequest請求 |
pre | -1 | FormBodyWrapperFilter | 包裝請求體 |
route | 1 | DebugFilter | 標記調試標志 |
route | 5 | PreDecorationFilter | 處理請求上下文供后續(xù)使用 |
route | 10 | RibbonRoutingFilter | serviceId請求轉發(fā) |
route | 100 | SimpleHostRoutingFilter | url請求轉發(fā) |
route | 500 | SendForwardFilter | forward請求轉發(fā) |
post | 0 | SendErrorFilter | 處理有錯誤的請求響應 |
post | 1000 | SendResponseFilter | 處理正常的請求響應 |
/*
* copyright(c) ?2003-2020 Young. All Rights Reserved.
*/
package com.talebase.zuul.filter;
import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;
import com.talebase.protocol.ServiceResponse;
import org.apache.commons.codec.digest.HmacUtils;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.util.CollectionUtils;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.ServletWebRequest;
import org.springframework.web.servlet.HandlerMapping;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.util.Arrays;
import java.util.Map;
import java.util.concurrent.TimeUnit;
/**
* 簽名過濾器(簽名防篡改,同時加入nonce+timestamp防重放)泽疆。
* 補充說明:
* 1户矢、“PRE” 類型,除了Zuul中默認實現的PRE的三個Filter,最先執(zhí)行殉疼,order為0梯浪。
* 2、order = -1 的為Zuul中已經實現的FormBodyWrapperFilter瓢娜,支持body參數重復讀驱证。
*
* @author Young
* @version v1.0
*/
public class SignatureFilter extends ZuulFilter {
private final Logger logger = LoggerFactory.getLogger(SignatureFilter.class);
private final static Integer REQUEST_FORBIDDEN_CODE = 403;
private static final String HEADER_APPID = "appid";
private static final String HEADER_TIMESTAMP = "timestamp";
private static final String HEADER_NONCE = "nonce";
private static final String HEADER_SIGNATURE = "signature";
/**
* APP_ID + SECRET 開放平臺的話,理應用線下分配恋腕,線上存儲的方式,但作為一個定向服務逆瑞,可以直接定義一個固定值荠藤。
*/
private static final String SIGN_APPID = "xxxxx";
private static final String SIGN_SECRET = "xxxxxx";
/**
* 同一個請求多長時間內有效(2min)。
*/
private static final Long EXPIRE_TIME = 60 * 1000 * 2L;
/**
* 同一個nonce 請求多長時間內不允許重復請求(2min)获高。
*/
private static final Long RESUBMIT_DURATION = 60 * 1000 * 2L;
@Autowired
private IJedis myJedis;
@Override
public String filterType() {
return "pre";
}
@Override
public int filterOrder() {
return 0;
}
@Override
public boolean shouldFilter() {
return true;
}
@Override
public Object run() {
RequestContext ctx = RequestContext.getCurrentContext();
HttpServletRequest request = ctx.getRequest();
String nonce = request.getHeader(HEADER_NONCE);
String timestamp = request.getHeader(HEADER_TIMESTAMP);
String oldSign = request.getHeader(HEADER_SIGNATURE);
if (null == nonce || null == timestamp || null == oldSign) {
setRequestContext(BizEnums.SIGNATURE_PARAM_MISS);
return null;
}
//其他合法性校驗
Long now = System.currentTimeMillis();
Long requestTimestamp = Long.parseLong(timestamp);
if ((now - requestTimestamp) > EXPIRE_TIME) {
setRequestContext(BizEnums.TIME_OUT);
return null;
}
if (nonce.length() < 10) {
setRequestContext(BizEnums.NONCE_LENGTH_ERROR);
return null;
}
//redis 存儲管理nonce
String key = "NONCE_" + nonce;
if (this.myJedis.exists(key)) {
setRequestContext(BizEnums.REPEAT_REQUEST_FORBIDDEN);
return null;
} else {
this.myJedis.set(key, nonce);
this.myJedis.expire(key, (int) TimeUnit.MILLISECONDS.toSeconds(RESUBMIT_DURATION));
}
//檢驗簽名
// this.checkSign(request);
String headerSplice = HEADER_APPID + "=" + SIGN_APPID + HEADER_NONCE + "=" + nonce + HEADER_TIMESTAMP + "=" + timestamp;
//獲取body(對應@RequestBody)
String body = null;
try {
body = IOUtils.toString(request.getInputStream(), "UTF-8");
} catch (IOException e) {
setRequestContext(BizEnums.SIGNATURE_ERROR);
return null;
}
//獲取parameters(對應@RequestParam)
Map<String, String[]> params = null;
if (!CollectionUtils.isEmpty(request.getParameterMap())) {
params = request.getParameterMap();
}
//獲取path variable(對應@PathVariable)
String[] paths = null;
ServletWebRequest webRequest = new ServletWebRequest(request, null);
Map<String, String> uriTemplateVars = (Map<String, String>) webRequest.getAttribute(
HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE, RequestAttributes.SCOPE_REQUEST);
if (!CollectionUtils.isEmpty(uriTemplateVars)) {
paths = uriTemplateVars.values().toArray(new String[]{});
}
String newSign = this.generateSign(headerSplice, paths, params, body);
this.logger.debug(request.getRequestURI() + "生成的簽名:" + newSign);
if (!newSign.equals(oldSign)) {
setRequestContext(BizEnums.SIGNATURE_ERROR);
return null;
}
ctx.set("SignError",false);
ctx.setSendZuulResponse(true);
return null;
}
private void setRequestContext(BizEnums bizEnums) {
RequestContext ctx = RequestContext.getCurrentContext();
ServiceResponse sr = new ServiceResponse();
sr.setCode(bizEnums.getCode());
sr.setMessage(bizEnums.getMessage());
ctx.setSendZuulResponse(false);
ctx.setResponseStatusCode(REQUEST_FORBIDDEN_CODE);//默認是200
ctx.setResponseBody(GsonUtil.toJson(sr));
ctx.set("SignError",true);
ctx.getResponse().setContentType("application/json;charset=utf-8");
}
/**
* 生成簽名哈肖。
* 請求參數的拼接:
* 對各種類型的請求參數,先做如下拼接處理:
* - Path:按照path中的順序將所有value進行拼接
* - Query:按照key字典序排序念秧,將所有key=value進行拼接
* - Form:按照key字典序排序淤井,將所有key=value進行拼接
* - Body:
* - Json: 按照key字典序排序,將所有key=value進行拼接(例如{"a":"a","c":"c","b":{"e":"e"}} => a=ab=e=ec=c)
* - String: 整個字符串作為一個拼接
* 如果存在多種數據形式摊趾,則按照path币狠、query、form砾层、body的順序進行再拼接漩绵,得到所有數據的拼接值。
* 上述拼接的值記作 Y肛炮。
* <p>
* 請求頭的拼接:
* X=”appid=xxxnonce=xxxtimestamp=xxx”
* <p>
* 生成簽名:
* 最終拼接值=XY止吐。最后將最終拼接值按照一個加密算法得到簽名(這里使用SHA-256算法)宝踪。
* 雖然散列算法會有推薦使用 SHA-256、SHA-384碍扔、SHA-512瘩燥,禁止使用 MD5。但其實簽名這里用MD5加密沒多大問題不同,不推薦MD5主要是因為厉膀,網絡有大量的MD5解密庫。
*
* @param body request中的body參數
* @param params request中的param參數
* @param paths request中的path參數
* @return 簽名信息
*/
private String generateSign(String headerSplice, String[] paths, Map<String, String[]> params, String body) {
StringBuilder sb = new StringBuilder();
sb.append(headerSplice);
if (ArrayUtils.isNotEmpty(paths)) {
// String pathValues = String.join(",", Arrays.stream(paths).sorted().toArray(String[]::new));
String pathValues = String.join("", Arrays.stream(paths).toArray(String[]::new));
sb.append(pathValues);
}
if (!CollectionUtils.isEmpty(params)) {
params.entrySet()
.stream()
.sorted(Map.Entry.comparingByKey())
.forEach(paramEntry -> {
String paramValue = String.join(",", Arrays.stream(paramEntry.getValue()).sorted().toArray(String[]::new));
sb.append(paramEntry.getKey()).append("=").append(paramValue);
});
}
if (StringUtils.isNotBlank(body)) {
sb.append(body);
}
sb.append('#');
this.logger.debug("參數拼接:" + sb.toString());
return HmacUtils.hmacSha256Hex(SIGN_SECRET, sb.toString());
}
}