- 接口安全问题
- 防止篡改
- 防止重放
- timestamp+nonce方案
- 签名流程
- 签名规则
- 签名生成
- 请求参数的拼接
- 请求头的拼接
- 生成签名
- 实现
- Spring Boot单项目的签名实现
- 过滤器中替换HttpServletRequest
- 签名拦截
- 微服务架构中Zuul中实现签名实现
- Spring Boot单项目的签名实现
- 参考文章
接口安全问题
在为第三方系统提供接口的时候,肯定要考虑接口数据的安全问题,比如数据是否被篡改,数据是否已经过时,请求是否唯一,数据是否可以重复提交等问题。其中数据是否被篡改相对重要。
防止篡改
请求携带参数appid和sign,只有拥有合法的身份appid和正确的签名sign才能放行。这样就解决了身份验证和参数篡改问题,即使请求参数被劫持,由于获取不到secret(仅作本地加密使用,不参与网络传输),无法伪造合法的请求。
防止重放
只使用appid和sign,虽然解决了请求参数被篡改的隐患,但是还存在着重复使用请求参数伪造二次请求的隐患。
timestamp+nonce方案
nonce指唯一的随机字符串,用来标识每个被签名的请求。通过为每个请求提供一个唯一的标识符,服务器能够防止请求被多次使用(记录所有用过的nonce以阻止它们被二次使用)。
然而,对服务器来说永久存储所有接收到的nonce的代价是非常大的。可以使用timestamp来优化nonce的存储。
假设允许客户端和服务端最多能存在10分钟的时间差,同时追踪记录在服务端的nonce集合。当有新的请求进入时,首先检查携带的timestamp是否在10分钟内,如超出时间范围,则拒绝,然后查询携带的nonce,如存在(说明该请求是第二次请求),则拒绝。否则,记录该nonce,并删除nonce集合内时间戳大于10分钟的nonce(可以使用redis的expire,新增nonce的同时设置它的超时失效时间为10分钟)。
签名流程
signature_flow.png对服务端而言,拦截请求用AOP切面或者用拦截器都行,如果要对所有请求进行拦截,可以直接拦截器处理(拦截器在切面之前,过滤器之后,具体在springmvc的dispather分发之后)。
过滤器→拦截器→切面的顺序:
filter&interceptor.png签名规则
-
线下分配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协议实现应用间接口通信时,服务端读取客户端请求过来的数据,会用到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中取出,这样后续的任何request.getParamter()都会有值
* @param request request
* @throws IOException io异常
*/
public BufferedHttpServletRequest(HttpServletRequest request) throws IOException {
super(request);
// request.getParameterMap();//此处将body中的parameter取出来,,这样后续的任何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 = "请求时间超过规定范围时间2分钟, signature=" + sign;
log.error(errMsg);
throw new RequestException("请求时间超过规定范围时间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 | 处理请求上下文供后续使用 |
route | 10 | RibbonRoutingFilter | serviceId请求转发 |
route | 100 | SimpleHostRoutingFilter | url请求转发 |
route | 500 | SendForwardFilter | forward请求转发 |
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,最先执行,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());
}
}
网友评论