SpringBoot中,接口签名,通用方案,以确保接口的安全性

devtools/2024/10/9 4:28:36/

1. 为什么需要接口签名?

  • 接口签名目的:防止第三方伪造请求。
  • 请求伪造:未经授权的第三方构造合法用户的请求来执行不希望的操作。
  • 转账接口示例:展示了如果接口没有安全措施,第三方可以轻易伪造请求,例如将资金从一个账户转移到另一个账户。

2. 如何实现接口签名?

  • 引入密钥:接口调用方和服务提供方之间共享一个密钥(secretKey),此密钥必须保密。
  • 签名算法:使用密钥和请求体的内容通过MD5算法生成签名。
  • 携带签名:客户端在请求头中附加生成的签名。
  • 服务端校验:服务器接收到请求后,使用同样的算法和密钥重新计算签名并与请求中提供的签名比较,如果不一致,则拒绝请求。

3. 防止请求伪造

  • 请求伪造解决办法:通过接口签名机制,第三方不知道密钥,因此无法正确生成匹配的签名,请求会被服务器拒绝。

4. 防止请求重放

  • 请求重放定义:攻击者截获合法请求后重新发送以达到重复执行的效果。
  • 解决请求重放的办法:引入随机字符串(nonce)和时间戳(timestamp)。nonce用来确保每个请求只能被使用一次,存储在Redis中并在一段时间后过期;时间戳用来限制请求的有效时间范围。





具体实现

1 整合springboot+redis环境

2 pom.xml

<dependency><groupId>cn.hutool</groupId><artifactId>hutool-all</artifactId><version>5.8.2</version>
</dependency>

3 yml配置

//redis 相关的配置省略了//秘钥,要保密
secret-key: b0e8668b-bcf2-4d73-abd4-893bbc1c6079

4 类ReusableBodyRequestWrapper,该类用于包装HttpServletRequest,以便在读取请求体后仍可重复读取

import org.apache.tomcat.util.http.fileupload.IOUtils;
import javax.servlet.ReadListener;
import javax.servlet.ServletInputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import java.io.*;/*** 该类用于包装HttpServletRequest,以便在读取请求体后仍可重复读取*/
public class ReusableBodyRequestWrapper extends HttpServletRequestWrapper {//参数字节数组,用于存储请求体的字节数据private byte[] requestBody;//Http请求对象private HttpServletRequest request;/*** 构造函数,初始化包装类* @param request 原始HttpServletRequest对象* @throws IOException 如果读取请求体时发生IO错误*/public ReusableBodyRequestWrapper(HttpServletRequest request) throws IOException {super(request);this.request = request;}/*** 重写getInputStream方法,实现请求体的重复读取* @return 包含请求体数据的ServletInputStream对象* @throws IOException 如果读取请求体时发生IO错误*/@Overridepublic ServletInputStream getInputStream() throws IOException {/*** 每次调用此方法时将数据流中的数据读取出来,然后再回填到InputStream之中* 解决通过@RequestBody和@RequestParam(POST方式)读取一次后控制器拿不到参数问题*///仅当requestBody未初始化时,从请求中读取并存储到requestBodyif (null == this.requestBody) {ByteArrayOutputStream baos = new ByteArrayOutputStream();IOUtils.copy(request.getInputStream(), baos);this.requestBody = baos.toByteArray();}//创建一个 ByteArrayInputStream 对象,用于重复读取requestBodyfinal ByteArrayInputStream bais = new ByteArrayInputStream(requestBody);return new ServletInputStream() {@Overridepublic boolean isFinished() {//始终返回false,表示数据流未完成return false;}@Overridepublic boolean isReady() {//始终返回false,表示数据流未准备好return false;}@Overridepublic void setReadListener(ReadListener listener) {//不执行任何操作,因为该数据流不支持异步操作}@Overridepublic int read() {//从ByteArrayInputStream中读取数据return bais.read();}};}/*** 获取请求体的字节数组* @return 请求体的字节数组*/public byte[] getRequestBody() {return requestBody;}/*** 重写getReader方法,返回一个基于getInputStream的BufferedReader* @return 包含请求体数据的BufferedReader对象* @throws IOException 如果读取请求体时发生IO错误*/@Overridepublic BufferedReader getReader() throws IOException {//基于getInputStream创建BufferedReaderreturn new BufferedReader(new InputStreamReader(this.getInputStream()));}
}

