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");
    }
}

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