前后端分离项目SM4加密传输数据实战指南

发布时间:2026/9/26 2:36:05
前后端分离项目SM4加密传输数据实战指南 写这篇东西的起因特别直白我接了一个前后端分离的老项目登录和提交订单的接口全部明文传输。前端把用户手机号、身份证号当查询参数拼在URL里后端日志里能直接看到完整数据。老板的原话是“来波猛的”意思就是别再补丁式修修补补直接把传输链路从明文换成正儿八经的加密方案。这篇文章就是记录这次前后端加密传输数据的完整改造过程包括前端如何加密、后端如何解密、密钥怎么管理、踩了哪些坑给同样在“前后端分离项目”里搞“数据加密传输”的人一份能直接抄作业的参考。我默认读这篇文章的人至少会用Spring Boot和Vue前后端能正常联调。如果你刚接触“前端传参”和“后端跨域”这类基础问题建议先把项目跑通再来动加密否则排查问题时分不清是加密的锅还是接口本身的锅心态容易崩。1. 整体设计与思路拆解1.1 明文的痛点和我踩过的坑接手的时候我第一件事就是抓包看了几个接口触目惊心。登录接口的密码用MD5做了一次散列但MD5早就被撞库撞烂了而且散列不等于加密该泄露还是泄露。订单接口更夸张用户地址、手机号直接在GET请求的Query String里浏览器的历史记录里都挂着完整数据。还有一次我在排查线上问题发现后端的access.log里直接打印了用户的身份证号因为接口把身份证号当作查询参数传进来日志框架又默认把整个URL打出来了。这种场景下仅靠HTTPS是不够的。为什么因为HTTPS解决的是传输过程中被窃听的问题但数据到了后端服务器之后日志、缓存、数据库慢查询日志、第三方埋点都可能把明文数据记录下来。而且很多公司内部链路是HTTP比如前置Nginx和后端应用之间没走HTTPS这一段照样裸奔。我见过不止一次前端已经上了HTTPS但后端日志里全是明文手机号等于HTTPS白上。所以这次改造的核心思路不是“再套一层SSL”而是“让业务层根本接触不到明文”。前端加密后端解密中间即使被截获、被日志记录也是密文。这才是“钱后端加密传输数据”真正的含义不是给传输通道加锁而是让数据本身变成密文。1.2 选型为什么选SM4而不是AES一开始团队里有人提议直接上AES理由是用例多、资料全。但我否了原因是第一AES的密钥协商要额外设计前后端写死在代码里的话一旦泄露就是全军覆没第二国密SM4在合规性上更有优势尤其是涉及个人信息和金融数据的场景有的等保和行业审查明确要求使用国密算法第三SM4的性能和AES差距极小尤其在短文本加密场景下体感几乎无差别没必要为了“熟悉”去选一个未来可能被审查挑战的方案。我最终选择了SM4-CBC模式搭配PKCS7填充。为什么用CBC而不是ECBECB模式有个经典问题相同的明文块会产生相同的密文块数据模式会在密文中显露出来攻击者可以通过比对块结构判断“这两段明文是否相同”。CBC模式每个块加密前都会和上一个密文块做异或再加上随机初始向量(IV)同样的明文每次加密结果都不一样安全性明显更高。对应的密钥管理方案我没有把密钥硬编码在前端代码里而是放在了前端构建时的环境变量中实际部署时通过Nginx的sub_filter或者后端提供一个临时的配置接口下发。下面我会专门用一章说密钥管理的细节这块是整幺蛾子最多的地方。1.3 三段式防线接口入参、响应返回、链路完整校验这次改造我做了三条防线不是只加密请求参数那么简单入参加密前端把POST body整体加密部分GET接口的特殊参数单独处理后端统一拦截解密业务层拿到的还是正常对象。响应加密后端的返回值统一加密回传前端在axios的响应拦截器里统一解密页面代码无感知。完整性校验在密文里附带签名和摘要后端校验数据是否被篡改防止中间人改密文改不了内容但可以改时间戳、重放请求。三条线缺一不可。如果只做入参加密响应数据照样明文用户敏感信息从后端返回时仍然暴露如果只做响应加密请求参数还是裸奔如果不做完整性校验攻击者虽然解不开密文但可以把一段旧请求原样重放造成重复下单。这个设计思路放在任何一个“前后端分离项目实战”里都适用不是只针对我这个项目。2. 核心细节解析与实操要点2.1 密钥管理的坑硬编码是最大的漏洞这是整个改造里最容易踩坑的地方。最初同事图省事直接把SM4密钥写在前端的JavaScript代码里虽然代码经过了webpack打包混淆但只要用浏览器开发者工具的Sources面板搜索密钥字符串几分钟就能扒出来。密钥一旦泄露加密形同虚设。我最终用的是“构建期注入 后端配置下发”的方式前端在.env.production里配置VUE_APP_CRYPTO_KEY和VUE_APP_CRYPTO_IV构建时注入到代码中。后端提供一个/api/config/crypto接口需登录态前端启动时先调用这个接口获取密钥密钥存放在内存中不写入localStorage。部署时Nginx用sub_filter将前端页面里固定的密钥占位符替换为真正的密钥这样密钥不会出现在Git仓库中。听上去有点绕但实际做起来不复杂。核心原则是源码仓库里不出现真实密钥部署环境里通过配置中心或环境变量注入。密钥轮换也要提上日程我设置的密钥有效期是7天到期后前端重新拉取配置。这里有一个容易掉的坑如果密钥轮换时旧请求还没处理完前后端密钥不一致会导致解密失败所以我做了“新旧密钥并行期”处理——后端在解密失败时尝试用上一版密钥再解一次前端则保留旧密钥的缓存切换时先试新密钥再退回到旧密钥。这套机制在并发高的场景下特别重要。2.2 网关统一处理还是业务层处理我选择了网关层解密逻辑放在哪里是个关键决策。如果放在每一个Controller里会造成大量重复代码而且容易漏掉某个接口导致明文数据漏网。我选择了加一个Spring Boot拦截器HandlerInterceptor统一处理前端请求进入Controller之前拦截器先取body里的密文解密成明文JSON后重新写入请求流这样Controller方法签名完全不用改。响应返回之前通过ResponseBodyAdvice统一对返回对象做加密前端收到的永远是密文。多线程安全问题必须注意RequestBodyAdvice的默认实现是有状态的我通过ThreadLocal来传递解密后的明文数据用完立即清理防止线程池复用导致数据串号。网关层的另一个好处是新来的后端同事不需要理解加解密逻辑他们只管写业务接口数据安全性由拦截器统一兜底。这个思路和“后端跨域”问题类似——跨域也适合在网关层统一配置而不是每个接口自己处理。如果你在“若依框架”这类内含多个模块的后端项目上做改造更应该在公共模块里做统一拦截而不是每个module写一套。2.3 前端加密封装axios拦截器和防调试技巧前端的核心工作是在请求拦截器里加密数据、在响应拦截器里解密数据。我封装了一个crypto.js模块对外暴露两个方法encryptData和decryptData。import CryptoJS from crypto-js const key CryptoJS.enc.Utf8.parse(process.env.VUE_APP_CRYPTO_KEY) const iv CryptoJS.enc.Utf8.parse(process.env.VUE_APP_CRYPTO_IV) export function encryptData(data) { const dataStr typeof data string ? data : JSON.stringify(data) const encrypted CryptoJS.SM4.encrypt(dataStr, key, { iv, mode: CryptoJS.mode.CBC, padding: CryptoJS.pad.Pkcs7 }) return encrypted.toString() } export function decryptData(cipherText) { const decrypted CryptoJS.SM4.decrypt(cipherText, key, { iv, mode: CryptoJS.mode.CBC, padding: CryptoJS.pad.Pkcs7 }) return CryptoJS.enc.Utf8.stringify(decrypted) }axios拦截器里这样用service.interceptors.request.use(config { if (config.method post) { config.data { payload: encryptData(config.data), timestamp: Date.now().toString() } } return config }) service.interceptors.response.use(response { if (response.data response.data.payload) { response.data JSON.parse(decryptData(response.data.payload)) } return response })注意几个细节加密前用JSON.stringify把对象转成字符串否则SM4加密的是对象的toString结果解密回来会变样。每次请求生成独立的时间戳timestamp不仅用于防止完全相同的重放还在后端校验时间偏差偏差超过5分钟的请求直接拒绝。crypto-js在浏览器里是大体积库建议用webpack的SplitChunks把它单独打成chunk避免主包过大拖慢首屏加载。前端代码被debugger打断时加解密逻辑可能会被逐步跟踪我加了一个简单的防调试循环但不建议过度过度会导致用户浏览器卡死反而伤体验。3. 实操过程与核心环节实现3.1 后端加解密工具类的完整实现后端我用Hutool的SM4工具类做了封装简洁可靠。核心代码如下Component public class Sm4CryptoUtil { Value(${crypto.sm4.key}) private String key; Value(${crypto.sm4.iv}) private String iv; private volatile SM4 sm4; PostConstruct public void init() { this.sm4 SmUtil.sm4(key.getBytes(StandardCharsets.UTF_8)); } public String encrypt(String plainText) { return sm4.encryptHex(plainText, iv.getBytes(StandardCharsets.UTF_8)); } public String decrypt(String cipherText) { return sm4.decryptStrHex(cipherText, iv.getBytes(StandardCharsets.UTF_8)); } public String encryptWithRetry(String plainText) { String result encrypt(plainText); if (result null || result.length() 0) { throw new CryptoException(加密结果为空请检查明文内容); } return result; } }Hutool的SM4实现默认就是CBC模式如果你用AES则要在SmUtil.sm4之外额外指定模式和填充。用PostConstruct初始化SM4实例是为了避免每个请求都重新创建对象减少性能损耗。生产环境里key和iv不要写在application.yml里提交到Git仓库应该用配置中心比如Nacos或者环境变量注入。不要以为写Value就是安全了配置文件的来源才是安全的真正关键。3.2 拦截器实现请求解密 响应加密 幂等性校验拦截器是这一套的核心入口。我做了一个CryptoInterceptor实现HandlerInterceptor接口在preHandle里解密请求在postHandle之后通过ResponseBodyAdvice统一加密响应。这里只展示请求解密部分的逻辑Component public class CryptoInterceptor implements HandlerInterceptor { private static final ThreadLocalString DECRYPTED_BODY new ThreadLocal(); Autowired private Sm4CryptoUtil sm4CryptoUtil; Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { if (!(handler instanceof HandlerMethod)) { return true; } // 读取body密文 String cipherBody getRequestBody(request); if (StringUtils.isBlank(cipherBody)) { throw new CryptoException(请求体不能为空); } // 校验幂等性摘要 JSONObject bodyJson JSON.parseObject(cipherBody); String timestamp bodyJson.getString(timestamp); if (Math.abs(System.currentTimeMillis() - Long.parseLong(timestamp)) 300_000) { throw new CryptoException(请求时间戳过期); } // 解密payload String plainText sm4CryptoUtil.decrypt(bodyJson.getString(payload)); // 校验完整性 if (!verifyIntegrity(bodyJson, plainText)) { throw new CryptoException(数据完整性校验失败); } DECRYPTED_BODY.set(plainText); // 重新包装request让Controller能正常读取到明文 RequestWrapper requestWrapper new RequestWrapper(request); requestWrapper.setBody(plainText.getBytes(StandardCharsets.UTF_8)); return true; } Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) { DECRYPTED_BODY.remove(); } private String getRequestBody(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 boolean verifyIntegrity(JSONObject bodyJson, String plainText) { // 前端在payload里追加了一个sign字段值为对明文内容做的HMAC-SHA256摘要 String expectedSign bodyJson.getString(sign); String actualSign hmacSha256(plainText, secretKey); return expectedSign.equals(actualSign); } }幂等性校验这里我做了两层时间戳和HMAC。时间戳防重放HMAC防篡改。HMAC和SM4共用了同一个密钥这其实不是最优解理想情况是SM4和HMAC分别用不同的密钥但从工程复杂度角度考虑在中小项目里共用一个密钥是权衡后的选择我心里有数等后续有精力再拆。3.3 加密粒度控制浓缩与拆分不是所有接口都需要全量加密。有些接口是GET请求参数直接在URL上前端不太好改有些接口返回的是大列表加密后流量膨胀反而影响性能。我做了一张表按场景区分加密策略场景是否加密原因登录接口、注册接口加密密码和手机号是敏感数据绝对不裸奔用户信息查询/修改加密涉及身份证、地址订单列表分页加密地址和联系方式是敏感信息商品列表公开数据不用加密加密纯属浪费CPU图形验证码接口不用加密验证码本来就是给机器看的加密没有意义文件上传接口单独处理体积大加密后Multipart解析会出问题一般只对文件名做加密文件上传接口是我踩过的一个特殊坑。如果直接对整个Multipart body做SM4加密后端解析时会直接报错因为multipart的boundary结构被破坏了。我最终的做法是文件名和业务参数走加密JSON文件本身单独走HTTPS上传两者用同一个uploadId关联。这个方案既保证了元数据安全又不会把大文件加密导致内存压力爆炸。如果你处理的是大文件上传可以进一步用分片加密但要考虑分片校验和组装的复杂度。3.4 密钥下发与前后端联调实测过程中的细节联调阶段我遇到了整个改造过程中最折磨人的问题前端加密后的JSON结构后端解析不对。排查了很久发现是前端在构造请求体时JSON的键顺序不确定而后端用的是RequestBody一旦某个字段缺失直接解析失败。解决方案是前端固定一个请求体模板service.interceptors.request.use(config { if (config.method post) { config.data { payload: encryptData(config.data), timestamp: Date.now().toString(), sign: signData(JSON.stringify(config.data)) } } return config })signData在加密前先对原始明文做一次HMAC-SHA256后端拿密文解密后再验签能有效防止密文被篡改。这个“先签名再加密、先解密后验签”的顺序是经过反复推敲的先说结论先签名后加密是对的。如果先加密再签名攻击者虽然无法解出明文但可以把签名对象换成别人的密文实现“换文攻击”。另外联调时前后端都要打开详细的日志。后端在拦截器里打日志时只打印密文长度和签名结果绝不能打印明文内容否则日志泄露比接口泄露更严重。前端在开发环境下可以临时打印明文但生产环境必须关掉console输出我用webpack的terser-plugin配置把console.log全部移除。4. 常见问题与排查技巧实录4.1 问题排查速查表我在改造过程中整理了一个速查表遇到问题先查这块问题现象可能原因排查命令/手段解决方案前端报“解密结果为空”后端响应格式不是{payload:...}结构浏览器Network面板查看原始响应确认ResponseBodyAdvice是否生效检查注解扫描路径后端报“时间戳过期”前端与服务器时间不同步date命令对比两台机器时间使用NTP时间同步或放宽偏差阈值解密后JSON是乱码SM4模式不一致前端CBC后端ECB对比前后端mode参数统一使用CBC模式IV长度必须是16字节响应内容被截断密文太长Nginx默认body大小限制查看Nginx错误日志在location里改client_max_body_size请求体为空GET请求走了加密逻辑但没加密检查axios拦截器的method判断GET请求单独处理不能走POST加密逻辑解密后数据含有\ufffdUTF-8编码转换错误查看前后端字符编码设置统一UTF-8不要在中间环节手动转码重复提交订单时间戳在有效期内但请求被重放给每个请求加唯一requestId服务端用Redis判断requestId是否已处理过“解密后数据含有\ufffd”这个问题的根因我在日志里查了半小时Cookie里带了一个非UTF-8的值导致request.getReader()读取时把字符流搞乱了。后来我统一用request.getInputStream()读字节流再手动按UTF-8转字符串彻底解决了。4.2 日志配置和敏感信息脱敏这是很多人忽略的一环。接口加了加密以后如果后端日志里直接打印decryptedData的内容那加密就白做了。我做了三件事日志框架配置里对包含“phone”、“idCard”、“password”关键字的字段自动打码只保留前3位和后4位中间用星号填充。全局异常处理器里对解密失败抛出的异常不打印密文原文只打印密文长度和请求路径避免日志被拖库后攻击者拿到密文做离线破解。链路追踪IDtraceId和密文长度、签名结果绑定打印便于定位问题是哪个请求导致的而不用依赖明文内容。这样调日志就变成了“只记录元数据不记录数据本身”。如果你的项目引入了“数据备份与恢复”策略这里再提醒一句备份文件和日志文件一样如果包含明文敏感数据备份文件本身也要加密存储。我在生产环境里见过备份SQL文件整整300GB明文放在对象存储上权限配置不当的话相当于裸奔给全网看。4.3 兼容性测试老版本客户端适配改造上线时最怕的是客户端没升级新后端只认密文老客户端还在发明文。我这个问题在App端尤其明显因为App发版周期长用户不更新的话接口直接挂。我的处理方式是后端拦截器做一个“双模兼容”尝试按密文解析如果解析出的JSON里没有payload字段则视为明文直接放行。给明文请求打一个deprecated标记在响应头里返回提示引导前端引导用户升级。设置一个过渡期比如两周后强制切到密文模式明文请求直接返回400并在HTTP头里写明原因UPGRADE_REQUIRED。这个兼容模式上线时注意安全明文请求的敏感数据仍然会被日志记录所以过渡期内要对日志里的敏感字段加强脱敏不能因为是兼容期就放松。“前端面试题2026”里经常考察的一个点——“如何优雅地做API版本兼容”其实这里的双模兼容就是一个答案只是很多人没机会在实际项目中实践。4.4 性能实测加解密损耗和创新点加密必然带来性能损耗但损耗在可接受范围内。我在生产环境做了压测接口平均耗时变化如下指标改造前改造后差异登录接口平均响应时间82ms96ms17%用户信息查询平均响应时间64ms78ms21%订单列表100条平均响应时间120ms145ms20%CPU使用率峰值压测35%42%7个百分点这个涨幅在可接受范围内因为SM4在纯软件实现下接近1Gbps的吞吐量短文本加密的耗时主要在序列化和I/O上不在算法本身。如果使用硬件加速卡或者指令集优化还能降一半损耗但中小项目没必要上这个。我优化性能的一个小技巧是“积累加密”后端返回响应时把多个小字段合并成一个JSON字符串再加密而不是分成多个小密文。这样减少了加解密函数的调用次数也减少了密文长度。前端的加密同理不要对每一个表单字段单独调用encryptData太浪费。5. 实测体检与扩展思路5.1 “加密体检”你确定真的加密了吗做完改造我最担心的是“自己以为加密了实际没加密”。我写了一个体检脚本模拟最真实的攻击路径来测试如果你的项目也做了加密建议按这个清单自查用浏览器开发者工具查看所有POST请求body确认里面看不到任何明文键名比如password、phone等。点击浏览器历史记录检查GET参数里有没有身份证号、手机号。模拟中间人攻击用fiddler或Charles抓包看响应body是否全是密文代码能不能在响应里直接搜索到明文关键词。检查后端应用日志和Nginx日志确认不打印明文请求参数。手工篡改其中一个字节的密文看后端能不能识别出校验失败而不是解密出乱码后继续走业务流程。测试重放攻击抓一个包原样重放看后端是否拒绝时间戳过期或requestId重复的请求。这六项如果都能通过才能说“钱后端加密传输数据”这件事真的落地了。别只看代码写了加密逻辑就觉得安全安全是测出来的不是写出来的。5.2 后续扩展双因子签名和国密整体改造这套方案目前覆盖了数据加解密和完整性校验但还有两个方向值得后续扩展。一个是“非对称密钥协商”现在前后端共用同一个SM4密钥一旦前端密钥泄露整个系统就完了。下一步可以做“SM2 SM4”混合方案前端持有SM2公钥后端持有私钥每次会话前端生成临时的SM4密钥用SM2公钥加密后传给后端后端用SM2私钥解密拿到临时SM4密钥之后的数据用这个临时密钥加密。这样的好处是即使前端密钥泄露攻击者也只能解密出来一个会话历史数据仍然安全。另一个方向是把传输加密与数据库加密联动起来前端加密的是传输过程中的数据后端解密后如果直接写入数据库那数据库文件泄露时还是明文。可以考虑敏感字段落库前再加密一次查询时解密这属于“数据库字段级加密”和传输加密之间是两层独立防线。但要注意的是字段加密会让索引、排序、模糊查询全部失效需要设计旁路索引或使用支持密文检索的中间件复杂度会上一个台阶。中小项目里先把传输加密做好数据库加密量力而行。5.3 一点踩坑后的个人体会这次改造给我的最大体会是加密传输数据这件事很多时候问题不在算法选型而在密钥管理和团队习惯。算法选型我反复纠结过多次但真正上线后被挑战的往往是“密钥存在哪里”“日志里有没有泄露”“旧客户端怎么兼容”。这些问题如果在设计阶段就想清楚后面能少熬很多夜。另外一个体会是别把加密做成一刀切的全量加密。我一开始想的是所有接口全部加密后来发现有些公开接口比如商品列表、公告加密纯粹是浪费CPU还会让首页加载更慢。合理的做法是按照数据敏感程度分等级等级高的接口加密等级低的保持原样。这个“分级治理”的思路在技术上值不了多少钱但在工程治理上价值很高。最后再分享一个小技巧前后端联调时把密钥和IV固定成一套开发环境的专用值别拉到生产配置。开发环境用一套测试环境用一套生产环境用一套三套独立。即使开发环境密钥被翻出来也不影响生产。这个细节很多人会忽略等出了问题才后悔当时嫌麻烦没分开。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询