MD5 + SHA-1 详解
哈希加密工具类文档(MD5 + SHA-1 详解)
一、文档概述
本文档将详细介绍 MD5 和 SHA-1 两种经典哈希加密算法,包括算法原理、实现思路、核心区别,并提供通用可直接使用的加密工具类(基于 Java 原生 API,无第三方依赖),同时结合实际场景说明适用场景,帮助开发者快速理解和使用。
二、两种加密算法核心介绍
2.1 MD5 加密(Message-Digest Algorithm 5)
2.1.1 算法简介
- 全称:消息摘要算法第5版,是一种不可逆的哈希函数(无法从加密结果反推原始数据);
- 核心特征:无论输入数据长度如何,输出固定为 128位(16字节) 的摘要,通常以 32位十六进制字符串 表示;
- 设计用途:主要用于数据完整性校验(如文件校验)、密码加密存储(早期常用);
- 安全性:已被破解(存在碰撞风险,即不同数据可能生成相同 MD5 结果),不建议用于高安全性场景(如金融、核心密码存储)。
2.1.2 实现思路
- 输入处理:将原始字符串按指定编码(如 UTF-8)转为字节数组;
- 实例化算法:通过 Java 原生
MessageDigest.getInstance("MD5")获取 MD5 加密实例; - 生成摘要:调用
digest()方法对字节数组进行哈希计算,得到 16 字节原始摘要; - 格式转换:将 16 字节摘要转为 32 位十六进制字符串(1 字节 = 2 位十六进制),便于存储和传输。
2.2 SHA-1 加密(Secure Hash Algorithm 1)
2.2.1 算法简介
- 全称:安全哈希算法第1版,同样是不可逆的哈希函数;
- 核心特征:输入数据无论长度,输出固定为 160位(20字节) 的摘要,通常以 40位十六进制字符串 表示;
- 设计用途:数据完整性校验、密码加密存储(安全性高于 MD5);
- 安全性:比 MD5 更安全(摘要长度更长,碰撞概率极低),但仍属于弱哈希(2017年已被破解),目前推荐用于非核心安全场景。
2.2.2 实现思路
- 输入处理:与 MD5 一致,将原始字符串转为 UTF-8 字节数组;
- 实例化算法:通过
MessageDigest.getInstance("SHA-1")获取 SHA-1 加密实例; - 生成摘要:调用
digest()方法计算字节数组的哈希值,得到 20 字节原始摘要; - 格式转换:将 20 字节摘要转为 40 位十六进制字符串,完成加密。
三、MD5 与 SHA-1 核心区别
| 对比维度 | MD5 | SHA-1 |
|---|---|---|
| 摘要长度 | 128位(16字节)→ 32位十六进制串 | 160位(20字节)→ 40位十六进制串 |
| 加密速度 | 较快(计算逻辑相对简单) | 较慢(计算步骤更多) |
| 安全性 | 较低(存在碰撞风险,已被破解) | 较高(碰撞概率极低,破解成本更高) |
| 适用场景 | 低安全需求:文件校验、普通数据加密 | 中安全需求:密码存储、接口签名校验 |
| 输出字符串长度 | 固定32位(小写/大写可自定义) | 固定40位(小写/大写可自定义) |
四、通用加密工具类(完整实现)
4.1 工具类代码(HashUtil.java)
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Objects;/*** 哈希加密工具类(MD5 + SHA-1)* 特点:无第三方依赖、支持空值处理、可自定义编码和大小写、线程安全*/
public class HashUtil {// 默认编码(UTF-8,主流项目通用)private static final Charset DEFAULT_CHARSET = StandardCharsets.UTF_8;// 十六进制字符集(小写)private static final char[] HEX_CHARS_LOWER = "0123456789abcdef".toCharArray();// 十六进制字符集(大写)private static final char[] HEX_CHARS_UPPER = "0123456789ABCDEF".toCharArray();/*** MD5 加密(默认 UTF-8 编码,小写输出)* @param data 原始数据(可为 null)* @return 32位十六进制加密串(data 为 null/空串时返回空串)*/public static String md5(String data) {return md5(data, DEFAULT_CHARSET, false);}/*** MD5 加密(自定义编码和大小写)* @param data 原始数据(可为 null)* @param charset 字符编码(如 StandardCharsets.UTF_8)* @param upperCase 是否大写输出(true:大写,false:小写)* @return 32位十六进制加密串*/public static String md5(String data, Charset charset, boolean upperCase) {if (data == null || data.trim().isEmpty()) {return "";}return hash(data, "MD5", charset, upperCase);}/*** SHA-1 加密(默认 UTF-8 编码,小写输出)* @param data 原始数据(可为 null)* @return 40位十六进制加密串(data 为 null/空串时返回空串)*/public static String sha1(String data) {return sha1(data, DEFAULT_CHARSET, false);}/*** SHA-1 加密(自定义编码和大小写)* @param data 原始数据(可为 null)* @param charset 字符编码(如 StandardCharsets.UTF_8)* @param upperCase 是否大写输出(true:大写,false:小写)* @return 40位十六进制加密串*/public static String sha1(String data, Charset charset, boolean upperCase) {if (data == null || data.trim().isEmpty()) {return "";}return hash(data, "SHA-1", charset, upperCase);}/*** 双重加密(MD5 → SHA-1,默认 UTF-8 编码,小写输出)* 场景:需要比单一加密更高安全性的场景(如密码存储)* @param data 原始数据(可为 null)* @return 40位十六进制加密串*/public static String md5Sha1(String data) {if (data == null || data.trim().isEmpty()) {return "";}// 先 MD5 加密,再对 MD5 结果做 SHA-1 加密String md5Result = md5(data);return sha1(md5Result);}/*** 核心哈希加密方法(统一实现 MD5/SHA-1 逻辑)* @param data 原始数据* @param algorithm 加密算法("MD5" 或 "SHA-1")* @param charset 字符编码* @param upperCase 是否大写输出* @return 十六进制加密串*/private static String hash(String data, String algorithm, Charset charset, boolean upperCase) {try {// 1. 获取加密算法实例MessageDigest messageDigest = MessageDigest.getInstance(algorithm);// 2. 字符串转字节数组并计算摘要byte[] digest = messageDigest.digest(data.getBytes(charset));// 3. 字节摘要转为十六进制字符串return bytesToHex(digest, upperCase);} catch (NoSuchAlgorithmException e) {// 算法不存在时抛出运行时异常(理论上不会发生,Java 原生支持 MD5/SHA-1)throw new RuntimeException("加密算法 " + algorithm + " 不支持", e);}}/*** 字节数组转为十六进制字符串* @param bytes 原始字节数组* @param upperCase 是否大写输出* @return 十六进制字符串*/private static String bytesToHex(byte[] bytes, boolean upperCase) {Objects.requireNonNull(bytes, "字节数组不能为 null");char[] hexChars = upperCase ? HEX_CHARS_UPPER : HEX_CHARS_LOWER;char[] result = new char[bytes.length * 2];for (int i = 0; i < bytes.length; i++) {byte b = bytes[i];// 高4位转十六进制result[i * 2] = hexChars[(b >>> 4) & 0x0F];// 低4位转十六进制result[i * 2 + 1] = hexChars[b & 0x0F];}return new String(result);}
}
4.2 工具类使用示例
public class HashUtilDemo {public static void main(String[] args) {String rawData = "123456"; // 原始数据// 1. MD5 加密(默认配置:UTF-8 + 小写)String md5Result = HashUtil.md5(rawData);System.out.println("MD5 加密结果(小写):" + md5Result); // 输出:e10adc3949ba59abbe56e057f20f883e// 2. MD5 加密(UTF-8 + 大写)String md5UpperResult = HashUtil.md5(rawData, StandardCharsets.UTF_8, true);System.out.println("MD5 加密结果(大写):" + md5UpperResult); // 输出:E10ADC3949BA59ABBE56E057F20F883E// 3. SHA-1 加密(默认配置:UTF-8 + 小写)String sha1Result = HashUtil.sha1(rawData);System.out.println("SHA-1 加密结果(小写):" + sha1Result); // 输出:10470c3b4b1fed12c3baac014be15fac67c6e815// 4. 双重加密(MD5 → SHA-1)String md5Sha1Result = HashUtil.md5Sha1(rawData);System.out.println("MD5+SHA-1 双重加密结果:" + md5Sha1Result); // 输出:2fd4e1c67a2d28fced849ee1bb76e7391b93eb12}
}
五、工具类核心特性
- 无依赖:仅使用 Java 原生
MessageDigest和字符编码 API,无需引入第三方 Jar 包; - 空值安全:处理
null、空字符串、纯空格等异常输入,避免空指针异常; - 灵活配置:支持自定义字符编码(如 GBK)、输出大小写(小写默认,大写可选);
- 线程安全:
MessageDigest实例为方法内局部变量,无线程安全问题; - 功能完整:包含单一加密(MD5/SHA-1)和双重加密(MD5→SHA-1),覆盖大部分场景。
六、适用场景推荐
6.1 MD5 适用场景
- 文件完整性校验(如下载文件后校验 MD5 是否与官方一致,判断文件是否篡改或损坏);
- 普通数据加密(如非核心业务的用户昵称、设备ID等,仅需防明文存储);
- 缓存键生成(如将复杂查询条件 MD5 加密作为缓存键,减少键长度)。
6.2 SHA-1 适用场景
- 密码存储(比 MD5 更安全,适合非金融类项目的用户密码加密存储);
- 接口签名校验(如 API 接口请求参数拼接后 SHA-1 加密,防止参数被篡改);
- 日志数据校验(如重要日志的摘要存储,后续可校验日志是否被修改)。
6.3 双重加密(MD5→SHA-1)适用场景
- 高安全性密码存储(如管理后台用户密码、支付相关密码);
- 核心数据加密(如敏感配置项、密钥相关数据)。
七、注意事项
- 不可逆性:MD5 和 SHA-1 都是哈希摘要算法,不是加密解密算法,无法从加密结果反推原始数据(如需可逆加密,需使用 AES/DES 等对称加密算法);
- 盐值增强:密码存储场景中,建议在加密前添加「盐值」(如
rawData + salt),避免彩虹表破解(工具类可扩展盐值参数,示例如下):// 扩展带盐值的 MD5 加密 public static String md5WithSalt(String data, String salt) {return md5(data + salt); // 盐值可随机生成并与加密结果一起存储 } - 高安全场景替代方案:金融、政务等核心安全场景,建议使用 SHA-256/SHA-512 等更强的哈希算法,或国密算法(SM3);
- 编码一致性:加密和校验时需使用相同的字符编码(如均为 UTF-8),否则会导致校验失败。
八、常见问题排查
- 加密结果不一致:
- 检查原始数据是否一致(含空格、特殊字符);
- 确认编码是否一致(如一方用 UTF-8,一方用 GBK);
- 核实输出大小写是否一致(小写 “a” 和大写 “A” 视为不同结果)。
- 空指针异常:
- 工具类已处理
null输入,若仍报错,检查是否传入了null编码(charset不可为 null)。
- 工具类已处理
- 算法不支持异常:
- 确保 JDK 环境支持 MD5/SHA-1(主流 JDK 均支持,无需额外配置)。
