Springboot实现一个接口加密

首先来看效果

image-20250713141307986

这个主要是为了防止篡改请求的。

我们这里采用的是一个AOP的拦截,在有需要这样的接口上添加了加密处理。

下面是一些功能

防篡改HMAC-SHA256 参数签名密钥仅客户端 & 服务器持有
防重放秒级时间戳 + 有效窗口校验默认允许 ±5 分钟
防窃听AES/CBC/PKCS5Padding 加密业务体对称密钥 16/24/32 字符
最小侵入Spring AOP + 自定义注解@SecureApi 一行即可启用

前后端交互流程

  1. 前端:在请求拦截器里自动
    • 生成 timestamp
    • 将业务 JSON → AES 加密得到 data
    • 按字典序拼接 timestamp=data,用 HMAC-SHA256 生成 sign
  2. 后端切面:仅拦截被 @SecureApi 标记的方法/类
    • 解析三字段 → 校验时间窗口
    • 移除 sign 再验签
    • 成功后解密 data → 注入 request.setAttribute("secureData", plaintext)

源码部分

首先是定义一个注解。

/*** 在 Controller 方法或类上添加该注解后,将启用参数签名、时间戳校验和 AES 解密校验。*/
@Target({ ElementType.METHOD, ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface SecureApi {
}

最主要的拦截器

package com.xiaou.secure.aspect;import com.xiaou.secure.exception.SecureException;
import com.xiaou.secure.properties.SecureProperties;
import com.xiaou.secure.util.AESUtil;
import com.xiaou.secure.util.SignUtil;
import jakarta.servlet.http.HttpServletRequest;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;import java.io.BufferedReader;
import java.io.IOException;
import java.time.Instant;
import java.util.*;
import java.util.stream.Collectors;/*** 安全校验切面*/
@Aspect
@Component
public class SecureAspect {private static final Logger log = LoggerFactory.getLogger(SecureAspect.class);@Autowiredprivate SecureProperties properties;@Around("@annotation(com.xiaou.secure.annotation.SecureApi)")public Object around(ProceedingJoinPoint pjp) throws Throwable {ServletRequestAttributes attrs = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();if (attrs == null) {return pjp.proceed();}HttpServletRequest request = attrs.getRequest();Map<String, String> params = extractParams(request);// 1. 时间戳校验validateTimestamp(params.get("timestamp"));// 2. 签名校验validateSign(params);// 3. AES 解密 data 字段if (params.containsKey("data")) {String plaintext = AESUtil.decrypt(params.get("data"), properties.getAesKey());// 把解密后的内容放到 request attribute,方便业务层读取request.setAttribute("secureData", plaintext);}return pjp.proceed();}private Map<String, String> extractParams(HttpServletRequest request) throws IOException {Map<String, String[]> parameterMap = request.getParameterMap();Map<String, String> params = new HashMap<>();parameterMap.forEach((k, v) -> params.put(k, v[0]));// 如果没有参数,但可能是 JSON body,需要读取 bodyif (params.isEmpty() && request.getContentType() != null&& request.getContentType().startsWith("application/json")) {String body = readBody(request);if (body != null && !body.isEmpty()) {try {com.fasterxml.jackson.databind.ObjectMapper mapper = new com.fasterxml.jackson.databind.ObjectMapper();Map<String, Object> jsonMap = mapper.readValue(body, Map.class);jsonMap.forEach((k, v) -> params.put(k, v == null ? null : v.toString()));} catch (Exception e) {// 回退到原始 & 分隔的解析方式,兼容 x-www-form-urlencoded 字符串Arrays.stream(body.split("&")).forEach(kv -> {String[] kvArr = kv.split("=", 2);if (kvArr.length == 2) {params.put(kvArr[0], kvArr[1]);}});}}}return params;}private String readBody(HttpServletRequest request) throws IOException {StringBuilder sb = new StringBuilder();try (BufferedReader reader = request.getReader()) {String line;while ((line = reader.readLine()) != null) {sb.append(line);}}return sb.toString();}private void validateTimestamp(String timestampStr) {if (timestampStr == null) {throw new SecureException("timestamp missing");}long ts;try {ts = Long.parseLong(timestampStr);} catch (NumberFormatException e) {throw new SecureException("timestamp invalid");}long now = Instant.now().getEpochSecond();if (Math.abs(now - ts) > properties.getAllowedTimestampOffset()) {throw new SecureException("timestamp expired");}}private void validateSign(Map<String, String> params) {String sign = params.remove("sign");if (sign == null) {throw new SecureException("sign missing");}// 排序Map<String, String> sorted = params.entrySet().stream().sorted(Map.Entry.comparingByKey()).collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue, (a, b) -> b, LinkedHashMap::new));String expected = SignUtil.sign(sorted, properties.getSignSecret());if (!Objects.equals(expected, sign)) {throw new SecureException("sign invalid");}}
}

配置方面:

springboot自动配置

@Configuration
@ConditionalOnClass(WebMvcConfigurer.class)
@AutoConfigureAfter(name = "org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration")
public class SecureAutoConfiguration {@Bean@ConditionalOnMissingBeanpublic SecureProperties secureProperties() {return new SecureProperties();}
}

动态配置 当然也可以用静态的

/*** 安全模块配置*/
@ConfigurationProperties(prefix = "secure")
public class SecureProperties {/*** AES 密钥(16/24/32 位)*/// 默认 16 字符,避免 InvalidKeyExceptionprivate String aesKey = "xiaou-secure-123";/*** 签名密钥*/private String signSecret = "xiaou-sign-secret";/*** 允许的时间差 (秒),默认 300 秒*/private long allowedTimestampOffset = 300;public String getAesKey() {return aesKey;}public void setAesKey(String aesKey) {this.aesKey = aesKey;}public String getSignSecret() {return signSecret;}public void setSignSecret(String signSecret) {this.signSecret = signSecret;}public long getAllowedTimestampOffset() {return allowedTimestampOffset;}public void setAllowedTimestampOffset(long allowedTimestampOffset) {this.allowedTimestampOffset = allowedTimestampOffset;}
}

工具类:

package com.xiaou.secure.util;import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.util.Base64;/*** AES/CBC/PKCS5Padding 工具类*/
public class AESUtil {private static final String AES_CBC_PKCS5 = "AES/CBC/PKCS5Padding";private static final String AES = "AES";private AESUtil() {}public static String encrypt(String data, String key) {try {Cipher cipher = Cipher.getInstance(AES_CBC_PKCS5);SecretKeySpec skeySpec = new SecretKeySpec(key.getBytes(StandardCharsets.UTF_8), AES);IvParameterSpec iv = new IvParameterSpec(key.substring(0, 16).getBytes(StandardCharsets.UTF_8));cipher.init(Cipher.ENCRYPT_MODE, skeySpec, iv);byte[] encrypted = cipher.doFinal(data.getBytes(StandardCharsets.UTF_8));return Base64.getEncoder().encodeToString(encrypted);} catch (Exception e) {throw new RuntimeException("AES encrypt error", e);}}public static String decrypt(String cipherText, String key) {try {Cipher cipher = Cipher.getInstance(AES_CBC_PKCS5);SecretKeySpec skeySpec = new SecretKeySpec(key.getBytes(StandardCharsets.UTF_8), AES);IvParameterSpec iv = new IvParameterSpec(key.substring(0, 16).getBytes(StandardCharsets.UTF_8));cipher.init(Cipher.DECRYPT_MODE, skeySpec, iv);byte[] original = cipher.doFinal(Base64.getDecoder().decode(cipherText));return new String(original, StandardCharsets.UTF_8);} catch (Exception e) {throw new RuntimeException("AES decrypt error", e);}}
}
package com.xiaou.secure.util;import org.apache.commons.codec.digest.HmacAlgorithms;
import org.apache.commons.codec.digest.HmacUtils;import java.nio.charset.StandardCharsets;
import java.util.Map;
import java.util.StringJoiner;/*** 签名工具类*/
public class SignUtil {private SignUtil() {}/*** 生成签名* * @param params 不包含 sign 的参数 map,已按字典序排序* @param secret 秘钥*/public static String sign(Map<String, String> params, String secret) {StringJoiner sj = new StringJoiner("&");params.forEach((k, v) -> sj.add(k + "=" + v));String data = sj.toString();return new HmacUtils(HmacAlgorithms.HMAC_SHA_256, secret.getBytes(StandardCharsets.UTF_8)).hmacHex(data);}
}

以上就是全部源码

如果想要看具体的一个实现可以参考我的开源项目里面的xiaou-common-secure模块 https://github.com/xiaou61/U-space

使用流程

在需要的接口上添加注解

	@SecureApi                // 生效!@PostMapping("/student/save")public R<Void> saveStudent(HttpServletRequest request) {String json = (String) request.getAttribute("secureData"); // 解密后明文StudentDTO dto = JSON.parseObject(json, StudentDTO.class);//其他业务操作return R.ok();}
}

前端接入

1. 安装依赖

npm i crypto-js

2. 编写工具 (src/utils/secure.js)

import CryptoJS from 'crypto-js';const AES_KEY  = import.meta.env.VITE_AES_KEY;      // 16/24/32 字符,与后端保持一致
const SIGN_KEY = import.meta.env.VITE_SIGN_SECRET;  // 与后端 sign-secret 一致// AES/CBC/PKCS5Padding 加密 → Base64
export function aesEncrypt(plainText) {const key = CryptoJS.enc.Utf8.parse(AES_KEY);const iv  = CryptoJS.enc.Utf8.parse(AES_KEY.slice(0, 16));const encrypted = CryptoJS.AES.encrypt(plainText, key, {iv,mode: CryptoJS.mode.CBC,padding: CryptoJS.pad.Pkcs7});return encrypted.ciphertext.toString(CryptoJS.enc.Base64);
}// 生成签名:字典序拼接后做 HMAC-SHA256
export function sign(params) {const sortedStr = Object.keys(params).sort().map(k => `${k}=${params[k]}`).join('&');return CryptoJS.HmacSHA256(sortedStr, SIGN_KEY).toString();
}

封装

import http from './request'
import { aesEncrypt, sign as genSign } from './secure'// securePost 重新实现:封装 { timestamp, data: cipher, sign }export async function securePost (url, bizData = {}, { encrypt = true } = {}) {const timestamp = Math.floor(Date.now() / 1000) // 秒级时间戳,和后端配置一致// 若开启加密,将 bizData 加密为 Base64 字符串const cipherText = encrypt ? aesEncrypt(bizData) : JSON.stringify(bizData)// 组装待签名参数const payload = {timestamp,data: cipherText}// 生成签名payload.sign = genSign(payload)// 发送 JSONreturn http.post(url, payload, {headers: {'Content-Type': 'application/json'}})
}// 向后兼容:导出旧别名
export { securePost as securePostV2 } 

调用

export const login = (data) => {// 学生登录接口使用新的 securePost (AES/CBC + HMAC-SHA256)return securePost('/student/auth/login', data)
}

原理解析

这个接口加密机制的出发点其实很简单:

我们不希望别人伪造请求或者直接看到请求内容。尤其是在登录、提交表单这种接口上,如果不做处理,参数一旦被篡改或者被抓包,后果可能挺严重。

所以我们在请求中加了一些“安全三件套”:

第一是签名。前端每次发请求的时候,会把参数(主要是 timestamp 和加密后的 data)按字典序拼起来,然后用我们双方约定好的一个密钥生成一个签名(HMAC-SHA256 算法)。后端拿到请求后,同样的算法再生成一遍签名,两个对不上就直接拒绝。这个方式能有效防止参数被篡改。

第二是时间戳。我们不允许别人把一两分钟前抓到的请求再发一次,所以前端在请求里带上当前时间(秒级)。后端检查这个时间是否还在允许的时间窗口(比如前后 5 分钟)内,超了就拒绝。这个能防止重放攻击。

第三是加密。我们不希望别人看到业务参数,比如手机号、密码、验证码这类字段,所以前端用 AES(CBC 模式)把整个业务数据 JSON 加密成密文,后端收到后再解密拿出真实参数。密钥是我们自己设定的,别人拿不到。

整套逻辑通过 Spring AOP 实现,不需要每个接口去写重复代码,只要在 Controller 上加一个 @SecureApi 注解就行了。请求数据校验通过后,解密出来的原始 JSON 会通过 request.setAttribute("secureData", plaintext) 注入进去,业务代码直接拿就行。

整体上,这个方案是为了在不增加太多开发成本的前提下,做到参数不可篡改、请求不可复用、敏感数据不可明文传输。

流程图

image-20250713144310930

image-20250713144325491

高清流程图

https://yxy7auidhk0.feishu.cn/wiki/LuXjwlXjxiFk4tkgrUEc0Ppbn4n?from=from_copylink

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若转载,请注明出处:http://www.pswp.cn/news/914343.shtml
繁体地址,请注明出处:http://hk.pswp.cn/news/914343.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

斯坦福 CS336 动手大语言模型 Assignment1 BPE Tokenizer TransformerLM

所有代码更新至 https://github.com/WangYuHang-cmd/CS336/tree/main/assignment1-basics 作业文件结构: CS336/assignment1-basics/ ├── tests/ # 测试文件目录 │ ├── adapters.py # 适配器测试 │ ├── conftest.py # pyt…

Spring Cloud Gateway 实战指南

关键词&#xff1a;微服务、API网关、Spring Cloud Gateway、路由转发、限流熔断 ✅ 文章摘要 随着互联网应用规模的不断扩大&#xff0c;传统的单体架构逐渐向微服务架构转型。在微服务架构中&#xff0c;API 网关作为系统的入口点&#xff0c;承担了诸如请求路由、负载均衡、…

PyTorch自动微分:从基础到实战

目录 1. 自动微分是什么&#xff1f; 1.1 计算图 1.2 requires_grad 属性 2. 标量和向量的梯度计算 2.1 标量梯度 2.2 向量梯度 3. 梯度上下文控制 3.1 禁用梯度计算 3.2 累计梯度 4. 梯度下降实战 4.1 求函数最小值 4.2 线性回归参数求解 5. 总结 在深度学习中&a…

Spring AI 项目实战(十六):Spring Boot + AI + 通义万相图像生成工具全栈项目实战(附完整源码)

系列文章 序号文章名称1Spring AI 项目实战(一):Spring AI 核心模块入门2Spring AI 项目实战(二):Spring Boot + AI + DeepSeek 深度实战(附完整源码)3Spring AI 项目实战(三):Spring Boot + AI + DeepSeek 打造智能客服系统(附完整源码)4

从零到一:企业如何组建安全团队

在这个"黑客满天飞&#xff0c;漏洞遍地跑"的时代&#xff0c;没有安全团队的企业就像裸奔的勇士——虽然很有勇气&#xff0c;但结局往往很悲惨。 &#x1f4cb; 目录 为什么要组建安全团队安全团队的核心职能团队架构设计人员配置策略技术体系建设制度流程建立实施…

业务访问控制-ACL与包过滤

业务访问控制-ACL与包过滤 ACL的定义及应用场景ACL&#xff08;Access Control List&#xff0c;访问控制列表&#xff09;是用来实现数据包识别功能的&#xff1b;ACL可以应用于诸多场景&#xff1a; 包过滤功能&#xff1a;对数据包进行放通或过滤操作。NAT&#xff08;Netwo…

穿梭时空的智慧向导:Deepoc具身智能如何赋予导览机器人“人情味”

穿梭时空的智慧向导&#xff1a;Deepoc具身智能如何赋予导览机器人“人情味”清晨&#xff0c;当第一缕阳光透过高大的彩绘玻璃窗&#xff0c;洒在博物馆光洁的地板上&#xff0c;一位特别的“馆员”已悄然“苏醒”。它没有制服&#xff0c;却有着清晰的指引&#xff1b;它无需…

PostgreSQL 查询库中所有表占用磁盘大小、表大小

SELECTn.nspname AS schema_name,c.relname AS table_name,-- 1️⃣ 总大小&#xff08;表 toast 索引&#xff09;pg_size_pretty(pg_total_relation_size(c.oid)) AS total_size,-- 2️⃣ 表不包含索引&#xff08;含 TOAST&#xff09;pg_size_pretty(pg_total_relation_s…

日记-生活随想

最近鼠鼠也是来到上海打拼&#xff08;实习&#xff09;了&#xff0c;那么秉持着来都来了的原则&#xff0c;鼠鼠也是去bw逛了逛&#xff0c;虽说没票只能在外场看看&#x1f62d;。可惜几乎没有多少我非常喜欢的ip&#xff0c;不由感慨现在的二次元圈已经变样了。虽说我知道内…

串口A和S的含义以及RT的含义

A async 异步S sync 同步RT 收发U A RT 异步U SA RT 同步/异步

spring cloud负载均衡分析之FeignBlockingLoadBalancerClient、BlockingLoadBalancerClient

本文主要分析被 FeignClient 注解的接口类请求过程中负载均衡逻辑&#xff0c;流程分析使用的依赖版本信息如下&#xff1a;<spring-boot.version>3.2.1</spring-boot.version><spring-cloud.version>2023.0.0</spring-cloud.version><com.alibaba.…

ref 和 reactive

文章目录ref 和 reactive一、差异二、能否替代的场景分析&#xff08;1&#xff09;基本类型数据&#xff08;2&#xff09;对象类型数据&#xff08;3&#xff09;数组类型数据&#xff08;4&#xff09; 需要整体替换的场景三、替代方案与兼容写法1. 用 reactive 模拟 ref2. …

BatchNorm 与 LayerNorm:原理、实现与应用对比

BatchNorm 与 LayerNorm&#xff1a;原理、实现与应用对比 Batch Normalization (批归一化) 和 Layer Normalization (层归一化) 是深度学习中两种核心的归一化技术&#xff0c;它们解决了神经网络训练中的内部协变量偏移问题&#xff0c;大幅提升了模型训练的稳定性和收敛速度…

OcsNG基于debian一键部署脚本

&#x1f914; 为什么有了GLPI还要部署OCS-NG&#xff1f; 核心问题&#xff1a;数据收集的风险 GLPI直接收集的问题&#xff1a; Agent直接向GLPI报告数据时&#xff0c;任何收集异常都会直接影响资产数据库网络问题、Agent故障可能导致重复资产、错误数据、资产丢失无法对收集…

001_Claude开发者指南介绍

Claude开发者指南介绍 目录 Claude简介Claude 4 模型开始使用核心功能支持资源 Claude简介 Claude 是由 Anthropic 构建的高性能、可信赖和智能的 AI 平台。Claude 具备出色的语言、推理、分析和编程能力&#xff0c;可以帮助您解决各种复杂任务。 想要与 Claude 聊天吗&a…

004_Claude功能特性与API使用

Claude功能特性与API使用 目录 API 基础使用核心功能特性高级功能开发工具平台支持 API 基础使用 快速开始 通过 Anthropic Console 获取 API 访问权限&#xff1a; 在 console.anthropic.com/account/keys 生成 API 密钥使用 Workbench 在浏览器中测试 API 认证方式 H…

ReAct论文解读(1)—什么是ReAct?

什么是ReAct&#xff1f; 在大语言模型&#xff08;LLM&#xff09;领域中&#xff0c;ReAct 指的是一种结合了推理&#xff08;Reasoning&#xff09; 和行动&#xff08;Acting&#xff09; 的提示方法&#xff0c;全称是 “ReAct: Synergizing Reasoning and Acting in Lan…

【云服务器安全相关】服务器防火墙常见系统日志信息说明

目录✅ 一、防火墙日志是做什么的&#xff1f;&#x1f6e0;️ 二、常见防火墙日志信息及说明&#x1f9ea; 三、典型日志示例解析1. 被阻断的访问&#xff08;DROP&#xff09;2. 被允许的访问&#xff08;ACCEPT&#xff09;3. 被拒绝的端口访问4. 可疑端口扫描行为&#x1f…

011_视觉能力与图像处理

视觉能力与图像处理 目录 视觉能力概述支持的图像格式图像上传方式使用限制最佳实践应用场景API使用示例视觉能力概述 多模态交互 Claude 3 系列模型具备强大的视觉理解能力,可以分析和理解图像内容,实现真正的多模态AI交互。这种能力使Claude能够: 图像内容分析:理解图…

ansible自动化部署考试系统前后端分离项目

1. ✅ansible编写剧本步骤1️⃣创建roles目录结构2️⃣在group_vars/all/main.yml中定义变量列表3️⃣在tasks目录下编写tasks任务4️⃣在files目录下准备部署文件5️⃣在templates目录下创建j2模板文件6️⃣在handlers目录下编写handlers7️⃣在roles目录下编写主playbook8️⃣…