欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

Springboot学习笔记:添加自定义拦截器之验证签名

程序员文章站 2022-07-09 17:22:30
...

环境

window10
springboot:2.0
开发工具:IDEA 2020.1
jdk8+

前言

最近做的一个项目需要和第三方对接,既然要对接,那么在接口调取时,就需要去验证签名是否合法;

虽然每个公司的签名策略都不一样。但是万变不离其宗的就是它们都需要根据请求的body数据和url中带的数据进行签名技术;

比如:
url:localhost:3000/ajax/post/handler?appkey=test
请求体body

{
    "storeId": 1,
    "itemType": 10,
    "forms": [
        {
            "skuId": 106449,
            "itemId": 106449,
            "id": 7478,
            "parentId": 106449,
            "name": "重酒石酸间羟胺注射液",
            "productName": "0.9%氯化钠注射液100ml",
            "alias": "0.9%氯化钠注射液100ml",
            "aliasPinyin": "lhnzsylhnzsy",
            "cost": 425,
            "price": 425
        }
    ]
}

签名基本都需要用到(上面的例子而言)appkey和请求体body来进行签名计算;

也就是说:需要在拦截器中获取到这两类数据;

自定义拦截器的类

这里需要去继承HttpServletRequestWrapper类。

SignInterceptor 类:

import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

@Component
public class SignInterceptor implements HandlerInterceptor {

    @Override
    public boolean preHandle(HttpServletRequest request,
                             HttpServletResponse response, Object handler) throws Exception {
        RequestWrapper requestWrapper = new RequestWrapper(request);
        String body = requestWrapper.getBody();
        String appkey = request.getParameter("appkey");
        String queryString = request.getQueryString();
        System.out.println("拦截器获取到的url参数" + appkey);
        System.out.println("拦截器获取到body内容:" + body);
        System.out.println("拦截器获取到url问号后面的参数:" + queryString);
        return true;
    }
}

配置拦截器

这里可以有两种方式来做:
① 继承WebMvcConfigurer类,并重写addInterceptors方法来添加自定义的拦截器
② 继承WebMvcConfigurationSupport类,并重写方法来添加自定义的拦截器;

以前WebMvcConfigurerAdapter也可以,但是WebMvcConfigurerAdapter 已经属于过时类,不推荐使用,其替代方案就是方法②

Q:WebMvcConfigurer和WebMvcConfigurerAdapter有什么区别呢?
A:WebMvcConfigurationSupport–>不需要返回逻辑视图,可以选择继承此类
WebMvcCofigurer–>返回逻辑视图,可以选择实现此方法,重写addInterceptor方法。

WebConfigurer 类:

import com.xingren.wms.admin.SignInterceptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class WebConfigurer implements WebMvcConfigurer {

    @Autowired
    private SignInterceptor signInterceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(signInterceptor).addPathPatterns("/ajax/post/handler");
    }
}

其实基本已经配置完了,但是目前这样的配置是有问题;
我们知道拦截器调用完毕后,接着就是进入Controller层,而从HttpServletRequest中获取数据,其实就是从流中获取数据(字节流);
在拦截器中从流中获取数据后,controller层就获取不到了,因为流是一次性的,具体来说就是,程序从流中获取数据,是通过偏移量pos来获取的;刚开始pos指向的是0位置,一旦从流中读取数据,pos就会发生改变;Controller层再去读取时,流可能已经被拦截器读取完毕导致不能再读了;
类似还有stream()方法,一旦读取完毕后,就不能再读,只能重新再生成流;

解决办法:
把拦截器中读取到的数据先保存起来,再填充回request中,保证后续流程也能读取到数据;

RequestWrapper 类:

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.StandardCharsets;

public class RequestWrapper extends HttpServletRequestWrapper {

    private final String body;

    public RequestWrapper(HttpServletRequest request) {
        super(request);
        body = getBodyString(request);
    }

