☰
MD5工具类实战:SpringBoot密码加密与接口签名最佳实践
2026/10/18 12:37:11 网站建设 项目流程

这次我们不用先讲概念,直接看代码。在基于 SpringBoot + Vue + Uniapp + LangChain4j + SpringAI 的智能视频推荐小程序项目里,注册登录、接口签名、防重复提交、AI 会话鉴权这些环节,几乎都绕不开一个基础工具类:MD5 工具类。

这一讲的核心任务是:把 MD5 工具类写好、写稳、写对。不是简单贴一个MessageDigest的封装,而是从实际业务出发,把字符串加密、文件摘要、密码加盐、签名校验、Uniapp 端调用、SpringBoot 接口联动全部串起来。同时,我们也要说清楚 MD5 的安全边界:什么场景能用,什么场景不能用,为什么推荐进一步用 BCrypt。

如果你正在做 SSM、SpringBoot 单体项目、小程序后端,或者想给自己的 AI 应用加一层简单的请求签名,这篇文章可以直接收藏。后面每一段代码都能直接复制到项目里运行,附带单元测试和接口联调示例。

1. 核心能力速览

能力项说明
项目背景SpringBoot + Vue + Uniapp 智能视频推荐小程序后端
技术栈Java 8 / Java 17、Maven、Spring Boot
工具类功能字符串 MD5、文件 MD5、加盐 MD5、签名校验
实现方式JDK 原生 MessageDigest、Spring DigestUtils、Hutool SecureUtil
核心应用用户密码加密存储、接口签名防篡改、AI 会话参数校验
前端配合Uniapp 端密码传输建议使用 HTTPS + RSA 或后端 MD5 盐值加密
安全边界MD5 存在碰撞风险,密码存储建议使用 BCrypt,MD5+盐仅用于学习与签名场景
是否支持批量支持批量文件摘要与批量参数签名
适合场景单体项目教程、接口签名、文件完整性校验、学习案例

注意:本讲不讨论 SHA-256 与 MD5 的算法细节优劣,重点放在工程落地与代码组织上。项目中的密码加密演示主要用于理解登录流程,生产环境请参考第 9 节使用 BCrypt 替代。

2. 适用场景与使用边界

2.1 MD5 工具类在项目中的定位

在一个智能视频推荐小程序中,MD5 工具类主要出现在这些位置:

  • 用户注册时,对用户明文密码做不可逆摘要,再落库。
  • 用户登录时,将前端传入的密码摘要与数据库中的摘要做比对。
  • 接口请求中,对关键参数拼接后计算签名,防止请求参数被篡改。
  • 视频素材上传后,计算文件 MD5,用于素材去重与完整性校验。
  • AI 推荐接口调用前后,对会话标识做摘要,避免非法遍历。

这些场景都有一个共同点:我们需要的是一个稳定的、可复用的摘要工具,而不是每次在不同 Service 里重复写MessageDigest的 try-catch。这就是工具类存在的意义。

2.2 使用边界与合规提醒

  • MD5 是摘要算法,不是加密算法。它生成的固定长度摘要不可逆,理论上无法还原原始数据,但存在碰撞攻击风险。
  • 密码存储场景,生产环境不建议直接使用 MD5。即使加盐,MD5 的迭代速度太快,容易被暴力破解。更稳妥的选择是 BCrypt、Argon2 或 scrypt。
  • 接口签名场景,MD5 可以用于内部系统间低安全等级的数据完整性校验。如果是对外开放 API,建议升级为 HMAC-SHA256 或 RSA 签名。
  • 视频推荐小程序涉及用户行为数据、观看记录、AI 推荐结果。所有摘要、加密、签名逻辑只能用于合法业务,不得用于绕过权限控制、伪造请求、窃取数据或规避平台规则。
  • 如果项目中涉及用户上传的视频、人脸信息、语音内容,必须获得用户明确授权,并按照最小必要原则存储与使用。

3. 环境准备与前置条件

3.1 开发环境检查清单

