Spring Boot接口驗籤(攔截器實現&解決Post請求body的輸入流只能讀取一次問題)

SpringBoot集成攔截器-接口簽名的統一驗證

  • 實現攔截器 繼承HandlerInterceptor
  • 攔截器註冊到spring容器 實現WebMvcConfigurer,添加攔截器即可

demo-web module添加攔截器實現類,接口驗籤使用MD5

package com.springboot.demo.web.interceptor;

import com.google.gson.Gson;
import com.springboot.demo.common.constants.Constants;
import com.springboot.demo.common.utils.MD5Utils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;
import org.springframework.web.servlet.HandlerInterceptor;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.BufferedReader;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;

/**
 * 攔截器 驗證簽名 目前只支持GET/POST請求
 *
 * @author zangdaiyang
 */
@Component
@Slf4j
public class SignAuthInterceptor implements HandlerInterceptor {

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws IOException {
        // 1、獲取請求的參數
        Map<String, String> params;
        String method = request.getMethod();
        if (Constants.HTTP_GET.equalsIgnoreCase(method)) {
            Map<String, String[]> paramsArr = request.getParameterMap();
            if (CollectionUtils.isEmpty(paramsArr)) {
                log.warn("Request for get method, param is empty, signature verification failed.");
                return false;
            }
            params = convertParamsArr(paramsArr);
        } else if (Constants.HTTP_POST.equalsIgnoreCase(method)) {
            // 此處讀取了request中的inputStream,因爲只能被讀取一次,後面spring框架無法讀取了,所以需要添加wrapper和filter解決流只能讀取一次的問題
            BufferedReader reader = request.getReader();
            if (reader == null) {
                log.warn("Request for post method, body is empty, signature verification failed.");
                return false;
            }
            params = new Gson().fromJson(reader, Map.class);
        } else {
            log.warn("Not supporting non-get or non-post requests, signature verification failed.");
            return false;
        }

        // 2、驗證簽名是否匹配
        boolean checkSign = params != null && params.getOrDefault(Constants.SIGN_KEY, "").equals(MD5Utils.stringToMD5(params));
        log.info("Signature verification ok: {}, URI: {}, method: {}, params: {}", checkSign, request.getRequestURI(), method, params);
        return checkSign;
    }

    private Map<String, String> convertParamsArr(Map<String, String[]> paramsArr) {
        Map<String, String> params = new HashMap<>();
        for (Map.Entry<String, String[]> entry : paramsArr.entrySet()) {
            params.put(entry.getKey(), entry.getValue()[0]);
        }
        return params;
    }
}

使用的常量類及MD5工具類

demo-common module中添加工具類

package com.springboot.demo.common.constants;

/**
 * 一般常量類
 */
public final class Constants {

    /**
     * 簽名加密前對應的key
     */
    public static final String SIGN_ORIGIN_KEY = "origin";

    /**
     * 簽名對應的key
     */
    public static final String SIGN_KEY = "sign";

    /**
     * post請求方法名
     */
    public static final String HTTP_POST = "POST";

    /**
     * get請求方法名
     */
    public static final String HTTP_GET = "GET";

    private Constants() {}
}

package com.springboot.demo.common.utils;

import com.google.gson.Gson;
import com.springboot.demo.common.constants.Constants;

import java.math.BigInteger;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.*;

public class MD5Utils {

    private static final String ALGORITHM_NAME = "md5";

    private static final String SECRET_KEY = "dfasuiyhkuhjk2t5290wouojjeerweeqwqdfd";

    private static final int RADIX = 16;

    private static final int LEN = 32;

    private static final String ADD_STR = "0";

    /**
     * 轉換成對應的MD5信息
     * @param paramMap
     * @return
     */
    public static String stringToMD5(Map<String,String> paramMap) {
        String covertString = covertParamMapToString(paramMap);
        byte[] secretBytes;
        try {
            secretBytes = MessageDigest.getInstance(ALGORITHM_NAME).digest(
                    covertString.getBytes());
        } catch (NoSuchAlgorithmException e) {
            throw new RuntimeException("No such MD5 Algorithm.");
        }
        String md5code = new BigInteger(1, secretBytes).toString(RADIX);
        for (int i = 0; i < LEN - md5code.length(); i++) {
            md5code = ADD_STR + md5code;
        }
        return md5code;
    }

    /**
     * 轉換成對應的string信息
     * @param paramMap
     * @return
     */
    private static String covertParamMapToString(Map<String,String> paramMap) {
        Set<String> sets = paramMap.keySet();
        List<String> valueList = new ArrayList<>();
        for (String key : sets) {
            if (key.equals(Constants.SIGN_KEY)) {
                continue;
            }
            String value = paramMap.get(key);
            valueList.add(value);
        }
        // 此處可以使用TreeMap
        Collections.sort(valueList); 
        String jsonString = new Gson().toJson(valueList);
        jsonString = jsonString + SECRET_KEY;
        return jsonString;
    }

}

Post請求body的輸入流只能讀取一次問題

驗證簽名時,Get請求可以直接使用request.getParameterMap()來獲取參數,之後進行簽名驗證即可;
如果是post請求,則需要分情況來看:
1、如果post請求使用的是form-data或者x-www-form-urlencoded方式,則也可以通過request.getParameterMap()來獲取參數;
2、如果是最常用的json形式(此處post請求場景只考慮此種請求形式的驗籤),則只能讀取post請求中的body(輸入流)。
在這裏插入圖片描述
但是,讀取了request中的輸入流,讀取後spring框架再獲取request中的body會因爲輸入流已經被讀取報錯。
可以通過創建包裝類和filter,每次讀取後重新賦值body中的輸入流來解決。