    /**
     * 获取请求Body
     */
    public String getBodyString(final ServletRequest request) {
        StringBuilder sb = new StringBuilder();
        InputStream inputStream = null;
        BufferedReader reader = null;
        try {
            inputStream = cloneInputStream(request.getInputStream());
            reader = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.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();
    }

    /**
     * Description: 复制输入流
     */
    public InputStream cloneInputStream(ServletInputStream inputStream) {
        ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
        byte[] buffer = new byte[1024];
        int len;
        try {
            while ((len = inputStream.read(buffer)) > -1) {
                byteArrayOutputStream.write(buffer, 0, len);
            }
            byteArrayOutputStream.flush();
        }
        catch (IOException e) {
            e.printStackTrace();
        }
        return new ByteArrayInputStream(byteArrayOutputStream.toByteArray());
    }


	/**
	 * 这段代码就是将request的数据又重新写回流中,
	 *  后续需要调用getInputStream()方法时,就能获取到数据
	 *
	 */
    @Override
    public ServletInputStream getInputStream() throws IOException {
    	// 这一步很关键  保证后续操作能获取到流数据
        final ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(body.getBytes(StandardCharsets.UTF_8));
        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 byteArrayInputStream.read();
            }
        };
    }

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

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

上面我们是继承HttpServletRequestWrapper类来保存从流中读取的数据,并重写了getInputStream()方法,但是servlet使用的是HttpServletRequest;如果保证拦截器的后续调用链能调用到HttpServletRequestWrapper类中的getInputStream()方法呢?

这个时候,我们还需要重写Filter类中的doFilter方法:

RepeatedlyReadFilter

import com.xingren.wms.admin.RequestWrapper;

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


@WebFilter(urlPatterns = "/*", filterName = "RepeatedlyReadFilter")
public class RepeatedlyReadFilter implements Filter {
    @Override
    public void init(FilterConfig filterConfig) throws ServletException {}

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain) throws IOException, ServletException {
        ServletRequest requestWrapper = null;
        if(request instanceof HttpServletRequest) {
            requestWrapper = new RequestWrapper((HttpServletRequest) request);
        }
        if(requestWrapper == null) {
            filterChain.doFilter(request, response);
        } else {
        	// 保证后续调用链能调用我们刚刚重写的getInputStream()方法来获取流数据
            filterChain.doFilter(requestWrapper, response);
        }
    }

    @Override
    public void destroy() {}
}

测试

我的controller:

@ApiOperation(value = "测试拦截器", notes = "测试拦截器")
@RequestMapping(value = "/ajax/post/handler", method = RequestMethod.POST)
@ResponseBody
public JsonResult<Boolean> postHandle(@Valid @RequestBody
                               ConfirmStockForm confirmStockForm) {
    System.out.println(confirmStockForm);
    return JsonResult.ok();
}

根据上面的url和请求体,我们运行看下结果:

Springboot学习笔记:添加自定义拦截器之验证签名

拦截器获取到的url参数test
拦截器获取到body内容:
{
    "storeId":1,
    "itemType":10,
    "forms":[
        {
            "skuId":106449,
            "itemId":106449,
            "id":7478,
            "parentId":106449,
            "name":"重酒石酸间羟胺注射液",
            "productName":"0.9%氯化钠注射液100ml",
            "alias":"0.9%氯化钠注射液100ml",
            "aliasPinyin":"lhnzsylhnzsy",
            "cost":425,
            "price":425
        }
    ]
}

总结

1、自定义拦截器一般用于鉴权、日志、签名验证等场景;
2、拦截器获取数据时,需要注意的问题;就像流水一样,用完了,就没了,得想办法保存好数据,让后面的调用链也能获取到流中的数据;

参考地址:

springboot 拦截器校验签名后controller就获取不到数据

SpringBoot拦截器获取Request的body数据

https://blog.csdn.net/a704397849/article/details/97267572

WebMvcConfigurationSupport和WebMvcConfigurer的区别