Springboot学习笔记:添加自定义拦截器之验证签名
环境
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和请求体,我们运行看下结果:
拦截器获取到的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就获取不到数据
上一篇: DotNetCore会话探索篇
下一篇: pycharm解决无法切换中文输入法问题