使用sa-token+SpringBoot+拦截器实现API 接口参数签名
在涉及跨系统接口调用时,我们容易碰到以下安全问题:
1.请求身份被伪造。
2.请求参数被篡改。
3.请求被抓包,然后重放攻击。
1.引入 sa-token
sa-token官方文档:https://sa-token.cc/doc.html#/
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-spring-boot-starter</artifactId>
<version>1.35.0.RC</version>
</dependency>
2.配置密钥
请求发起端和接收端需要配置一个相同的秘钥,在 application.yml 中配置
# 开发接口密钥配置
sa-token:
sign:
# API 接口签名秘钥
secret-key: 8ba6126f-3921-4eca-8f1b-451aa38a563b
3.重写HttpServletRequestWrapper类
方便获取请求头的参数,包括@RequestBody注解接受的参数
package com.xhs.interceptor;
import org.apache.commons.io.IOUtils;
import javax.servlet.ReadListener;
import javax.servlet.ServletInputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Map;
/**
* @desc: 保存请求体参数的内容
* @projectName: java-tools-parent
* @author: xhs
* @date: 2023-8-27 027 19:06
* @version: JDK 1.8
*/
public class RequestWrapper extends HttpServletRequestWrapper {
/**
* 保存请求体参数
*/
private final String body;
/**
* 保存其他类型的参数
*/
private final Map<String, String[]> parameterMap;
/**
* 获取参数
*
* @param request request
* @throws IOException IOException
*/
public RequestWrapper(HttpServletRequest request) throws IOException {
super(request);
// 获取请求体参数
body = IOUtils.toString(request.getInputStream(), StandardCharsets.UTF_8);
// 获取其他类型的参数
parameterMap = new HashMap<>(request.getParameterMap());
}
@Override
public BufferedReader getReader() throws IOException {
return new BufferedReader(new InputStreamReader(getInputStream()));
}
@Override
public ServletInputStream getInputStream() throws IOException {
final ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(body.getBytes());
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() {
return byteArrayInputStream.read();
}
};
}
@Override
public String getParameter(String name) {
String[] values = parameterMap.get(name);
if (values != null && values.length > 0) {
return values[0];
}
return null;
}
@Override
public Map<String, String[]> getParameterMap() {
return parameterMap;
}
@Override
public Enumeration<String> getParameterNames() {
return Collections.enumeration(parameterMap.keySet());
}
@Override
public String[] getParameterValues(String name) {
return parameterMap.get(name);
}
/**
* 获取请求体参数
*
* @return String
*/
public String getBody() {
return body;
}
}
4.创建签名校验的拦截器 SignInterceptor
校验请求的参数是否有效
package com.xhs.interceptor;
import cn.dev33.satoken.sign.SaSignUtil;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.MethodParameter;
import org.springframework.http.HttpMethod;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Map;
/**
* @desc: 签名校验的拦截器
* @projectName: java-tools-parent
* @author: xhs
* @date: 2023-8-27 027 17:56
* @version: JDK 1.8
*/
@Slf4j
public class SignInterceptor implements HandlerInterceptor {
/**
* 创建一个签名校验的拦截器
*/
public SignInterceptor() {
}
/**
* 每次请求之前触发的方法
*
* @param request request
* @param response response
* @param handler handler
* @return boolean
*/
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 如果是OPTIONS请求,让其响应一个 200状态码,说明可以正常访问
if (HttpMethod.OPTIONS.toString().equals(request.getMethod())) {
response.setStatus(HttpServletResponse.SC_OK);
// 放行OPTIONS请求
return true;
}
// 保存传递过来的参数
Map<String, String> map = new HashMap<>(16);
// 在拦截器中获取处理方法的参数,并检查是否带有 @RequestBody 注解
HandlerMethod handlerMethod = (HandlerMethod) handler;
MethodParameter[] methodParameters = handlerMethod.getMethodParameters();
for (MethodParameter parameter : methodParameters) {
if (parameter.hasParameterAnnotation(RequestBody.class)) {
// 参数带有 @RequestBody 注解
// 获取请求体参数
RequestWrapper requestWrapper = new RequestWrapper(request);
String requestBody = requestWrapper.getBody();
if (StringUtils.hasLength(requestBody)) {
JSONObject jsonObject = JSONObject.parseObject(requestBody);
map = JSON.parseObject(jsonObject.toJSONString(), HashMap.class);
log.info("请求体参数map:{}", map);
}
} else {
// 参数不带 @RequestBody 注解
// 非请求体参数
Enumeration<String> parameterNames = request.getParameterNames();
// 遍历参数名,并获取对应的参数值
while (parameterNames.hasMoreElements()) {
String paramName = parameterNames.nextElement();
String paramValue = request.getParameter(paramName);
// 处理动态参数,如打印参数名和参数值
map.put(paramName, paramValue);
}
log.info("非请求体参数map:{}", map);
}
}
// 1、校验请求中的签名
SaSignUtil.checkParamMap(map);
return true;
}
}
5.使用拦截器
配置那些接口需要校验参数签名
package com.xhs.filter;
import com.xhs.interceptor.SignInterceptor;
import org.jetbrains.annotations.NotNull;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
/**
* @desc: 检查签名过滤器
* @projectName: java-tools-parent
* @author: xhs
* @date: 2023-8-27 027 15:17
* @version: JDK 1.8
*/
@Configuration
public class SignFilter implements WebMvcConfigurer {
/**
* 注册拦截器
*
* @param registry
* @return void
*/
@Override
public void addInterceptors(@NotNull InterceptorRegistry registry) {
// 校验规则为
registry.addInterceptor(new SignInterceptor())
//需要校验的接口
.addPathPatterns("/tools/getUser","/tools/getName")
// 不需要校验的接口
.excludePathPatterns();
}
}
6.创建HttpServletRequestFilter过滤器
解决@RequestBody注解接受的参数,校验完签名后报:
org.springframework.http.converter.HttpMessageNotReadableException: Required request body is missing:
异常
package com.xhs.filter;
import com.xhs.interceptor.RequestWrapper;
import org.springframework.stereotype.Component;
import org.springframework.web.multipart.support.StandardServletMultipartResolver;
import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
/**
* @desc: 过滤器
* 解决:在拦截器中获取body后,接口报错:Required request body is missing
* @projectName: java-tools-parent
* @author: xhs
* @date: 2023-8-27 027 20:07
* @version: JDK 1.8
*/
@Component
@WebFilter(filterName = "HttpServletRequestFilter", urlPatterns = "/")
public class HttpServletRequestFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
Filter.super.init(filterConfig);
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) servletRequest;
String contentType = request.getContentType();
String method = "multipart/form-data";
if (contentType != null && contentType.contains(method)) {
// 将转化后的 request 放入过滤链中
request = new StandardServletMultipartResolver().resolveMultipart(request);
}
request = new RequestWrapper((HttpServletRequest) servletRequest);
//获取请求中的流如何,将取出来的字符串,再次转换成流,然后把它放入到新request对象中
// 在chain.doFiler方法中传递新的request对象
if (request == null) {
filterChain.doFilter(servletRequest, servletResponse);
} else {
filterChain.doFilter(request, servletResponse);
}
}
}
7.创建生成签名的方法
7.1 controller层代码
package com.xhs.controller;
import com.xhs.message.ReturnResult;
import com.xhs.service.SignService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
import java.util.Map;
/**
* @desc: 生成签名
* @projectName: java-tools-parent
* @author: xhs
* @date: 2023-8-27 027 16:03
* @version: JDK 1.8
*/
@Slf4j
@RestController
public class SignController {
@Resource
private SignService signService;
/**
* 生成签名 参数拼接到url后面
*
* @param paramsMap 参数
* @return ReturnResult<Object>
*/
@PostMapping("/signGet")
public ReturnResult<Object> signGet(@RequestBody Map<String, Object> paramsMap) {
return signService.signGet(paramsMap);
}
/**
* 生成签名 JSON格式的参数
*
* @param paramsMap 参数
* @return ReturnResult<Object>
*/
@PostMapping("/signPost")
public ReturnResult<Object> signPost(@RequestBody Map<String, Object> paramsMap) {
return signService.signPost(paramsMap);
}
}
7.2 service层代码
package com.xhs.service;
import com.xhs.message.ReturnResult;
import java.util.Map;
/**
* @desc:
* @projectName: java-tools-parent
* @author: xhs
* @date: 2023-8-27 027 16:36
* @version: JDK 1.8
*/
public interface SignService {
/**
* 生成签名 GET请求方式
*
* @param paramsMap 参数
* @return ReturnResult<Object>
*/
ReturnResult<Object> signGet(Map<String, Object> paramsMap);
/**
* 生成签名 POST请求方式
*
* @param paramsMap 参数
* @return ReturnResult<Object>
*/
ReturnResult<Object> signPost(Map<String, Object> paramsMap);
}
7.3 service实现层代码
package com.xhs.service.impl;
import cn.dev33.satoken.sign.SaSignUtil;
import com.xhs.message.Result;
import com.xhs.message.ReturnResult;
import com.xhs.service.SignService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import java.util.Map;
/**
* @desc:
* @projectName: java-tools-parent
* @author: xhs
* @date: 2023-8-27 027 16:36
* @version: JDK 1.8
*/
@Slf4j
@Service
public class SignServiceImpl implements SignService {
/**
* 生成签名 POST请求方式
*
* @param paramsMap 参数
* @return ReturnResult<Object>
*/
@Override
public ReturnResult<Object> signGet(Map<String, Object> paramsMap) {
log.info("生成签名的入参-paramsMap:{}", paramsMap);
String signParams = SaSignUtil.addSignParamsAndJoin(paramsMap);
log.info("生成签名后的参数-signParams:{}", signParams);
return ReturnResult.build(Result.SUCCESS).setData(signParams);
}
/**
* 生成签名 POST请求方式
*
* @param paramsMap 参数
* @return ReturnResult<Object>
*/
@Override
public ReturnResult<Object> signPost(Map<String, Object> paramsMap) {
log.info("生成签名的入参-paramsMap:{}", paramsMap);
Map<String, Object> map = SaSignUtil.addSignParams(paramsMap);
log.info("生成签名后的参数-signParams:{}", map);
return ReturnResult.build(Result.SUCCESS).setData(map);
}
}
8.调用接口并校验签名是否合法
8.1 GET请求参数拼接到url后面
http://127.0.0.1:1000/tools/getName?name=admin×tamp=1693145776820&nonce=kaTqdadO4u04hZG0gekEIvXmeN5QZD8A&sign=4b5f414e24290ed7766c2d79910264a7
8.2 POST请求 使用@RequestBody接受参数
注意
使用@RequestBody接受参数需要创建过滤器将请求体内容传递给下一个处理器,否则会报错
org.springframework.http.converter.HttpMessageNotReadableException: Required request body is missing:
解决方法:参照 第五步”5.创建HttpServletRequestFilter过滤器“
9.源码地址
https://gitee.com/xhs101/java-tools-parent