5 SignatureVerificationFilter类,签名验证过滤器,用于校验请求的合法性

import cn.hutool.crypto.digest.DigestUtil;
import cn.hutool.json.JSONUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Component;
import org.springframework.util.StreamUtils;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.TimeUnit;/*** 签名验证过滤器,用于校验请求的合法性*/
@Order(Ordered.HIGHEST_PRECEDENCE)
@WebFilter(urlPatterns = "/**", filterName = "SignatureVerificationFilter")
@Component
public class SignatureVerificationFilter extends OncePerRequestFilter {public static Logger logger = LoggerFactory.getLogger(SignatureVerificationFilter.class);@Overrideprotected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {// 对request进行包装,支持重复读取bodyReusableBodyRequestWrapper requestWrapper = new ReusableBodyRequestWrapper(request);// 校验签名if (this.verifySignature(requestWrapper, response)) {filterChain.doFilter(requestWrapper, response);}}@Autowiredprivate RedisTemplate<String, String> redisTemplate;// 签名秘钥@Value("${secret-key}")private String secretKey;/*** 校验签名** @param request  HTTP请求* @param response HTTP响应* @return 签名验证结果* @throws IOException 如果读取请求体失败*/public boolean verifySignature(HttpServletRequest request, HttpServletResponse response) throws IOException {// 签名String sign = request.getHeader("X-Sign");// 随机数String nonce = request.getHeader("X-Nonce");// 时间戳String timestampStr = request.getHeader("X-Timestamp");if (!StringUtils.hasText(sign) || !StringUtils.hasText(nonce) || !StringUtils.hasText(timestampStr)) {this.write(response, "参数错误");return false;}// timestamp 10分钟内有效long timestamp = Long.parseLong(timestampStr);long currentTimestamp = System.currentTimeMillis() / 1000;if (Math.abs(currentTimestamp - timestamp) > 600) {this.write(response, "请求已过期");return false;}// 防止请求重放,nonce只能用一次,放在redis中,有效期 20分钟String nonceKey = "SignatureVerificationFilter:nonce:" + nonce;if (!this.redisTemplate.opsForValue().setIfAbsent(nonceKey, "1", 20, TimeUnit.MINUTES)) {this.write(response, "nonce无效");return false;}// 请求体String body = StreamUtils.copyToString(request.getInputStream(), StandardCharsets.UTF_8);// 需要签名的数据:secretKey+noce+timestampStr+body// 校验签名String data = String.format("%s%s%s%s", this.secretKey, nonce, timestampStr, body);if (!DigestUtil.md5Hex(data).equals(sign)) {write(response, "签名有误");return false;}return true;}/*** 向客户端写入响应信息** @param response HTTP响应* @param msg      响应信息* @throws IOException 如果写入失败*/private void write(HttpServletResponse response, String msg) throws IOException {response.setContentType(MediaType.APPLICATION_JSON_VALUE);response.setCharacterEncoding(StandardCharsets.UTF_8.name());response.getWriter().write(JSONUtil.toJsonStr(msg));}
}

6 自己写的一个生成签名的工具类,可选项,因为在实现中,应该是前台传参或代码里写的,这是只是方便测试调度调试

import cn.hutool.crypto.digest.DigestUtil;
import org.springframework.util.StringUtils;
import java.util.UUID;public class SignatureUtil {/*** 生成签名** @param body      请求体* @param secretKey 密钥* @param nonce     随机数* @param timestamp 时间戳* @return 签名*/public static String generateSignature(String body, String secretKey, String nonce, String timestamp) {if (!StringUtils.hasText(body) || !StringUtils.hasText(secretKey) || !StringUtils.hasText(nonce) || !StringUtils.hasText(timestamp)) {throw new IllegalArgumentException("参数不能为空");}// 按照 secretKey + nonce + timestamp + body 的顺序拼接字符串String data = String.format("%s%s%s%s", secretKey, nonce, timestamp, body);System.out.println("data = " + data);// 使用MD5算法计算签名String sign = DigestUtil.md5Hex(data);return sign;}public static void main(String[] args) {// 示例参数String body = "{\n" +"  \"fromAccountId\": \"张三\",\n" +"  \"toAccountId\": \"李四\",\n" +"  \"transferPrice\": 100\n" +"}";//秘钥String secretKey = "b0e8668b-bcf2-4d73-abd4-893bbc1c6079";// 随机数String nonce = UUID.randomUUID().toString().replace("-", "");// 时间戳long timestamp = System.currentTimeMillis() / 1000;// 生成签名String sign = generateSignature(body, secretKey, nonce, String.valueOf(timestamp));// 输出生成的签名System.out.println("X-Sign: " + sign);System.out.println("X-Nonce: " + nonce);System.out.println("X-Timestamp: " + timestamp);}
}

7 写一个接口,用于调用

import lombok.*;
import java.math.BigDecimal;@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class TransferRequest {//付款人账户idprivate String fromAccountId;//收款人账号idprivate String toAccountId;//转账金额private BigDecimal transferPrice;
}///import cn.hutool.json.JSONUtil;
import com.example.demo_26.dto.TransferRequest;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;@RestController
@Slf4j
public class AccountController {@RequestMapping("/account/transfer")public Object transfer(@RequestBody TransferRequest request) {log.info("转账成功:{}", JSONUtil.toJsonStr(request));return "转账成功";}
}

8 测试
最后的效果,只能发一次请求,重复发送请求,就会失败,需要用新的随机数,时间戳,生成新的签名才可以


http://www.ppmy.cn/devtools/123188.html

相关文章

springboot自动配置

自动配置的核心就在SpringBootApplication注解上&#xff0c;SpringBootApplication这个注解 底层包含了3个注解&#xff0c;分别是&#xff1a; SpringBootConfiguration ComponentScan EnableAutoConfiguration EnableAutoConfiguration这个注解才是自动配置的核心,它 封…

Android2024.2.1升级错误

提示 Gradle 版本不兼容&#xff0c;升级后就报错了 。 1.gradle安装包镜像 //distributionUrlhttps\://services.gradle.org/distributions/gradle-8.5-bin.zip distributionUrlhttps://mirrors.cloud.tencent.com/gradle/gradle-8.5-bin.zip //镜像 2. Build报错&#xff1a…

基于 Seq2Seq 的中英文翻译项目(pytorch)

项目简介 本项目旨在使用 PyTorch 构建一个基于 Seq2Seq&#xff08;编码器-解码器架构&#xff09;的中英文翻译模型。我们将使用双语句子对的数据进行训练&#xff0c;最终实现一个能够将英文句子翻译为中文的模型。项目的主要步骤包括&#xff1a; 数据预处理&#xff1a;…

[ESP32]ESP-IDF使用组件添加U8g2图形库

U8g2 在ESP32使用u8g2的时候可以使用添加component的方式进行, 由于官方的component库没有, 这里我找到了一个可以使用的github库, 使用git的方式进行添加这一个库 具体的原理可以看[官方手册](https://docs.espressif.com/projects/esp-idf/zh_CN/stable/esp32/api-guides/to…

记HttpURLConnection下载图片

目录 一、示例代码1 二、示例代码2 一、示例代码1 import java.io.*; import java.net.HttpURLConnection; import java.net.URL;public class Test {/*** 下载图片*/public void getNetImg() {InputStream inStream null;FileOutputStream fOutStream null;try {// URL 统…

如何使用ssm实现基于Java的民宿预订管理系统的设计与实现

TOC ssm773基于Java的民宿预订管理系统的设计与实现jsp 绪论 1.1课题研究背景意义 随着科技的发展&#xff0c;计算机的应用&#xff0c;人们的生活方方面面都和互联网密不可分。计算机的普及使得人们的生活更加方便快捷&#xff0c;网络也遍及到我们生活的每个角落&#x…

面试题:Redis(一)

1. redis是单线程还是多线程&#xff1f; 2. IO多路复用听说过么&#xff1f; 3. Redis为什么快&#xff1f; 1. Redis是单线程还是多线程&#xff1f; 版本不同&#xff0c;Redis基于的架构也不同&#xff0c;所以单单问是单还是多线程并不严谨 3.x 之前 redis都是单线程 4.x …

Linux文件属性

Linux 文件基本属性 为了保护系统的安全性&#xff0c;Linux 系统对不同的用户访问同一文件&#xff08;包括目录文件&#xff09;的权限做了不同的规定。 在 Linux 中我们通常使用以下两个命令来修改文件或目录的所属用户与权限&#xff1a; - chown (change owner) &#…