以当前项目的后端工程为例,本讲代码可以在以下环境直接运行:

  • JDK:Java 8 或 Java 17(Spring Boot 2.x 建议 Java 8,Spring Boot 3.x 建议 Java 17)
  • 构建工具:Maven 3.6+ 或 Gradle 7.x
  • 框架版本:Spring Boot 2.7.x 或 3.1.x
  • 前端开发工具:HBuilderX 或 VSCode(Uniapp 项目)
  • 数据库:MySQL 5.7 / 8.0(本讲不涉及复杂表结构)
  • Redis:可选,用于接口防重与签名缓存

如果没有项目环境,也可以单独创建一个 Spring Boot 工程验证本讲内容。只需要spring-boot-starter-web和lombok即可。

3.2 pom.xml 最小依赖

<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <!-- Hutool 工具库,可选;如果不想引入,可以直接用 JDK 实现 --> <dependency> <groupId>cn.hutool</groupId> <artifactId>hutool-all</artifactId> <version>5.8.25</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies>

Hutool 版本号建议以 Maven 仓库最新稳定版为准。如果你不想引入额外依赖,可以使用 JDK 自带的MessageDigest,后者在本文第 4 节有完整实现。

3.3 目录规划

本讲新增代码统一放在以下包结构中:

com.example.videorecommend ├── common │ └── util │ ├── Md5Util.java │ └── SignUtil.java ├── controller │ └── AuthController.java ├── service │ └── UserService.java └── entity └── User.java

如果项目是多模块结构,建议把Md5Util放在公共模块的common-util子模块中,方便视频推荐服务、AI 会话服务、用户服务共同引用。

4. MD5 工具类实现与代码解析

4.1 基于 JDK 原生实现

JDK 自带的MessageDigest是实现 MD5 最基础的方式。优点是零依赖,缺点是代码需要处理字节数组与十六进制字符串的转换。

