接口签名实现

  • 接口安全问题
  • 防止篡改
  • 防止重放
    • timestamp+nonce方案
  • 签名流程
  • 签名规则
  • 签名生成
    • 请求参数的拼接
    • 请求头的拼接
    • 生成签名
  • 实现
    • Spring Boot单项目的签名实现
      • 过滤器中替换HttpServletRequest
      • 签名拦截
    • 微服务架构中Zuul中实现签名实现
  • 参考文章

接口安全问题

在为第三方系统提供接口的时候,肯定要考虑接口数据的安全问题,比如数据是否被篡改,数据是否已经过时,请求是否唯一,数据是否可以重复提交等问题。其中数据是否被篡改相对重要。

防止篡改

请求携带参数appidsign,只有拥有合法的身份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,所有数据的签名信息。

其中,需要放在请求头的字段:appidtimestampnoncesignature

签名生成

请求参数的拼接

对各种类型的请求参数,先做如下拼接处理:

  • 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单项目的签名实现

实现可以分以下几步:

  1. 过滤器中替换自定义的缓存有body参数的HttpServletRequest
  2. 切面或者拦截器中,实现签名拦截
过滤器中替换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());
    }

}

参考文章

java接口签名(Signature)实现方案
开放API接口签名验证,让你的接口从此不再裸奔

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
禁止转载,如需转载请通过简信或评论联系作者。
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,390评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,821评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,632评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,170评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,033评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,098评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,511评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,204评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,479评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,572评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,341评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,213评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,576评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,893评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,171评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,486评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,676评论 2 335