接口签名实现

作者: 番茄精 | 来源:发表于2020-06-23 18:49 被阅读0次
    • 接口安全问题
    • 防止篡改
    • 防止重放
      • 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接口签名验证,让你的接口从此不再裸奔

    相关文章

      网友评论

        本文标题:接口签名实现

        本文链接:https://www.haomeiwen.com/subject/rqahfktx.html