package com.example.videorecommend.common.util; import java.nio.charset.StandardCharsets; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; /** * MD5 工具类 * 基于 JDK 原生 MessageDigest 实现 */ public class Md5Util { private static final char[] HEX_DIGITS = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'}; private Md5Util() { } /** * 计算字符串的 MD5 摘要 * * @param text 原始字符串,不允许为空 * @return 32 位小写十六进制字符串 */ public static String md5(String text) { if (text == null) { throw new IllegalArgumentException("text 不能为 null"); } try { MessageDigest digest = MessageDigest.getInstance("MD5"); byte[] bytes = digest.digest(text.getBytes(StandardCharsets.UTF_8)); return toHex(bytes); } catch (NoSuchAlgorithmException e) { // 实际不会发生,MD5 在 JDK 中始终可用 throw new IllegalStateException("当前 JDK 不支持 MD5 算法", e); } } /** * 计算文件的 MD5 摘要,适合视频素材去重与完整性校验 * * @param file 文件对象 * @return 32 位小写十六进制字符串 */ public static String md5File(File file) { if (file == null || !file.exists()) { throw new IllegalArgumentException("文件不存在"); } try (InputStream in = new FileInputStream(file)) { MessageDigest digest = MessageDigest.getInstance("MD5"); byte[] buffer = new byte[8192]; int len; while ((len = in.read(buffer)) != -1) { digest.update(buffer, 0, len); } return toHex(digest.digest()); } catch (Exception e) { throw new RuntimeException("文件 MD5 计算失败", e); } } private static String toHex(byte[] bytes) { char[] chars = new char[bytes.length * 2]; int index = 0; for (byte b : bytes) { chars[index++] = HEX_DIGITS[(b >>> 4) & 0x0F]; chars[index++] = HEX_DIGITS[b & 0x0F]; } return new String(chars); } }

这段代码有四个关键点:

  1. 字符串统一使用UTF-8编码,避免不同系统默认编码导致摘要不一致。
  2. toHex方法把字节数组转换为 32 位小写字符串,这是最常用的格式。
  3. md5File使用缓冲流逐块读取,视频文件较大时不会一次性把所有字节加载到内存。
  4. 工具类构造方法私有化,不允许外部实例化。

4.2 基于 Spring 的 DigestUtils 实现

Spring 框架自带的org.springframework.util.DigestUtils是对MessageDigest的封装,Spring Boot 项目可以直接使用,不需要额外引入依赖:

package com.example.videorecommend.common.util; import org.springframework.util.DigestUtils; import java.io.File; import java.io.FileInputStream; import java.io.InputStream; public class SpringMd5Util { private SpringMd5Util() { } /** * 字符串 MD5 */ public static String md5(String text) { return DigestUtils.md5DigestAsHex(text.getBytes(StandardCharsets.UTF_8)); } /** * 文件 MD5 */ public static String md5File(File file) { try (InputStream in = new FileInputStream(file)) { return DigestUtils.md5DigestAsHex(in); } catch (Exception e) { throw new RuntimeException("文件 MD5 计算失败", e); } } }

如果你用的是 Spring Boot 3.x,DigestUtils仍然存在于spring-core中,上面的代码可以直接使用。唯一需要注意的是,Spring 官方文档曾提示DigestUtils.md5DigestAsHex仅用于非安全场景,因为 MD5 已经不适合密码存储。

4.3 基于 Hutool 的 SecureUtil

如果项目中已经引入 Hutool,那么 MD5 工具类可以写得非常简洁:

package com.example.videorecommend.common.util; import cn.hutool.crypto.SecureUtil; import cn.hutool.crypto.digest.DigestUtil; import java.io.File; public class HutoolMd5Util { private HutoolMd5Util() { } public static String md5(String text) { return SecureUtil.md5(text); } public static String md5File(File file) { return DigestUtil.md5File(file); } /** * 加盐 MD5,盐值直接拼接到原文后面 */ public static String md5WithSalt(String text, String salt) { return SecureUtil.md5(text + salt); } }

在纯工具类层面,我更推荐直接使用 Spring 自带的DigestUtils或 Hutool。不过在教程项目中,自己实现Md5Util的好处是可以让新手看到底层原理,后续切换到 SHA-256、BCrypt 时也更容易理解。

4.4 加盐增强版

考虑到项目中有用户密码存库的需求,这里给出一个加盐 MD5 的完整实现。盐值的处理方式是在原密码后面拼接一段随机字符串,然后再做摘要:

package com.example.videorecommend.common.util; import java.security.MessageDigest; import java.security.SecureRandom; import java.nio.charset.StandardCharsets; public class PasswordUtil { private static final int SALT_LENGTH = 16; private PasswordUtil() { } /** * 生成随机盐值 */ public static String generateSalt() { byte[] bytes = new byte[SALT_LENGTH]; new SecureRandom().nextBytes(bytes); StringBuilder sb = new StringBuilder(); for (byte b : bytes) { sb.append(String.format("%02x", b)); } return sb.toString(); } /** * 带盐 MD5 摘要 */ public static String md5WithSalt(String password, String salt) { return Md5Util.md5(salt + password + salt); } /** * 密码校验 */ public static boolean matches(String rawPassword, String salt, String encodedPassword) { String target = md5WithSalt(rawPassword, salt); return target.equalsIgnoreCase(encodedPassword); } }

需要说明的是:加盐 MD5 比裸 MD5 安全很多,但仍然不是最优方案。因为在现代 GPU 环境下,MD5 的计算速度非常快,攻击者可以对“盐+密码”的组合进行大规模字典攻击。第 9 节会给出 BCrypt 的推荐写法。

5. 在注册与登录业务中集成 MD5 工具类

5.1 用户实体类

package com.example.videorecommend.entity; import com.baomidou.mybatisplus.annotation.IdType; import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; import lombok.Data; import java.time.LocalDateTime; @Data @TableName("t_user") public class User { @TableId(type = IdType.AUTO) private Long id; private String username; /** 实际存储的是 MD5(盐 + 密码 + 盐) */ private String password; /** 随机盐值 */ private String salt; private String nickname; private String avatar; private LocalDateTime createTime; }

5.2 注册逻辑

注册时,后端拿到 Uniapp 传来的用户名和密码,先生成盐值,再用PasswordUtil.md5WithSalt计算摘要,最后落库:

package com.example.videorecommend.service.impl; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.example.videorecommend.common.util.PasswordUtil; import com.example.videorecommend.entity.User; import com.example.videorecommend.mapper.UserMapper; import com.example.videorecommend.service.UserService; import org.springframework.stereotype.Service; @Service public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService { @Override public void register(String username, String rawPassword) { // 检查用户名是否已存在 long count = lambdaQuery() .eq(User::getUsername, username) .count(); if (count > 0) { throw new RuntimeException("用户名已存在"); } User user = new User(); user.setUsername(username); user.setSalt(PasswordUtil.generateSalt()); user.setPassword(PasswordUtil.md5WithSalt(rawPassword, user.getSalt())); user.setCreateTime(LocalDateTime.now()); save(user); } @Override public String login(String username, String rawPassword) { User user = lambdaQuery() .eq(User::getUsername, username) .one(); if (user == null) { throw new RuntimeException("用户不存在"); } boolean matched = PasswordUtil.matches(rawPassword, user.getSalt(), user.getPassword()); if (!matched) { throw new RuntimeException("密码错误"); } // 实际项目中这里会生成 JWT Token,本讲不做展开 return "mock-token-" + user.getId(); } }

注册流程的时序是:前端提交密码 -> 后端生成盐值 -> 拼接并计算 MD5 -> 存盐值和摘要。登录流程的时序是:前端提交密码 -> 后端从数据库查出盐值和摘要 -> 重新拼接计算 MD5 -> 比对。

5.3 认证接口 Controller

package com.example.videorecommend.controller; import com.example.videorecommend.service.UserService; import org.springframework.web.bind.annotation.*; import java.util.Map; @RestController @RequestMapping("/api/auth") public class AuthController { private final UserService userService; public AuthController(UserService userService) { this.userService = userService; } @PostMapping("/register") public Map<String, Object> register(@RequestBody RegisterRequest request) { userService.register(request.getUsername(), request.getPassword()); return Map.of("code", 200, "message", "注册成功"); } @PostMapping("/login") public Map<String, Object> login(@RequestBody RegisterRequest request) { String token = userService.login(request.getUsername(), request.getPassword()); return Map.of("code", 200, "message", "登录成功", "data", Map.of("token", token)); } public static class RegisterRequest { private String username; private String password; public String getUsername() { return username; } public void setUsername(String username) { this.username = username; } public String getPassword() { return password; } public void setPassword(String password) { this.password = password; } } }

5.4 Uniapp 前端调用示例

在 Uniapp 中,注册和登录页面只需要把用户输入的密码通过 HTTPS 提交到后端,不在前端做 MD5 处理。原因是 MD5 一旦在前端执行,摘要结果就变成了事实上的“密码”,如果被抓包逆向,攻击者可以直接用摘要请求登录接口。

// pages/login/login.vue 中 uniapp 请求示例 uni.request({ url: 'https://api.example.com/api/auth/login', method: 'POST', data: { username: this.username, password: this.password }, header: { 'Content-Type': 'application/json' }, success: (res) => { if (res.data.code === 200) { uni.setStorageSync('token', res.data.data.token); uni.switchTab({ url: '/pages/index/index' }); } else { uni.showToast({ title: res.data.message, icon: 'none' }); } } });

这里必须强调一个工程原则:注册登录链路中,MD5 是后端用来“处理密码摘要与落库”,不是用来“加密传输”。网络传输安全靠 HTTPS 保证。如果项目要求更高安全性,前端可以使用 RSA 公钥加密密码再传输,后端用私钥解密后再做 MD5 或 BCrypt 摘要。

6. 在 AI 推荐接口与视频推荐场景中使用签名

6.1 为什么需要接口签名

智能视频推荐小程序中,后端会暴露两类接口:普通业务接口和 AI 推荐接口。普通业务接口有登录态控制,使用 JWT 即可。AI 推荐接口可能出现这样的问题:

  • AI 推荐参数被恶意篡改,例如把用户 ID 换成另一个用户,导致越权访问。
  • 外部脚本反复调用推荐接口,消耗服务器资源。
  • 推荐结果回传时被中间人替换,导致前端展示异常内容。

针对这些问题,可以在关键接口上增加 MD5 签名校验。签名规则是:将请求参数按字典序排序,拼接字符串,再附加一个双方约定的密钥,最后计算 MD5 作为签名字段。

6.2 签名工具类实现

package com.example.videorecommend.common.util; import java.util.Map; import java.util.TreeMap; /** * 接口签名工具类 * 规则:参数按 key 字典序排序,拼接 key=value&...&key=value&key=secret * 最终计算 MD5 */ public class SignUtil { private static final String SECRET = "your-ai-secret-key"; private SignUtil() { } public static String generateSign(Map<String, String> params) { // TreeMap 会自动按 key 排序 Map<String, String> sortedParams = new TreeMap<>(params); StringBuilder sb = new StringBuilder(); for (Map.Entry<String, String> entry : sortedParams.entrySet()) { if (entry.getValue() == null || entry.getValue().isEmpty()) { continue; } sb.append(entry.getKey()) .append("=") .append(entry.getValue()) .append("&"); } sb.append("key=").append(SECRET); return Md5Util.md5(sb.toString()); } public static boolean verifySign(Map<String, String> params, String sign) { String expected = generateSign(params); return expected.equalsIgnoreCase(sign); } }

6.3 在视频推荐接口中接入

package com.example.videorecommend.controller; import com.example.videorecommend.common.util.SignUtil; import org.springframework.web.bind.annotation.*; import java.util.HashMap; import java.util.Map; @RestController @RequestMapping("/api/recommend") public class RecommendController { @PostMapping("/ai-videos") public Map<String, Object> aiVideos(@RequestBody AiRecommendRequest request) { // 参数校验与签名校验可以放在拦截器里,这里演示手动校验 Map<String, String> params = new HashMap<>(); params.put("userId", request.getUserId()); params.put("category", request.getCategory()); params.put("timestamp", request.getTimestamp()); if (!SignUtil.verifySign(params, request.getSign())) { return Map.of("code", 401, "message", "签名校验失败"); } // 这里调用 LangChain4j / SpringAI 相关服务生成推荐结果 // 本讲不做展开 return Map.of("code", 200, "message", "推荐成功"); } public static class AiRecommendRequest { private String userId; private String category; private String timestamp; private String sign; public String getUserId() { return userId; } public void setUserId(String userId) { this.userId = userId; } public String getCategory() { return category; } public void setCategory(String category) { this.category = category; } public String getTimestamp() { return timestamp; } public void setTimestamp(String timestamp) { this.timestamp = timestamp; } public String getSign() { return sign; } public void setSign(String sign) { this.sign = sign; } } }

6.4 前端签名计算示例

如果签名在 Uniapp 端计算,需要和后端保持完全相同的排序与拼接规则。由于 API 的请求体比较复杂,推荐使用 Hash 模式把userId、category、timestamp三个字段传给签名工具,同时后端排除sign字段。

// utils/sign.js function md5(str) { // 使用 js-md5 库 const md5 = require('js-md5'); return md5(str); } function generateSign(params, secret) { const keys = Object.keys(params).sort(); let str = ''; for (let key of keys) { if (params[key] !== '' && params[key] !== null) { str += `${key}=${params[key]}&`; } } str += `key=${secret}`; return md5(str); } module.exports = { generateSign };

这里有一个实际项目经常踩的坑:签名参数必须排除sign本身,否则拼接结果会出现循环依赖。另一个坑是时间戳校验,建议后端把timestamp与当前时间的差值限制在 5 分钟以内,超过直接拒绝。

7. 单元测试与接口联调验证

7.1 工具类单元测试

package com.example.videorecommend.common.util; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; class Md5UtilTest { @Test void md5_knownInput_shouldReturnExpectedValue() { // 已知 md5("123456") = e10adc3949ba59abbe56e057f20f883e String md5 = Md5Util.md5("123456"); Assertions.assertEquals("e10adc3949ba59abbe56e057f20f883e", md5); } @Test void md5_sameInput_shouldReturnSameValue() { String first = Md5Util.md5("video-recommend"); String second = Md5Util.md5("video-recommend"); Assertions.assertEquals(first, second); } @Test void md5_diffInput_shouldReturnDifferentValue() { String first = Md5Util.md5("video-recommend"); String second = Md5Util.md5("video-recommend2"); Assertions.assertNotEquals(first, second); } @Test void md5WithSalt_samePasswordDifferentSalt_shouldReturnDifferentValue() { String pwd1 = PasswordUtil.md5WithSalt("123456", "salt-1"); String pwd2 = PasswordUtil.md5WithSalt("123456", "salt-2"); Assertions.assertNotEquals(pwd1, pwd2); } @Test void matches_correctPassword_shouldReturnTrue() { String salt = PasswordUtil.generateSalt(); String encoded = PasswordUtil.md5WithSalt("123456", salt); Assertions.assertTrue(PasswordUtil.matches("123456", salt, encoded)); Assertions.assertFalse(PasswordUtil.matches("wrong", salt, encoded)); } }

7.2 注册登录接口联调流程

启动 SpringBoot 应用后,按下面步骤验证:

  1. 启动项目,默认端口 8080。
  2. 注册用户:
curl -X POST http://127.0.0.1:8080/api/auth/register \ -H "Content-Type: application/json" \ -d '{"username":"test01","password":"123456"}'
  1. 查看数据库t_user表,确认salt字段有随机值,password字段是 32 位十六进制字符串。
  2. 登录测试:
curl -X POST http://127.0.0.1:8080/api/auth/login \ -H "Content-Type: application/json" \ -d '{"username":"test01","password":"123456"}'
  1. 错误密码测试:
curl -X POST http://127.0.0.1:8080/api/auth/login \ -H "Content-Type: application/json" \ -d '{"username":"test01","password":"654321"}'

预期行为:正确密码返回code: 200,错误密码返回code: 500或自定义错误码。

7.3 API 签名接口的 curl 验证

先手动生成签名。假设参数是userId=1001、category=comedy、timestamp=1700000000,密钥是your-ai-secret-key,拼接待签名字符串:

category=comedy&timestamp=1700000000&userId=1001&key=your-ai-secret-key

然后请求:

curl -X POST http://127.0.0.1:8080/api/recommend/ai-videos \ -H "Content-Type: application/json" \ -d '{ "userId": "1001", "category": "comedy", "timestamp": "1700000000", "sign": "上面计算出的签名" }'

如果签名正确,返回推荐成功;如果随意修改userId或category字段,签名校验会失败,返回 401。

8. 常见问题与排查方法

问题现象可能原因排查方式解决方案
前后端 MD5 结果不一致编码方式不同;字符串包含不可见字符统一使用 UTF-8;输出格式统一为小写十六进制后端打印拼接字符串,前端逐字符比对
登录时密码校验失败盐值未正确存储或读取;密码摘要拼接顺序不一致检查数据库中的 salt 字段;检查 PasswordUtil 的拼接顺序保证注册和登录使用同一个 PasswordUtil
文件 MD5 计算超时视频文件过大,单线程读取慢打印耗时;检查文件流是否关闭加大缓冲;异步处理;考虑只抽取前 N 字节做初步校验
接口签名校验失败参数排序不一致;包含空值;sign 字段被拼接在后端临时打印签名字符串使用 TreeMap 排序;过滤空值;排除 sign 字段
数据库直接存 MD5,数据库被盗后密码泄露裸 MD5 易被破解检查 password 字段是否带盐已上线项目可增加 salt 字段并做摘要迁移,新项目直接使用 BCrypt
注册时有相同用户名同时提交并发注册存在竞态条件查看数据库唯一索引为 username 添加唯一索引,捕获重复键异常
Uniapp 请求提示 415 或 400Content-Type 未设置或参数格式错误查看浏览器 Network 面板设置Content-Type: application/json
时间戳签名过期客户端与服务器时区不一致打印双方当前时间统一使用时间戳秒;放宽时间窗口到 5 分钟;校准服务器时间

9. 最佳实践与使用建议

9.1 密码存储升级:从 MD5 加盐到 BCrypt

本讲为了教学演示,使用了 MD5 加盐方案。在真实项目中,更推荐直接使用 Spring Security 自带的BCryptPasswordEncoder,或者引入 jBCrypt。BCrypt 自带盐值,每次加密结果都不同,而且计算速度远慢于 MD5,能有效对抗暴力破解。

import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; public class BCryptExample { private static final BCryptPasswordEncoder ENCODER = new BCryptPasswordEncoder(); public static void main(String[] args) { String rawPassword = "123456"; String encoded1 = ENCODER.encode(rawPassword); String encoded2 = ENCODER.encode(rawPassword); System.out.println(encoded1); System.out.println(encoded2); // 每次输出都不一样,但 matches 结果都为 true System.out.println(ENCODER.matches(rawPassword, encoded1)); System.out.println(ENCODER.matches(rawPassword, encoded2)); } }

如果已经上线了 MD5 密码的用户,可以在登录逻辑里判断密码摘要前缀,命中旧格式就自动升级为 BCrypt 并更新数据库,实现平滑迁移。

9.2 接口签名的最佳实践

  • 签名密钥不要硬编码在 front-end 代码中。Uniapp 端如果必须计算签名,密钥需要做混淆并定期更换;更稳妥的方案是登录后由后端下发临时密钥,或者把计算签名移到后端专用接口。
  • 签名必须带时间戳,并加一个 5 分钟的时间窗口。
  • 重要接口加请求频率限制,例如同一个用户每 10 秒最多调用一次 AI 推荐接口。
  • Auditor 不要只在 Controller 手动校验,建议抽成拦截器或注解,例如@SignCheck。

9.3 工具类的工程规范

  • 工具类构造方法私有化,方法全部静态。
  • 输入参数做非空校验,避免空指针。
  • 文件流使用 try-with-resources 自动关闭。
  • 不要在工具类里混入业务参数,例如把SECRET单独抽到一个配置类。
  • 工具类的日志使用LoggerFactory.getLogger,不要打印签名密钥。

9.4 小程序端的合规建议

  • 用户注册协议必须写明密码摘要存储方式和数据用途。
  • 视频推荐系统会采集观看记录、点击行为、搜索关键词,需要在隐私政策中明确说明,并获得用户授权。
  • AI 推荐功能如果用到了用户的历史行为数据,只能在授权范围内使用,不能超范围收集。
  • 如果涉及未成年人,必须依法取得监护人同意,并做数据最小化处理。

10. 总结与下一步

这一讲重点落在 MD5 工具类的实现与业务接入,表面上只是写一个Md5Util,实际上解决了三类问题:

  • 用户注册登录时的密码摘要与校验流程;
  • AI 推荐接口的签名防篡改;
  • 视频素材文件完整性校验的通用方案。

从代码层面看,最值得先把Md5Util和PasswordUtil两个类跑通,再用 curl 或 Uniapp 调一次注册登录接口,最后把签名工具集成到推荐接口的拦截器中。最容易踩的坑不是 MD5 算法本身,而是编码不一致、盐值存储不统一、签名拼接顺序不一致。这三类问题只要按第 8 节的排查思路处理,基本都能快速定位。

下一步建议优先做这几件事:给t_user表增加唯一索引,防止并发注册问题;把密码存储升级为 BCrypt;把签名校验从 Controller 抽到拦截器;最后配合 LangChain4j + SpringAI 的推荐逻辑,完成 AI 视频推荐接口的联调。这样,整个小程序后端的认证与接口安全体系就能稳定跑起来,后续再接入 Apache ShardingSphere、Redis 缓存、消息队列时,也不会因为基础工具类出问题。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询