使用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());}@Overridepublic BufferedReader getReader() throws IOException {return new BufferedReader(new InputStreamReader(getInputStream()));}@Overridepublic ServletInputStream getInputStream() throws IOException {final ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(body.getBytes());return new ServletInputStream() {@Overridepublic boolean isFinished() {return false;}@Overridepublic boolean isReady() {return false;}@Overridepublic void setReadListener(ReadListener readListener) {}@Overridepublic int read() {return byteArrayInputStream.read();}};}@Overridepublic String getParameter(String name) {String[] values = parameterMap.get(name);if (values != null && values.length > 0) {return values[0];}return null;}@Overridepublic Map<String, String[]> getParameterMap() {return parameterMap;}@Overridepublic Enumeration<String> getParameterNames() {return Collections.enumeration(parameterMap.keySet());}@Overridepublic 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*/@Overridepublic 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*/@Overridepublic 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 {@Overridepublic void init(FilterConfig filterConfig) throws ServletException {Filter.super.init(filterConfig);}@Overridepublic 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 {@Resourceprivate 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>*/@Overridepublic 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>*/@Overridepublic 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