在智能视频推荐小程序的开发过程中用户登录、接口签名、缓存 Key 生成这些看似不起眼的底层工具往往是线上问题最多的环节。尤其是当项目同时涉及 Uniapp 小程序端、SpringBoot 后端、SpringAI 和 LangChain4j 的 AI 能力时接口的稳定性和安全性会直接影响推荐效果。很多问题并不是推荐算法本身而是签名不一致、参数被篡改、缓存 Key 设计不合理这类基础功能导致的。本文是“基于 SpringBoot Vue Uniapp LangChain4j SpringAI 的智能视频推荐小程序”系列教程的第十讲重点补齐后端 common 模块中的 MD5 工具类设计、代码实现以及它在推荐项目中的实际用法。同时也会提醒大家MD5 能用在哪、不能用在哪。学完这一讲你除了能直接复制一个 MD5 工具类落地到 Spring Boot 项目里还会对签名校验、密码加密的正确姿势有更清晰的认识。适合读者已经跑通前几讲的读者、正在做前后端分离项目但缺少公共工具类封装经验的开发者、需要在 Uniapp 端和后端之间做接口安全校验的同学。1. 背景与核心概念1.1 什么是 MD5MD5 全称 Message-Digest Algorithm 5中文常译为“消息摘要算法第五版”。它是一种被广泛使用的哈希函数能够把任意长度的输入数据计算生成一个固定长度 128 位、通常用 32 位十六进制字符串表示的摘要值。一句话解释MD5 不是加密算法而是摘要算法。加密算法是可逆的拥有密钥后可以还原原始内容MD5 不可逆无法从摘要反推原始内容。所以它更适合用来做完整性校验、指纹生成而不是用来隐藏密码等敏感数据。在很多项目中MD5 常用于以下场景接口防篡改请求参数 时间戳 密钥拼起来计算 MD5服务端重新计算并比对不一致就拒绝请求。文件完整性校验上传文件后对文件字节计算 MD5用于比对是否与源文件一致。缓存 Key 指纹当缓存 Key 由多个字段组成且比较长时统一拼接后取 MD5 作为短 Key。唯一标识生成结合业务字段生成固定长度标识便于索引和存储。在智能视频推荐小程序中这些场景都有体现。例如用户在小程序端搜索“科幻 动作 2024”搜索条件是一组组合条件直接把 JSON 当作缓存 Key 既浪费内存又容易超出 Redis 的 Key 长度限制。更好的做法是对条件串计算 MD5将其结果作为缓存 Key。1.2 为什么视频推荐系统需要 MD5 工具类推荐系统内部依赖大量中间结果用户行为日志、视频内容标签、AI 模型结果。接口暴露给 Uniapp 端之后如果参数被人恶意修改可能导致推荐结果错乱甚至出现刷量、刷接口等问题。通过 MD5 生成请求签名后端可以快速识别参数有没有被篡改。同时项目里接了 LangChain4j 和 SpringAIAI 对话、内容生成这类接口往往也是高耗时操作。我们通常会把用户问题、上下文摘要、视频特征进行组合生成一个指纹作为 Redis 缓存 Key缓存 AI 推荐结果避免重复计算。这个指纹同样可以用 MD5 工具类完成。从工程角度看MD5 工具类是 common 模块最基础的能力之一。它不依赖 Spring 容器不依赖第三方中间件就是一个纯 Java 工具类。但越是简单的工具类越容易在细节上出错比如字节转换、字符集、线程安全、大小写统一等。这也是本讲单独用一篇文章讲透它的原因。1.3 MD5、SHA-1、SHA-256 怎么选MD5 输出 128 位摘要SHA-1 输出 160 位SHA-256 输出 256 位。在抗碰撞性上SHA-256 明显优于 MD5。如果是安全等级要求高的签名场景可以优先选择 SHA-256MD5 适合性能要求高、安全要求不极端、主要用于内部校验和指纹生成的场景。本文并不是让大家在所有场景无脑使用 MD5。一个合格的工具包需要具备扩展空间所以下面代码中我会把摘要算法名称作为参数开放方便后续切换为 SHA-256。2. 环境准备与版本说明2.1 环境依赖因为 MD5 算法是 JDK 自带的java.security.MessageDigest所以不需要引入第三方依赖。只要你的项目是标准 Java 工程能编译运行就可以直接使用。JDK8 或 11/17 都可以本文示例以 JDK 8 兼容写法为主避免新特性导致老项目无法编译。Spring Boot2.x 或 3.x 均可工具类本身不依赖 Spring 容器只是一个普通 Java 类。构建工具Maven 或 Gradle需要能把类编译进 classpath。IDEIDEA、Eclipse、VS Code 都可以。其他组件Redis、MySQL、Vue、Uniapp、LangChain4j、SpringAI 属于业务链路组件本讲暂不直接依赖。需要注意如果你使用的是 Spring Boot 3.xJDK 版本至少是 17如果是 Spring Boot 2.xJDK 8 也可以。不同版本不影响 MD5 工具类代码本身。2.2 工具类应该放在哪个模块本系列项目结构建议如下smart-video-recommend/ ├── backend/ │ ├── smart-video-common/ // 公共模块工具类、常量、统一返回值 │ ├── smart-video-system/ // 系统模块用户、权限、视频管理 │ ├── smart-video-recommend/ // 推荐模块推荐算法、AI 接入 │ └── smart-video-admin/ // 管理后台Vue 项目 ├── uniapp/ // 小程序端 └── sql/ // 数据库脚本MD5 工具类放在backend/smart-video-common/src/main/java/com/example/common/util/MD5Util.java比较合适。如果项目没有拆分多模块放在common/util包下也行后续所有模块复用。2.3 本讲代码约定为了便于演示我会以单模块项目的包路径com.example.common.util来写。你实际开发时可以根据公司规范调整包名不影响功能。3. MD5 工具类设计与核心代码实现3.1 基于 MessageDigest 的最小实现先看最简单的实现输入字符串输出 32 位小写 MD5。package com.example.common.util; import java.nio.charset.StandardCharsets; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; public class MD5Util { /** * 计算字符串的 MD5 值小写 * * param text 原始字符串 * return 32 位小写 MD5 */ public static String md5(String text) { if (text null) { return null; } try { MessageDigest md MessageDigest.getInstance(MD5); byte[] bytes md.digest(text.getBytes(StandardCharsets.UTF_8)); StringBuilder sb new StringBuilder(); for (byte b : bytes) { // 0xff 是为了把 byte 转成 0~255 的 int然后按 16 进制输出 String hex Integer.toHexString(b 0xff); if (hex.length() 1) { sb.append(0); } sb.append(hex); } return sb.toString(); } catch (NoSuchAlgorithmException e) { // JDK 必然包含 MD5 算法这里做兜底处理 throw new IllegalStateException(MD5 algorithm not found, e); } } }解释一下关键点text.getBytes(StandardCharsets.UTF_8)指定 UTF-8避免跨平台乱码。不同系统默认字符集可能不同如果不指定同样的字符串在不同环境下算出的 MD5 可能不一样。b 0xff因为 byte 是有符号的直接Integer.toHexString(b)在高位会补f产生错误结果。要与0xff做与运算把值转换到 0~255 的范围内。长度不足 2 位时前面补 0保证每个字节都输出两位十六进制最终结果一定为 32 位。MessageDigest.getInstance(MD5)每调用一次都会创建实例存在一定开销后面会用 ThreadLocal 优化。3.2 支持参数数组与大文件在实际业务中经常需要把多个字段拼接后计算摘要。我们可以提供重载方法接收Object... arguments内部统一拼接。同时文件上传场景需要计算 InputStream 的摘要。增强版工具类代码如下package com.example.common.util; import java.io.IOException; import java.io.InputStream; import java.nio.charset.StandardCharsets; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; public class MD5Util { private MD5Util() { } public static String md5(String text) { if (text null) { return null; } return md5(text.getBytes(StandardCharsets.UTF_8)); } public static String md5(byte[] data) { if (data null) { return null; } return md5Digest(data); } /** * 多个参数拼接后计算 MD5避免调用方拼串出错 */ public static String md5(Object... params) { if (params null || params.length 0) { return md5(); } StringBuilder sb new StringBuilder(); for (Object param : params) { sb.append(param null ? : param.toString()); } return md5(sb.toString()); } /** * 计算输入流的 MD5适合文件校验 */ public static String md5(InputStream inputStream) throws IOException { if (inputStream null) { return null; } MessageDigest md getMessageDigest(); byte[] buffer new byte[8192]; int len; while ((len inputStream.read(buffer)) ! -1) { md.update(buffer, 0, len); } return toHex(md.digest()); } private static String md5Digest(byte[] data) { MessageDigest md getMessageDigest(); byte[] digest md.digest(data); return toHex(digest); } private static MessageDigest getMessageDigest() { try { return MessageDigest.getInstance(MD5); } catch (NoSuchAlgorithmException e) { throw new IllegalStateException(MD5 algorithm not found, e); } } private static String toHex(byte[] bytes) { StringBuilder sb new StringBuilder(); for (byte b : bytes) { String hex Integer.toHexString(b 0xff); if (hex.length() 1) { sb.append(0); } sb.append(hex); } return sb.toString(); } }这段代码有几个设计点私有构造方法防止外部实例化。工具类应该是静态方法集合不需要创建对象。md5(Object... params)适合多个字段拼接。但要注意如果两个参数拼接结果相同比如md5(ab, c)和md5(a, bc)结果会一样。为了避免歧义建议使用 3.4 节中的带分隔符方法。InputStream 版本使用 8KB 缓冲区避免一次性把大文件读入内存适合视频封面、推广素材等文件校验。3.3 加盐 MD5 与随机盐纯 MD5 容易被彩虹表破解所以如果某些低敏感场景需要加盐我们可以允许传入盐值或者生成随机盐。注意加盐 MD5 仍然不适合密码存储只适合内部签名算法的辅助手段。/** * 使用固定盐值计算 MD5 * 拼接规则MD5(盐 原文 盐) */ public static String md5WithSalt(String text, String salt) { if (text null) { return null; } return md5(salt text salt); }但更规范的做法是明确盐与文本拼接规则前后端统一。实际项目里可以约定MD5(盐 原文)或者MD5(原文 盐)。只要两端一致就能校验。如果要生成随机盐可以用如下方法public static String generateSalt(int length) { SecureRandom random new SecureRandom(); StringBuilder sb new StringBuilder(); String chars ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789; for (int i 0; i length; i) { sb.append(chars.charAt(random.nextInt(chars.length()))); } return sb.toString(); }注意随机盐要使用java.security.SecureRandom不要使用Math.random()。SecureRandom提供密码学安全的随机数适合生成盐值。3.4 使用 ThreadLocal 提升并发性能MessageDigest实例不是线程安全的。如果每次都调用getInstance在高并发下会有少量性能开销但更危险的是某些错误做法在 Spring 容器里把MessageDigest定义成单例 Bean 后多线程共用会导致计算结果错乱。正确做法是使用ThreadLocalMessageDigest。private static final ThreadLocalMessageDigest DIGEST_THREAD_LOCAL ThreadLocal.withInitial(() - { try { return MessageDigest.getInstance(MD5); } catch (NoSuchAlgorithmException e) { throw new IllegalStateException(MD5 algorithm not found, e); } }); private static MessageDigest getMessageDigest() { return DIGEST_THREAD_LOCAL.get(); }注意MessageDigest在调用digest()之后会自动重置状态所以用 ThreadLocal 复用是安全的。但更简单的方式是每次新建实例。对于工具类绝大多数项目直接每次getInstance也没问题。是否引入 ThreadLocal取决于团队对性能的要求。如果项目 QPS 不高不必过度设计。3.5 是否要引入 commons-codecApache Commons Codec 提供了DigestUtils.md5Hex(text)非常方便。如果项目中已经引入了commons-codec可以直接使用如果没有为了一个方法引入整个依赖不太划算。自己封装一个工具类还能根据项目规范定制盐拼接、大小写、参数数组等功能。所以我更推荐在 common 模块里自研轻量工具类。但也要说明如果团队统一要求使用稳定、经过验证的库避免重复造轮子那么使用 Commons Codec 或 Hutool 的DigestUtil.md5Hex也是合理选择。教学场景下手写MessageDigest更有利于理解原理。4. 在智能视频推荐小程序中的实际应用4.1 场景一接口签名防篡改Uniapp 小程序端向后端请求推荐接口时请求体里有userId、videoTag、pageNum等参数。为了防止参数被第三方工具篡改小程序端和后端约定一套简单签名规则。这里给出一种通用签名流程将业务参数按照字典序排序。拼接成keyvaluekeyvalue字符串。在末尾追加时间戳timestamp和密钥secretKey。对整串计算 MD5得到sign。请求头或参数中携带sign和timestamp。后端在拦截器中校验签名。核心代码如下Component public class SignInterceptor implements HandlerInterceptor { Value(${app.sign.secret-key:smart-video-2024}) private String secretKey; // 允许的时间偏差单位毫秒 private static final long ALLOWED_TIME_DRIFT 5 * 60 * 1000L; Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String sign request.getParameter(sign); String timestamp request.getParameter(timestamp); if (StringUtils.isEmpty(sign) || StringUtils.isEmpty(timestamp)) { throw new BusinessException(签名参数缺失); } // 1. 校验时间戳防止重放攻击 long ts Long.parseLong(timestamp); if (Math.abs(System.currentTimeMillis() - ts) ALLOWED_TIME_DRIFT) { throw new BusinessException(请求已过期); } // 2. 获取业务参数生成待签名串 // 注意这里要排除 sign、timestamp 两个参数 MapString, String[] parameterMap request.getParameterMap(); SortedMapString, String sortedMap new TreeMap(); for (Map.EntryString, String[] entry : parameterMap.entrySet()) { if (sign.equals(entry.getKey()) || timestamp.equals(entry.getKey())) { continue; } // 如果参数值是多选取第一个值仅作为示例 sortedMap.put(entry.getKey(), entry.getValue()[0]); } StringBuilder sb new StringBuilder(); for (Map.EntryString, String entry : sortedMap.entrySet()) { if (sb.length() 0) { sb.append(); } sb.append(entry.getKey()).append().append(entry.getValue()); } sb.append(timestamp).append(timestamp).append(secretKey).append(secretKey); String serverSign MD5Util.md5(sb.toString()); if (!serverSign.equals(sign)) { throw new BusinessException(签名校验失败); } return true; } }同时需要注册拦截器Configuration public class WebConfig implements WebMvcConfigurer { Resource private SignInterceptor signInterceptor; Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(signInterceptor) .addPathPatterns(/api/video/recommend/**) .excludePathPatterns(/api/auth/login, /error); } }这里要说明几点secretKey不要写死在代码里建议放到配置中心或环境变量。示例中使用了Value注入且给了默认值仅用于本地开发。签名串拼接规则前后端必须完全一致否则会验签失败。最常见的排错方式就是打印出前后端拼接的原文进行比对。推荐接口必须走 HTTPS防止明文参数和密钥被抓包。MD5 签名能防篡改但不能防窃听HTTPS 是不可替代的。4.2 场景二生成 Redis 缓存 Key智能视频推荐模块中用户请求推荐结果时推荐条件可能包含视频分类、标签、排序方式、时间范围、用户 ID 等。如果直接用这些字符串拼 Redis Key不仅长而且可读性差。改造前String cacheKey video:recommend: userId : videoTag : category : sortType : pageNum;改造后String rawKey userId : videoTag : category : sortType : pageNum; String cacheKey video:recommend: MD5Util.md5(rawKey);这样 Key 长度固定也便于 Redis 管理。需要注意如果把pageNum加进去不同页码会生成不同缓存推荐列表分页缓存量会比较大。实际项目中通常只缓存前几页或基于最后推荐时间缓存而不是缓存全部分页。这个话题会在后面的缓存设计一讲展开。4.3 场景三AI 推荐结果缓存指纹项目使用 SpringAI 和 LangChain4j 对接大模型通常把「用户问题 视频上下文摘要 推荐规则」组合后发送给模型。如果模型结果比较稳定可以把这个组合串做 MD5 指纹作为 Redis 缓存 Key缓存一段时间避免重复请求大模型降低成本和延迟。示例代码Service public class AiRecommendService { Resource private StringRedisTemplate stringRedisTemplate; public String getAiRecommend(String userId, String question, String videoContext) { String fingerprint MD5Util.md5(userId, question, videoContext); String cacheKey ai:recommend: fingerprint; String cached stringRedisTemplate.opsForValue().get(cacheKey); if (cached ! null) { return cached; } // 调用 LangChain4j / SpringAI 接口生成推荐结果 String result callAiModel(userId, question, videoContext); // 缓存 10 分钟防止命中过期数据 stringRedisTemplate.opsForValue().set(cacheKey, result, Duration.ofMinutes(10)); return result; } private String callAiModel(String userId, String question, String videoContext) { // 这里省略真正的模型调用逻辑实际开发中可调用 LangChain4j 或 SpringAI 的 API return 推荐结果; } }这里MD5Util.md5(userId, question, videoContext)等价于把三个字符串直接拼接后做 MD5。虽然碰撞概率极低但为了规范和避免歧义建议在拼接时加入明确分隔符。可以扩展一个带分隔符的方法public static String md5WithSeparator(String separator, Object... params) { StringBuilder sb new StringBuilder(); for (Object param : params) { if (param null) { continue; } if (sb.length() 0) { sb.append(separator); } sb.append(param); } return md5(sb.toString()); }然后在 AI 推荐服务中使用String fingerprint MD5Util.md5WithSeparator(#, userId, question, videoContext);这样能明显降低字段拼接歧义同时让缓存指纹的含义更清晰。4.4 场景四文件完整性校验视频推荐后台需要运营人员上传视频封面、推广素材。上传完成后后端可以计算文件的 MD5与前端传入的 MD5 做比对确保上传过程中文件没有被损坏。PostMapping(/upload) public ResultString upload(RequestParam(file) MultipartFile file, RequestParam(clientMd5) String clientMd5) throws IOException { String serverMd5 MD5Util.md5(file.getInputStream()); if (!serverMd5.equalsIgnoreCase(clientMd5)) { return Result.fail(文件校验失败请重新上传); } // 保存文件并返回访问路径 String url fileService.store(file); return Result.ok(url); }这个用法很常见适合大文件校验。注意equalsIgnoreCase忽略大小写因为小程序端和后端生成 MD5 的大小写可能不一致。5. 密码存储为什么不要用 MD55.1 MD5 用于密码存储的风险在一些老项目中经常看到password MD5(password)直接入库。这种做法非常危险无法防彩虹表攻击者可以预计算大量常见密码的 MD5 值反向匹配得到明文密码。MD5 计算速度太快普通硬件环境下每秒可以尝试海量密码暴力破解成本极低。无盐时相同密码产生相同摘要攻击者破解一次所有相同密码的用户都暴露。如果本项目要开发用户登录注册强烈建议使用 Spring Security 的BCryptPasswordEncoder或 Argon2。即便使用加盐 MD5也只是延缓破解并不能保证安全。5.2 在 Spring Security 中使用 BCrypt如果项目已经集成 Spring Security可以这样配置Configuration public class SecurityConfig { Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } }注册时user.setPassword(passwordEncoder.encode(rawPassword));登录校验时passwordEncoder.matches(rawPassword, user.getPassword());BCrypt 的特点每次加密使用的盐是随机生成的。计算时间可以调整增加暴力破解成本。哈希结果包含盐和版本信息存储格式通常为$2a$10$...。所以本讲实现的 MD5 工具类请用在签名、指纹、校验这些场景不要用于密码存储。这是安全底线。5.3 如果项目没有集成 Spring Security 怎么做如果还没有引入 Spring Security可以考虑使用 jBCrypt 库或者使用 Spring Security crypto 模块中的BCryptPasswordEncoder而不引入完整 Security 依赖。依赖示例dependency groupIdorg.springframework.security/groupId artifactIdspring-security-crypto/artifactId !-- 版本跟随 Spring Boot BOM 管理不建议手动写死 -- /dependency注意如果项目使用 Spring Boot 3.x对应 spring-security-crypto 6.x。建议直接使用 starter 自带的依赖版本避免版本冲突。6. 常见问题与排查思路6.1 问题速查表问题现象常见原因解决思路同样字符串前后端计算出的 MD5 不一致字符集不同或签名字段拼接顺序不一致统一使用 UTF-8统一排序和拼接规则中文参数验签失败getParameter 读取后编码混乱检查 Tomcat URI 编码配置 server.tomcat.uri-encodingUTF-8或对参数统一转码MD5 结果出现多余 Fbyte 转 int 时没有与 0xff 做与运算使用 b 0xff 后再转十六进制结果只有 31 位十六进制转换时没有补前导零小于 16 的字节前面补 0同一线程多次计算结果不同多个地方共用同一个 MessageDigest 实例每次计算新实例或使用 ThreadLocal验签时原始参数已被框架解码sign 用的是 URL 编码前字符串建议使用 POST body 流式验签或规范前后端编码规则代码里用 MD5 加密密码被扫描工具报漏洞MD5 不适合密码存储改用 BCrypt 或 Argon2 等密码哈希算法6.2 签名校验失败的排查步骤如果遇到“签名校验失败”可以按以下顺序排查确认前后端字符集都是 UTF-8尤其是小程序端encodeURIComponent后的参数。确认参数排序方式一致是否按照字典序排列。确认拼接时是否包含签名参数和时间戳以外的额外参数。确认时间戳字段有没有参与排序或者参与排序但前后顺序一致。打印服务端和客户端计算出的待签名字符串肉眼对比差异。检查密钥是否一致有没有多余的空格、换行符。如果请求走 GET注意参数值在 URL 中是否被转义比如%20、等。实际项目中90% 的验签失败都是拼接规则不一致造成的。所以在设计签名规则时一定要把规则写成文档前后端各留一份。6.3 关于 MD5 碰撞MD5 已经被证明存在碰撞攻击即两个不同内容可以计算出相同的 128 位摘要。对于一般业务参数签名场景由于密钥和业务参数是绑定的攻击者要让碰撞生效还需要同时知道密钥并构造出合法业务参数难度较大。但如果做文件指纹、数字证书这类高安全场景应升级到 SHA-256 或更高。这也是我建议工具类预留算法参数的原因。7. 最佳实践与工程建议7.1 工具类设计规范构造函数私有化禁止外部实例化。方法名语义清晰md5、md5WithSalt、md5WithSeparator。所有输入参数判空抛出统一异常或返回 null取决于团队约定。字符集固定为 UTF-8避免换机后结果变化。常量提取密钥、盐值不能散落在代码里。返回值统一小写。如果下游需要大写可以额外提供md5Upper方法但内部统一小写。7.2 签名校验的最佳实践签名参数名不要随意暴露密钥。timestamp允许时间偏差建议 5 分钟内。太长容易重放太短会影响用户体验。敏感接口要加访问频率限制防止暴力遍历签名。在非加密通道下明文参数和 MD5 签名都可被转发。签名只能防篡改不能防窃听必须配 HTTPS。生产环境密钥通过配置中心管理定期轮换。可以添加nonce随机数服务端缓存已使用过的nonce进一步防止重放攻击。7.3 使用第三方工具类的经验如果项目里已经有 Hutool直接用SecureUtil.md5(value)更省事。如果已经有 commons-codec用DigestUtils.md5Hex(value)。如果都没有再考虑自己封装。封装时尽量复用项目已有的异常体系避免工具类直接抛出裸异常。7.4 单元测试用例工具类必须配套单元测试至少覆盖以下场景package com.example.common.util; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNull; class MD5UtilTest { Test void testMd5KnownValue() { // hello 的已知 MD5 值 String hash MD5Util.md5(hello); assertEquals(5d41402abc4b2a76b9719d911017c592, hash); } Test void testMd5Null() { assertNull(MD5Util.md5((String) null)); } Test void testMd5Empty() { // d41d8cd98f00b204e9800998ecf8427e 是空串的 MD5 assertEquals(d41d8cd98f00b204e9800998ecf8427e, MD5Util.md5()); } Test void testMd5WithSalt() { String hash MD5Util.md5WithSalt(123456, abc); assertEquals(MD5Util.md5(abc123456abc), hash); } }注意如果项目使用 JUnit4请对应导入org.junit.Test。这里的已知 MD5 值为通用测试值可以在开发环境中验证。7.5 日志规范不要在日志中打印明文密码、密钥、完整签名串。打印待签名字符串时可对关键字段脱敏。推荐使用统一日志框架SLF4J Logback不要使用System.out.println。线上出现验签失败时日志中只需要记录 userId、timestamp、sign 前缀不要记录完整 secretKey。8. 总结与下一步本讲围绕智能视频推荐小程序项目中的 MD5 工具类展开从MessageDigest底层实现到工具类设计、加盐、ThreadLocal 优化再到接口签名、缓存 Key、AI 缓存指纹、文件校验等实际场景。最重要的一点是MD5 可以用于摘要、防篡改、指纹生成但不适合密码存储密码存储建议换 BCrypt。如果你正在做自己的独立项目建议先为 common 模块补齐这样一套基础工具类并配合 JUnit 测试。这样后续开发登录模块、推荐模块、上传模块时就会顺畅很多。下一讲可以继续分享智能视频推荐中的 Redis 缓存设计或者 LangChain4j 与 SpringAI 的推荐接入方式。如果这篇教程对你排查签名问题有帮助欢迎收藏需要使用时直接对照配置和代码排错。
阅读完成 · 觉得有帮助?