package com.springboot.demo.web.filter;

import lombok.extern.slf4j.Slf4j;

import javax.servlet.ReadListener;
import javax.servlet.ServletInputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import java.io.*;

/**
 * RequestWrapper 解決POST請求中的BODY參數不能重複讀取多次的問題
 *
 * @author zangdaiyang
 * @since 2019.11.08
 */
@Slf4j
public class RequestWrapper extends HttpServletRequestWrapper {
    private static final int BUFFER_LEN = 128;

    private final String body;

    public RequestWrapper(HttpServletRequest request) throws IOException {
        super(request);
        StringBuilder stringBuilder = new StringBuilder();
        BufferedReader bufferedReader = null;
        try {
            InputStream inputStream = request.getInputStream();
            if (inputStream != null) {
                bufferedReader = new BufferedReader(new InputStreamReader(inputStream));
                char[] charBuffer = new char[BUFFER_LEN];
                int bytesRead;
                while ((bytesRead = bufferedReader.read(charBuffer)) > 0) {
                    stringBuilder.append(charBuffer, 0, bytesRead);
                }
            }
        } catch (IOException ex) {
            throw ex;
        } finally {
            if (bufferedReader != null) {
                try {
                    bufferedReader.close();
                } catch (IOException ex) {
                    throw ex;
                }
            }
        }
        body = stringBuilder.toString();
    }

    @Override
    public ServletInputStream getInputStream() throws IOException {
        final ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(body.getBytes());
        ServletInputStream servletInputStream = new ServletInputStream() {
            public boolean isFinished() {
                return false;
            }
            public boolean isReady() {
                return false;
            }
            public void setReadListener(ReadListener readListener) {}
            public int read() throws IOException {
                return byteArrayInputStream.read();
            }
        };
        return servletInputStream;

    }
    @Override
    public BufferedReader getReader() throws IOException {
        return new BufferedReader(new InputStreamReader(this.getInputStream()));
    }

    public String getBody() {
        return this.body;
    }

}

過濾器實現

  • 實現過濾器 繼承Filter
  • 註冊filter 定義FilterRegistrationBean
package com.springboot.demo.web.filter;

import org.springframework.stereotype.Component;

import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;

/**
 * 過濾器 以便http post請求body輸入流可多次讀取
 *
 * @author zangdaiyang
 * @since 2019.11.08
 */
@Component
public class HttpServletFilter implements Filter {

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        ServletRequest requestWrapper = null;
        if (request instanceof HttpServletRequest) {
            requestWrapper = new RequestWrapper((HttpServletRequest) request);
        }
        if (requestWrapper == null) {
            chain.doFilter(request, response);
        } else {
            chain.doFilter(requestWrapper, response);
        }
    }

}

攔截器及過濾器註冊

攔截器、過濾器需要註冊到spring容器中才能生效

package com.springboot.demo.web.config;

import com.springboot.demo.web.filter.HttpServletFilter;
import com.springboot.demo.web.interceptor.SignAuthInterceptor;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

import javax.annotation.Resource;

/**
 * 配置攔截器與過濾器
 */
@Configuration
public class WebApplicationConfig implements WebMvcConfigurer {

    private static final String FILTER_PATH = "/*";

    @Resource
    private SignAuthInterceptor signAuthInterceptor;

    @Resource
    private HttpServletFilter httpServletFilter;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(signAuthInterceptor);
    }

    @Bean
    public FilterRegistrationBean registerFilter() {
        FilterRegistrationBean registration = new FilterRegistrationBean();
        registration.setFilter(httpServletFilter);
        registration.addUrlPatterns(FILTER_PATH);
        registration.setOrder(1);
        return registration;
    }
}

構建Controller及測試類

package com.springboot.demo.web.controller;

import com.springboot.demo.web.model.ResultInfo;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@Slf4j
public class DemoController {

    @GetMapping("/query")
    public ResultInfo query() {
        log.info("DemoController query.");
        return new ResultInfo();
    }

    @PostMapping("/clear")
    public ResultInfo clear() {
        log.info("DemoController clear.");
        return new ResultInfo();
    }

}

package com.springboot.demo.web;

import com.springboot.demo.common.constants.Constants;
import com.springboot.demo.common.utils.MD5Utils;
import com.springboot.demo.web.model.ResultInfo;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.util.Assert;
import org.springframework.web.client.RestTemplate;

import java.util.HashMap;
import java.util.Map;

@SpringBootTest
class TestController {

    private static final String URL_PREFIX = "http://localhost:10095/demo";

    private RestTemplate restTemplate = new RestTemplate();

    @Test
    public void testQuery() {
        ResultInfo resultInfo = restTemplate.getForObject(URL_PREFIX + "/query?origin=1&sign=f8a7e51875f63413479d561248398264", ResultInfo.class);
        Assert.isTrue(resultInfo.getCode() == 0, "Query Failed");
    }

    @Test
    public void testClear() {
        Map<String, String> request = new HashMap<>();
        request.put(Constants.SIGN_ORIGIN_KEY, "1");
        request.put(Constants.SIGN_KEY, MD5Utils.stringToMD5(request));
        ResultInfo resultInfo = restTemplate.postForObject(URL_PREFIX + "/clear", request, ResultInfo.class);
        Assert.isTrue(resultInfo.getCode() == 0, "Query Failed");
    }
}

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章