基于SpringBoot实现HTTP请求签名验证机制
目录
- 概述
- 功能特性
- 核心实现解析
- 1. 主校验流程
- 2. 关键技术点
- 优化建议
- 注意事项
- 总结
- 完整代码
概述
在分布式系统交互中,API接口的安全性至关重要。本文将深入解析基于Spring Boot实现的HTTP请求签名验证机制,该方案支持GET/POST等多种请求方式,提供时效性验证和数据完整性保障。以下是核心实现的技术要点解析。
功能特性
- 多协议支持:完整覆盖GET、POST(jsON/Form-Data)等常见请求类型
- 时效控制:5分钟有效期的请求时间窗口
- 多重验证:公钥校验 + 签名验证 + 时间戳的三重防护机制
- 安全过滤:自动排除签名参数参与验签计算
- 编码兼容:自动处理URL特殊字符编码问题
核心实现解析
1. 主校验流程
public boolean verifySignature(HttpServletRequest request, HttpServletResponse response, GridAccountUser accountUser) { // 获取公钥配置 StateGridAccount gridAccount = stateGridService .getStateGridAccountById(accountUser.getAccountId()); // 请求类型路由 if(HttpMethod.POST.matches(request.getMethod())) { // 处理JSON/Form-Data类型 } else if(HttpMethod.GET.matches(request.getMethod())) { // 处理GET参数 } // 统一返回校验结果 }
2. 关键技术点
2.1 时间戳验证
private Boolean verifyTimestamp(String timestampStr) { long timestamp = Long.parseLong(timestampStr); long currentTime = System.currentTimeMillis(); return Math.abs(currentTime - timestamp) <= 300_000; // 5分钟有效期 }
- 防止重放攻击
- 要求客户端服务端时间同步
- 误差窗口可配置化建议
2.2 请求体处理
JSON请求处理:
String requestStr = getRequestBody(request); JSONObject jsonObject = JSON.parseobject(requestStr); map.remove("sign"); // 过滤签名参数
Form-Data处理:
Map<String, Object> formData = WebUtils.getParameteRSStartingWith(request, ""); formData.remove("sign");
GET请求处理:
Map<String, String[]> queryParams = request.getParameterMap(); signature = signature.replaceAll(" ", "+"); // 处理URL编码
2.3 签名验证
SignUtil.verifySignature( uri, // 请求路径 data.toString(), // 过滤后的参数 Long.parseLong(timestamp), signature, publicKey );
优化建议
1.参数序列化优化
- 当前同时使用Fastjson和Hutool的JSONUtil,建议统一JSON处理库
- 推荐使用Jackson进行标准化处理
2.异常处理增强
try { // 验签逻辑 } catch (NumberFormatException e) { log.error("时间戳格式异常: {}", timestampStr); throw new InvalidTimestampException(); } catch (SignatureException e) { log.warn("签名验证失败: {}", e.getMessage()); }
3.性能优化
- 添加公钥缓存机制(RedisCache)
- 采用连接池管理数据库查询
4.安全增强
- 添加重放攻击计数器
- 支持动态时间窗口配置
- 增加黑名单IP机制
注意事项
- 时间同步:确保NTP服务的时间同步
- 密钥管理:建议采用密钥轮换机制
- 空参数处理:需要明确空字符串和null的处理策略
- 编码一致性:统一使用UTF-8字符集
- 日志脱敏:敏感参数需要做日志过滤
总结
该实现方案为API接口安全提供了基础保障,在实际生产环境中可根据业务需求扩展以下功能:
- 增加流量限频控制
- 实现双向证书验证
- 支持多种哈希算法
- 添加OpenAPI规范支持
- 集成API管理平台
通过持续优化验签流程和完善监控机制,可以有效构建安全可靠的API网关体系。
完整代码
生产秘钥私钥工具类
package com.ASPire.datasynchron.common.utils; import Java.security.KeyPair; import java.security.KeyPairGenerator; import java.security.NoSuchAlgorithmException; import java.util.Base64; import java.util.Map; public class KeyGenExample { private static final String ALGORITHM = "RSA"; public static Map<String, String> generateKey() throws NoSuchAlgorithmException { // 1. 选择算法(RSA/EC/DSA) KeyPairGenerator keyGen = KeyPairGenerator.getInstance(ALGORITHM); keyGen.initialize(2048); // 密钥长度 // 2. 生成密钥对 KeyPair keyPair = keyGen.generateKeyPair(); // 3. 获取公私钥(Base64 编码打印) byte[] privateKeyBytes = keyPair.getPrivate().getEncoded(); byte[] publicKeyBytes = keyPair.getPublic().getEncoded(); String privateKey = Base64.getEncoder().encodeToString(privateKeyBytes); String publicKey = Base64.getEncoder().encodeToString(publicKeyBytes); Map<String, String> map = ApiKeyGenerator.generateApiCredentials(); map.put("private_key", privateKey); map.put("public_key", publicKey); return map; } public static void main(String[] args) { try { Map<String, String> stringStringMap = generateKey(); System.out.println(stringStringMap); } catch (NoSuchAlgorithmException e) { e.printStackTrace(); } } }
生成签名和验签方法
package com.aspire.datasynchron.common.utils; import cn.hutool.core.bean.BeanUtil; import cn.hutool.json.JSONUtil; import org.apache.commons.lang3.ArrayUtils; import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; import java.security.*; import java.security.spec.PKCS8EncodedKeySpec; import java.security.spec.X509EncodedKeySpec; import java.util.Base64; import java.util.HashMap; import java.util.Map; /** * System:NCC * Title:SignUtil.java * Description:添加描述信息 * @author: zhouxiaomin_a * @date:2018/6/21 17:00 * @version: 1.0.0.0 * Copyright (c) 2017 ASPire Tech. */ public class SignUtil { private static final String CHARSET = "UTF-8"; private static final String SIGNATURE_ALGORITHM = "SHA256withRSA"; private static final String KEY_ALGORITHM = "RSA"; // 通过传入私钥、数据和时间戳生成签名的方法 www.devze.com public static String generateSignature(String url, String data, long timestamp, String privateKeyStr) throws Exception { // 将数据和时间戳合并成一个字符串 String dataWithTimestamp = url + "|" + data + "|" + timestamp; // 通过传入的私钥字符串生成私钥 PrivateKey privateKey = getPrivateKeyFromString(privateKeyStr); // 创建签名对象,使用 SHA256withRSA 算法 Signature signature = Signature.getInstance(SIGNATURE_ALGORITHM); signature.initSign(privateKey); // 更新数据 signature.update(dataWithTimestamp.getBytes(StandardCharsets.UTF_8)); // 生成签名 byte[] signedData = signature.sign(); // 将签名转换为 Base64 编码的字符串 return Base64.getEncoder().encodeToString(signedData); } // 通过传入私钥字符串,生成私钥对象的方法 public static PrivateKey getPrivateKeyFromString(String privateKeyStr) throws Exception { // 将 Base64 编码的私钥字符串解码为字节数组 byte[] decodedKey = Base64.getDecoder().decode(privateKeyStr); // 使用 KeyFactory 生成私钥对象 KeyFactory keyFactory = KeyFactory.getInstance(KEY_ALGORITHM); PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(decodedKey); return keyFactory.generatePrivate(keySpec); } // 验证签名的方法 public static boolean verifySignature(String url, String data, Long timestamp, String signatureStr, String publicKeyStr) throws Exception { if (StringUtils.isEmpty(url) || StringUtils.isEmpty(data) || null == timestamp || StringUtils.isEmpty(signatureStr) || StringUtils.isEmpty(publicKeyStr)) { return false; } // 将数据和时间戳合并成一个字符串 String dataWithTimestamp = url + "|" + data + "|" + timestamp; // 通过传入的公钥字符串生成公钥 PublicKey publicKey = getPublicKeyFromString(publicKeyStr); // 创建签名对象,使用 SHA256withRSA 算法 Signature signature = Signature.getInstance(SIGNATURE_ALGORITHM); signature.initVerify(publicKey); // 更新数据 signature.update(dataWithTimestamp.getBytes(StandardCharsets.UTF_8)); // 将 Base64 编码的签名字符串转换为字节数组 byte[] signatureBytes = Base64.getDecoder().decode(signatureStr); // 验证签名 return signature.verify(signatureBytes); } // 通过传入公钥字符串,生成公钥对象的方法 public static PublicKey getPublicKeyFromString(String publicKeyStr) throws Exception { // 将 Base64 编码的公钥字符串解码为字节数组 byte[] decodedKey = Base64.getDecoder().decode(publicKeyStr); // 使用 KeyFactory 生成公钥对象 KeyFactory keyFactory = KeyFactory.getInstance(KEY_ALGORITHM); X509EncodedKeySpec keySpec = new X509EncodedKeySpec(decodedKey); return keyFactory.generatePublic(keySpec); } // 测试代码 public static void main(String[] args) throws Exception { // 获取公钥和私钥的 Base64 编码字符串 String privateKeyStr = "MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQC+3ZTH/VCeWaesY4muy7fOvqLLTX07QpfJVeG1KZKSCjeciih3VRQfMn1PsxL0elMUFIfRKGOtjS7o0+V3UqAwaJCozyxEMPw3OGKQ0bb2dCId/TCnjWkFpSOj1UKct/LdOT/CZ07Bu+8DCynjzHSxwATe2DYWZp8fHf9FAuYjPuXhkeFEkf+/TG5ObyDE4VqzHiTb4XmjkJCuhk98H5CLOCrWb+NiyHpbp/fPF865QzEG1n9/pyFKyMDsBgSxmx0iXDheR6ue/wotxnDvJgGqaG00kD88r/oU2M3xo581a9sF+v6fzNCldtF+MHJJVd4OhFcscahBySEhIUY9TZo/AgMBAAECggEAIh6TP7MBa+VEC5WZocUqHQvIJ0a5adQMNUIkgJGncXLhIRszg62SVMdeTlaJP2n0mwTWiKXLN9Wiup1SimObXjv7DCpI1AHbvHVYbWIH7oOxK6I8xd8KFKfCOMHhUAm0ISbgRnzYP9q8LdObj+zXOYVFeZ62AIgkzte6b9hGUqtWv4yGL3VTiki5TvzHpRPJoeEYHEk+zGhdCn9dr7GY+3mm3EfnNL69s78jWAdkib8fHQsgunOB4NiA4Gnn58Aphzw/SYio6mS5ieRXz4h9ng77UI4agkuU6QhYMdt+s+7YTs4Z6+iXnE3f0zHZJT/UA1GItcxnYuznl2X+vKWS3QKBgQD3L/lUfh5R2N40rB16cE4W75betT59TIjEW+/g5VY4WhZZXEmIi7HWcxoAYXeTp1/Ls2KrKWyAs5ys89lTmasJh2M8afS04mUTCtOO2/bsnUXc0g7P8M913X2fWME+uIVLXBbG2+0xfXGgLCtsgZro5IkDqrHO09SX8s02jOAbJQKBgQDFq5HkGsxJZzS7anFoaBnXxUK590bkIt4eIyOs5Z7AldppWIaT5uCR9TAq3N29rvxBPBwiRKIsghleTHtkvccImDECu6ztVubn1oaudvNnHPbUyi5rfxg+dAKUoLxX3nzniSYupyN94QDq+q/OPiTBopyAXxRmNfn8x3YGvOi0kwKBgQCv3D/E7x1fGa2tR66JR5EnHDn4JHZa6rJ7EPWuyTr4SI+R7+iY7toNOkKLdsx+DhxHbk6Ke6QoRKD5I1vA8JkQ5HOjrbZdYpyKWa99+dzJJnNn0UKcijTvJC+VyK1jlB+xJ8lEnX85MIhAbmxOfD7b5ovcQfrSrT6ZBDMf1kYyyQKBgHQq32NRyGr/B0N5S8rTGxTubceCphvezfCiMA4lKAYAS0qL5xM2pRXCJZubD4mxM7hWziXpdfF4R9ZeVkofKcBISM1VZExbPPpU3fPcHjGkGP93Do7IM4RIg1e7mtR9AaTEujbCrR4GRJbT2sv3Q3y0xwq+Veu3nwHKaveMv6mXAoGBAM0trI9b/k7oHS8z9FfETTeiXxac3puumRcGt3HmCISPmXxc4RWMvbMPUWSzm2KFdwtKEYMMxvSZcEG90rxM/Mh3Uhdj8Pdz2iwAnCghpwzAtp60N2yKxJyq80gRFywTlNp70VxDxYCFZ9Dugjzg7NT2JbhOdSjoZDP9FXg+konq"; String publicKeyStr = "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvt2Ux/1QnlmnrGOJrsu3zr6iy019O0KXyVXhtSmSkgo3nIood1UUHzJ9T7MS9HpTFBSH0ShjrY0u6NPld1KgMGiQqM8sRDD8NzhikNG29nQiHf0wp41pBaUjo9VCnLfy3Tk/wmdOwbvvAwsp48x0scAE3tg2FmafHx3/RQLmIz7l4ZHhRJH/v0xuTm8gxOFasx4k2+F5o5CQroZPfB+Qizgq1m/jYsh6W6f3zxfOuUMxBtZ/f6chSsjA7AYEsZsdIlw4Xkernv8KLcZw7yYBqmhtNJA/PK/6FNjN8aOfNWvbBfr+n8zQpXbRfjBySVXeDoRXLHGoQckhISFGPU2aPwIDAQAB"; String url = "/api/rest/demo/queryAlarmInfo"; // 要签名的数据和时间戳 String json = "{\"SPECIALTY\":8,\"NETWORKTYPE\":801,\"VENDORNAME\":\"华为\",\"NETYPE\":\"801\",\"ALARMTITLE\":\"\",\"ORG_EVENT_ID\":\"\"}"; Map map = JSONUtil.toBean(json, Map.class); String data = map.toString(); System.out.println(data); long timestamp = System.currentTimeMillis(); // 当前时间戳 System.out.println(timestamp); // 生成签名 String signature = generateSignature(url, data, timestamp, privateKeyStr); System.out.println("Generated Signature: " + signature); // 验证签名 boolean isVerified = SignUtil.verifySignature(url, data, timestamp, signature, publicKeyStr); System.out.println("Signature Verified: " + isVerified); System.out.println("---------------------------------------------------------"); String url2 = "/api/rest/demo/getUser"; Map<String, Object> params = new HashMap<>(); params.put("idcard", "123456"); long timestamp1 = System.currentTimeMillis(); // 当前时间戳 System.out.println(timestamp1); String data2 = params.toString(); System.out.println(data2); // 生成签名 String signature1 = generateSignature(url2, data2, timestamp1, privateKeyStr); System.out.println("Generated Signature1: " + signature1); // 验证签名 boolean isVerified1 = SignUtil.verifySignature(url2, data2, timestamp1, signature1, publicKeyStr); System.out.println("Signature isVerified1: " + isVerified1); System.out.prinMeBshUbaiAtln("---------------------------------------------------------"); String url3 = "/api/rest/demo/getIdCard"; Map<String, Object> params1 = new HashMap<>(); params1.put("name", "李四"); params1.put("age", "27"); String data3 = params1.toString(); System.out.println(data3); long timestamp2 = System.currentTimeMillis(); // 当前时间戳 System.out.println(timestamp2); // 生成签名 String signature2 = generateSignature(url3, data3, timestamp2, privateKeyStr); System.out.println("Generated signature2: " + signature2); // 验证签名 boolean isVerified2 = SignUtil.verifySignature(url3, data3, timestamp2, signature2, publicKeyStr); System.out.println("Signature isVerified2: " + isVerified2); } } javascript
自定义拦截器
package com.aspire.datasynchron.framework.interceptor; import com.aspire.datasynchron.common.core.domain.model.GridAccountUser; import com.aspire.datasynchron.common.utils.StringUtils; import com.aspire.datasynchron.framework.web.service.TokenService; import com.aspire.datasynchron.framework.web.service.VerifySignatureService; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.stereotype.Component; import org.springframework.web.servlet.HandlerInterceptor; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.nio.charset.StandardCharsets; @Slf4j @Component public class SignatureInterceptor implements HandlerInterceptor { private final TokenService tokenService; private final VerifySignatureService verifySignatureService; @Autowired public SignatureInterceptor(TokenService tokenService, VerifySignatureService verifySignatureService) { this.tokenService = tokenService; this.verifySignatureService = verifySignatureService; } @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String clientType = request.getHeader("clientType"); if (StringUtils.isNotEmpty(clientType)) { GridAccountUser accountUser = tokenService.getAccountUser(request); // 自定义验签逻辑 boolean verified = verifySignatureService.verifySignature(request, response, accountUser); if (!verified) { log.error("验签失败"); response.setStatus(HttpStatus.FORBIDDEN.value()); response.setContentType(MediaType.APPLICATION_JSON_VALUE); response.setCharacterEncoding(StandardCharsets.UTF_8.name()); // 构建错误响应 String errorMessage = "{\"error\": \"签名不合法\", \"message\": \"Signature is invalid.\"}"; response.getWriter().write(errorMessage); return false; } } return true; } }
验签实现类
package com.aspire.datasynchron.framework.web.service; import cn.hutool.json.JSONUtil; import com.alibaba.fastjson2.JSON; import com.alibaba.fastjson2.JSONObject; import com.aspire.datasynchron.common.core.domain.model.GridAccountUser; import com.aspire.datasynchron.common.core.redis.RedisCache; import com.aspire.datasynchron.common.utils.SignUtil; import com.aspire.datasynchron.stateGrid.domain.StateGridAccount; import com.aspire.datasynchron.stateGrid.domain.vo.StateGridAccountDetailsVO; import com.aspire.datasynchron.stateGrid.service.IStateGridAccountService; import com.fasterXML.jackson.databind.ObjectMapper; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpMethod; import org.springframework.http.MediaType; import org.springframework.stereotype.Service; import org.springframework.util.StringUtils; import org.springframework.web.util.ContentCachingRequestWrapper; import org.springframework.web.util.ContentCachingResponseWrapper; import org.springframework.web.util.WebUtils; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; import java.util.*; @Slf4j @Service public class VerifySignatureService { @Autowired private IStateGridAccountService stateGridService; public boolean verifySignature(HttpServletRequest request, HttpServletResponse response, GridAccountUser accountUser) { //查询公钥 StateGridAccount gridAccount = stateGridService.getStateGridAccountById(accountUser.getAccountId()); if (gridAccount == null || StringUtils.isEmpty(gridAccount.getPublicKey())) { log.error("账号[{}]公钥未配置", accountUser.getAccountId()); return false; } String publicKey = gridAccount.getPublicKey(); String uri = request.getRequestURI(); // 判断当前请求方式 if (HttpMethod.POST.name().equals(request.getMethod())) { // 判断是否是 JSON 格式的请求 if (MediaType.APPLICATION_JSON_VALUE.equals(request.getContentType())) { // 获取请求体的内容 String requestStr = getRequestBody(request); log.info("请求体内容:{}", requestStr); if (StringUtils.isEmpty(requestStr)) { return false; } JSONObject jsonObject = JSON.parseObject(requestStr); Map map = JSONUtil.toBean(requestStr, Map.class); map.remove("sign"); map.remove("timestamp"); String timestamp = jsonObject.getString("timestamp"); String signature = jsonObject.getString("sign"); // 验证时间戳 Boolean verifed = verifTimestamp(timestamp); if (!verifed) { return false; } try { boolean isValid = SignUtil.verifySignature(uri, map.toString(), Long.parseLong(timestamp), signature, publicKey); return isValid; } catch (Exception e) { log.error(e.getMessage()); return false; } } else if (request.getContentType() != null && request.getContentType().startsWith(MediaType.MULTIPART_FORM_DATA_VALUE)) { // 处理 form 表单数据格式 Map<String, Object> formData = WebUtils.getParametersStartingWith(request, ""); log.info("表单数据:{}", formData); String timestamp = (String) formData.get("timestamp"); String signature = (String) formData.get("sign"); formData.remove("sign"); android formData.remove("timestamp"); // 验证时间戳 Boolean verifed = verifTimestamp(timestamp); if (!verifed) { return false; } try { boolean isValid = SignUtil.verifySignature(uri, formData.toString(), Long.parseLong(timestamp), signature, publicKey); return isValid; } catch (Exception e) { throw new RuntimeException(e); } } } else if (HttpMethod.GET.name().equals(request.getMethod())) { // GET 请求的处理,获取查询参数进行验签 Map<String, String[]> queryParams = request.getParameterMap(); if (queryParams == null) { return false; } Map<String, String> filteredParams = new HashMap<>(); queryParams.forEach((key, values) -> { // 过滤掉 "sign" 和 "timestamp" 参数 if (!"sign".equals(key) && !"timestamp".equals(key)) { filteredParams.put(key, values.length > 0 ? values[0] : ""); } }); String timestamp = request.getParameter("timestamp"); String signature = request.getParameter("sign"); if (timestamp == null || signature == null) { return false; } // 验证时间戳 Boolean verifed = verifTimestamp(timestamp); if (!verifed) { return false; } //+ 符号通常会被转换为一个空格 (%20),这是因为在 URL 编码中,+ 符号表示空格 signature = signature.replaceAll(" ", "+"); boolean isValid = false; try { isValid = SignUtil.verifySignature(uri, filteredParams.toString(), Long.parseLong(timestamp), signature, publicKey); if (!isValid) { return false; // 返回 false 表示验签失败 } return true; // 返回 true 表示验签成功 } catch (Exception e) { log.error(e.getMessage()); return false; } } return false; // 默认返回 false,如果请求方法不支持 } /** * @throws * @MethodName verifTimestamp * @author zhouzihao * @param: timestampStr * @DateTime 2025年3月25日, 0025 下javascript午 06:13 * @return: java.lang.Boolean * @description:检查时间戳:确保客户端和服务端时间同步,误差在5分钟内。 */ private Boolean verifTimestamp(String timestampStr) { long timestamp = Long.parseLong(timestampStr); long currentTime = System.currentTimeMillis(); if (Math.abs(currentTime - timestamp) > 300000) { // 5分钟 log.error("验签失败:时间戳已过期"); return false; } return true; } /** * 读取请求体的内容 */ private static String getRequestBody(HttpServletRequest request) { InputStream in = null; StringBuffer sb = null; try { in = request.getInputStream(); BufferedReader br = new BufferedReader(new InputStreamReader(in, Charset.forName("UTF-8"))); sb = new StringBuffer(""); String temp; while ((temp = br.readLine()) != null) { sb.append(temp); } if (in != null) { in.close(); } if (br != null) { br.close(); } } catch (IOException e) { throw new RuntimeException(e); } return sb.toString(); } }
到此这篇关于基于SpringBoot实现HTTP请求签名验证机制的文章就介绍到这了,更多相关SpringBoot HTTP请求签名验证内容请搜索编程客栈(www.devze.com)以前的文章或继续浏览下面的相关文章希望大家以后多多支持编程客栈(www.devze.com)!
精彩评论