接口簽名實現

  • 接口安全問題
  • 防止篡改
  • 防止重放
    • timestamp+nonce方案
  • 簽名流程
  • 簽名規(guī)則
  • 簽名生成
    • 請求參數的拼接
    • 請求頭的拼接
    • 生成簽名
  • 實現
    • Spring Boot單項目的簽名實現
      • 過濾器中替換HttpServletRequest
      • 簽名攔截
    • 微服務架構中Zuul中實現簽名實現
  • 參考文章

接口安全問題

在為第三方系統(tǒng)提供接口的時候铛只,肯定要考慮接口數據的安全問題夯膀,比如數據是否被篡改童叠,數據是否已經過時祝高,請求是否唯一捣域,數據是否可以重復提交等問題沿侈。其中數據是否被篡改相對重要衣形。

防止篡改

請求攜帶參數appidsign翎苫,只有擁有合法的身份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分鐘)斩跌。

簽名流程

signature_flow.png

對服務端而言,攔截請求用AOP切面或者用攔截器都行捞慌,如果要對所有請求進行攔截耀鸦,可以直接攔截器處理(攔截器在切面之前,過濾器之后啸澡,具體在springmvc的dispather分發(fā)之后)袖订。

過濾器→攔截器→切面的順序:

filter&interceptor.png

簽名規(guī)則

  • 線下分配appid和appsecret,針對不同的調用方分配不同的appid和appsecret

  • 加入timestamp(時間戳)嗅虏,2分鐘內數據有效

  • 加入流水號nonce(防止重復提交)洛姑,至少為10位。針對查詢接口皮服,流水號只用于日志落地楞艾,便于后期日志核查参咙。 針對辦理類接口需校驗流水號在有效期內的唯一性,以避免重復請求硫眯。

  • 加入signature蕴侧,所有數據的簽名信息。

其中两入,需要放在請求頭的字段:appid 净宵、timestampnonce 裹纳、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單項目的簽名實現

實現可以分以下幾步:

  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協(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());
    }

}

參考文章

java接口簽名(Signature)實現方案
開放API接口簽名驗證套鹅,讓你的接口從此不再裸奔

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
禁止轉載站蝠,如需轉載請通過簡信或評論聯(lián)系作者。
  • 序言:七十年代末卓鹿,一起剝皮案震驚了整個濱河市菱魔,隨后出現的幾起案子,更是在濱河造成了極大的恐慌吟孙,老刑警劉巖澜倦,帶你破解...
    沈念sama閱讀 211,194評論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現場離奇詭異杰妓,居然都是意外死亡藻治,警方通過查閱死者的電腦和手機,發(fā)現死者居然都...
    沈念sama閱讀 90,058評論 2 385
  • 文/潘曉璐 我一進店門巷挥,熙熙樓的掌柜王于貴愁眉苦臉地迎上來桩卵,“玉大人,你說我怎么就攤上這事倍宾〕冢” “怎么了?”我有些...
    開封第一講書人閱讀 156,780評論 0 346
  • 文/不壞的土叔 我叫張陵高职,是天一觀的道長钩乍。 經常有香客問我,道長怔锌,這世上最難降的妖魔是什么寥粹? 我笑而不...
    開封第一講書人閱讀 56,388評論 1 283
  • 正文 為了忘掉前任,我火速辦了婚禮埃元,結果婚禮上涝涤,老公的妹妹穿的比我還像新娘。我一直安慰自己岛杀,他們只是感情好妄痪,可當我...
    茶點故事閱讀 65,430評論 5 384
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著楞件,像睡著了一般衫生。 火紅的嫁衣襯著肌膚如雪裳瘪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,764評論 1 290
  • 那天罪针,我揣著相機與錄音彭羹,去河邊找鬼。 笑死泪酱,一個胖子當著我的面吹牛派殷,可吹牛的內容都是我干的。 我是一名探鬼主播墓阀,決...
    沈念sama閱讀 38,907評論 3 406
  • 文/蒼蘭香墨 我猛地睜開眼毡惜,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了斯撮?” 一聲冷哼從身側響起经伙,我...
    開封第一講書人閱讀 37,679評論 0 266
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎勿锅,沒想到半個月后帕膜,有當地人在樹林里發(fā)現了一具尸體,經...
    沈念sama閱讀 44,122評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡溢十,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 36,459評論 2 325
  • 正文 我和宋清朗相戀三年垮刹,在試婚紗的時候發(fā)現自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片张弛。...
    茶點故事閱讀 38,605評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡荒典,死狀恐怖,靈堂內的尸體忽然破棺而出吞鸭,到底是詐尸還是另有隱情种蝶,我是刑警寧澤,帶...
    沈念sama閱讀 34,270評論 4 329
  • 正文 年R本政府宣布瞒大,位于F島的核電站,受9級特大地震影響搪桂,放射性物質發(fā)生泄漏透敌。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,867評論 3 312
  • 文/蒙蒙 一踢械、第九天 我趴在偏房一處隱蔽的房頂上張望酗电。 院中可真熱鬧,春花似錦内列、人聲如沸撵术。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,734評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽嫩与。三九已至寝姿,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間划滋,已是汗流浹背饵筑。 一陣腳步聲響...
    開封第一講書人閱讀 31,961評論 1 265
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留处坪,地道東北人根资。 一個月前我還...
    沈念sama閱讀 46,297評論 2 360
  • 正文 我出身青樓,卻偏偏與公主長得像同窘,于是被迫代替她去往敵國和親玄帕。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 43,472評論 2 348