Java摘要算法实战:pan-common从MD5到国密SM3全方位指南
·
Java摘要算法实战:pan common 从MD5到国密SM3全方位指南
选题说明:摘要算法在项目中的应用远比想象中广泛——密码存储、文件完整性校验、API 签名、消息认证、数据去重等场景都离不开它。但"MD5 已不安全"“加盐方式不对”"HMac 密钥管理混乱"等问题经常导致安全隐患。本文从业务视角出发,覆盖 MD5/SHA/SM3/HMac 的完整 API 和真实业务场景。
一、摘要算法的典型业务场景
摘要算法(Hash)在项目中的核心价值:
- 密码存储:用户密码不能明文存储,需要加盐哈希后存入数据库
- 文件完整性校验:下载文件后计算摘要,与官方发布的摘要对比,确保文件未被篡改
- API 签名:对请求参数计算摘要,防止参数被篡改
- 消息认证(HMac):带密钥的摘要,验证消息来源和完整性
- 数据去重:计算文件摘要,相同摘要的文件只存一份(秒传功能)
- 区块链/数字证书:基于摘要算法实现数据不可篡改
摘要算法 vs 加密算法:
| 特性 | 摘要算法(MD5/SHA) | 加密算法(AES/RSA) |
|---|---|---|
| 可逆性 | 不可逆(单向) | 可逆(可解密) |
| 输出长度 | 固定长度 | 与输入相关 |
| 用途 | 完整性校验、密码存储 | 数据加密 |
| 密钥 | 不需要(HMac 除外) | 需要密钥 |
选型建议:
- 密码存储 → SHA-256 + 加盐(或 bcrypt/PBKDF2)
- 文件校验 → SHA-256 或 SHA3-256
- 国密合规 → SM3
- 消息认证 → HMac-SHA256
- 绝对不要用 MD5/SHA-1 存储密码(已被破解)
环境准备:
<!-- Spring Boot 2.x (javax) -->
<dependency>
<groupId>com.gitee.apanlh</groupId>
<artifactId>pan-common</artifactId>
<version>2.0.6</version>
</dependency>
<!-- Spring Boot 3.x (jakarta) -->
<dependency>
<groupId>com.gitee.apanlh</groupId>
<artifactId>pan-common</artifactId>
<version>3.0.6</version>
</dependency>
<!-- SHA-3 和 SM3 需要 Bouncy Castle -->
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcprov-jdk18on</artifactId>
<version>1.84</version>
</dependency>
二、算法选型与输出长度
2.1 算法对比
| 算法 | 输出长度(字节) | 输出长度(位) | 安全性 | 适用场景 |
|---|---|---|---|---|
| MD5 | 16 | 128 | ★☆☆☆☆ | 已不安全,仅兼容旧系统 |
| SHA-1 | 20 | 160 | ★★☆☆☆ | 已不推荐,仅兼容旧系统 |
| SHA-224 | 28 | 224 | ★★★☆☆ | 中等安全场景 |
| SHA-256 | 32 | 256 | ★★★★☆ | 通用推荐 |
| SHA-384 | 48 | 384 | ★★★★★ | 高安全场景 |
| SHA-512 | 64 | 512 | ★★★★★ | 极高安全场景 |
| SHA3-256 | 32 | 256 | ★★★★☆ | 新一代标准 |
| SM3 | 32 | 256 | ★★★★☆ | 国密合规 |
| HMac | 与对应算法相同 | - | ★★★★☆ | 消息认证 |
2.2 获取摘要实例
// 通过 DigestUtils 工厂类获取
MD5 md5 = DigestUtils.mD5(); // MD5
SHA sha256 = DigestUtils.shaWith256(); // SHA-256
SHA sha512 = DigestUtils.shaWith512(); // SHA-512
SHA3 sha3 = DigestUtils.sha3With256(); // SHA3-256
SM3 sm3 = DigestUtils.sm3(); // 国密 SM3
// HMac 摘要(默认生成随机密钥)
HMac hmacMd5 = DigestUtils.hmacWithMd5(); // HMac-MD5
HMac hmacSha256 = DigestUtils.hmacWithSha256(); // HMac-SHA256
// 自定义密钥的 HMac
byte[] key = "mySecretKey".getBytes(StandardCharsets.UTF_8);
HMac hmac = DigestUtils.hmac(key, DigestType.HMAC_SHA256);
三、基础摘要计算
3.1 多种输出格式
String data = "Hello, world";
// 获取摘要字节数组
byte[] digestBytes = md5.digest(data);
// 获取 Hex 字符串(默认小写)
String hex = sha256.digestToHex(data);
String hexUpper = sha256.digestToHex(data, false); // 大写
// 获取 Base64 字符串(默认标准 RFC4648)
String base64 = sm3.digestToBase64Str(data);
String base64Url = sm3.digestToBase64Str(data, Base64Type.RFC4648_URLSAFE); // URL 安全
便捷方法一览:
digest(String/byte[])→byte[]digestToHex(String/byte[])→String(Hex 编码)digestToBase64Str(String/byte[])→String(Base64 编码)
3.2 实战:用户密码存储(加盐哈希)
@Service
public class UserPasswordService {
@Autowired
private UserMapper userMapper;
/**
* 用户注册 - 密码加盐哈希存储
*/
public void register(String username, String password) {
// 1. 生成随机盐值(16 字节)
byte[] salt = KeyUtils.generate128();
// 2. 加盐哈希(盐值前置)
SHA sha256 = DigestUtils.shaWith256();
String hashedPassword = sha256.digestSaltToHex(password, salt);
// 3. 存储用户名、哈希密码、盐值
User user = new User();
user.setUsername(username);
user.setPassword(hashedPassword);
user.setSalt(HexUtils.encode(salt)); // 盐值以 Hex 存储
userMapper.insert(user);
}
/**
* 用户登录 - 验证密码
*/
public boolean login(String username, String inputPassword) {
User user = userMapper.selectByUsername(username);
if (user == null) {
return false;
}
// 1. 取出盐值
byte[] salt = HexUtils.decode(user.getSalt());
// 2. 用相同盐值计算输入密码的哈希
SHA sha256 = DigestUtils.shaWith256();
String inputHash = sha256.digestSaltToHex(inputPassword, salt);
// 3. 对比哈希值
return inputHash.equals(user.getPassword());
}
}
数据库表结构:
CREATE TABLE users (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
username VARCHAR(50) NOT NULL UNIQUE,
password VARCHAR(64) NOT NULL, -- SHA-256 哈希(64 字符 Hex)
salt VARCHAR(32) NOT NULL, -- 盐值(32 字符 Hex = 16 字节)
create_time DATETIME DEFAULT CURRENT_TIMESTAMP
);
安全要点:
- 每个用户使用不同的随机盐值
- 盐值不需要保密,与哈希一起存储
- 使用 SHA-256 或更强算法(不要用 MD5)
- 生产环境建议使用 bcrypt/PBKDF2(更慢,抗暴力破解)
3.3 实战:文件完整性校验
@Service
public class FileIntegrityService {
/**
* 上传文件时计算并保存摘要
*/
public FileRecord uploadFile(MultipartFile file) throws IOException {
// 1. 保存文件
String filePath = "/uploads/" + UUID.randomUUID() + "_" + file.getOriginalFilename();
file.transferTo(new File(filePath));
// 2. 计算文件摘要(流式处理,支持大文件)
SHA sha256 = DigestUtils.shaWith256();
String fileHash = sha256.digestToHex(new File(filePath));
// 3. 保存文件记录
FileRecord record = new FileRecord();
record.setFileName(file.getOriginalFilename());
record.setFilePath(filePath);
record.setFileSize(file.getSize());
record.setHash(fileHash);
record.setUploadTime(LocalDateTime.now());
fileRecordMapper.insert(record);
return record;
}
/**
* 下载文件时验证完整性
*/
public boolean verifyFileIntegrity(Long fileId) {
FileRecord record = fileRecordMapper.selectById(fileId);
if (record == null) {
return false;
}
// 1. 重新计算文件摘要
SHA sha256 = DigestUtils.shaWith256();
String currentHash = sha256.digestToHex(new File(record.getFilePath()));
// 2. 与存储的摘要对比
return sha256.verifyHex(currentHash, record.getHash());
}
/**
* 验证下载的文件是否与服务器一致
*/
public boolean verifyDownloadedFile(Long fileId, File downloadedFile) {
FileRecord record = fileRecordMapper.selectById(fileId);
if (record == null) {
return false;
}
SHA sha256 = DigestUtils.shaWith256();
// 验证文件与已知摘要
return sha256.verifyHex(downloadedFile, record.getHash());
}
}
使用示例:
// 上传文件
FileRecord record = fileIntegrityService.uploadFile(file);
System.out.println("文件摘要: " + record.getHash());
// 输出: a1b2c3d4e5f6... (64 字符)
// 验证文件完整性
boolean valid = fileIntegrityService.verifyFileIntegrity(record.getId());
if (valid) {
System.out.println("文件完整,未被篡改");
} else {
System.out.println("文件已被篡改!");
}
3.4 实战:网盘秒传功能
@Service
public class CloudStorageService {
/**
* 秒传功能:通过文件摘要判断文件是否已存在
*/
public UploadResult uploadWithQuickUpload(MultipartFile file, String fileHash) throws IOException {
// 1. 检查文件是否已存在(通过摘要)
FileRecord existingFile = fileRecordMapper.selectByHash(fileHash);
if (existingFile != null) {
// 文件已存在,直接返回引用(秒传)
UserFile userFile = new UserFile();
userFile.setUserId(getCurrentUserId());
userFile.setFileName(file.getOriginalFilename());
userFile.setFileId(existingFile.getId()); // 引用已存在的文件
userFileMapper.insert(userFile);
return UploadResult.quickUpload(existingFile.getId());
}
// 2. 文件不存在,正常上传
String filePath = "/storage/" + UUID.randomUUID() + "_" + file.getOriginalFilename();
file.transferTo(new File(filePath));
// 3. 验证客户端传来的摘要是否正确
SHA sha256 = DigestUtils.shaWith256();
String actualHash = sha256.digestToHex(new File(filePath));
if (!actualHash.equals(fileHash)) {
throw new IllegalArgumentException("文件摘要不匹配,可能传输错误");
}
// 4. 保存文件记录
FileRecord record = new FileRecord();
record.setFilePath(filePath);
record.setHash(fileHash);
record.setFileSize(file.getSize());
fileRecordMapper.insert(record);
// 5. 创建用户文件引用
UserFile userFile = new UserFile();
userFile.setUserId(getCurrentUserId());
userFile.setFileName(file.getOriginalFilename());
userFile.setFileId(record.getId());
userFileMapper.insert(userFile);
return UploadResult.normalUpload(record.getId());
}
}
@Data
public class UploadResult {
private boolean quickUpload;
private Long fileId;
public static UploadResult quickUpload(Long fileId) {
UploadResult result = new UploadResult();
result.quickUpload = true;
result.fileId = fileId;
return result;
}
public static UploadResult normalUpload(Long fileId) {
UploadResult result = new UploadResult();
result.quickUpload = false;
result.fileId = fileId;
return result;
}
}
前端代码示例:
// 前端计算文件摘要(使用 Web Crypto API)
async function uploadFile(file) {
// 1. 计算文件摘要
const hashBuffer = await crypto.subtle.digest('SHA-256', await file.arrayBuffer());
const hashArray = Array.from(new Uint8Array(hashBuffer));
const fileHash = hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
// 2. 发送文件 + 摘要
const formData = new FormData();
formData.append('file', file);
formData.append('fileHash', fileHash);
const response = await fetch('/api/upload', {
method: 'POST',
body: formData
});
const result = await response.json();
if (result.quickUpload) {
alert('秒传成功!');
} else {
alert('上传成功!');
}
}
四、流式大文件摘要
4.1 流式更新接口
// 对大文件进行 SHA-256 摘要计算
SHA sha256 = DigestUtils.shaWith256();
try (InputStream in = new FileInputStream("large.dat")) {
byte[] buffer = new byte[8192];
int len;
while ((len = in.read(buffer)) != -1) {
sha256.update(buffer, 0, len); // 分块更新
}
byte[] digest = sha256.digest(); // 完成计算,实例自动重置
String hexResult = HexUtils.encode(digest);
}
4.2 便捷方法
// 直接对文件计算摘要(自动流式处理)
SHA sha256 = DigestUtils.shaWith256();
byte[] fileDigest = sha256.digest(new File("large.dat"));
String fileHex = sha256.digestToHex(new File("large.dat"));
// 注意:digest(File) 会重置实例,之后可直接调用 digestToHex()
String hex = sha256.digestToHex(); // 获取上次的结果
4.3 实战:数据库备份完整性校验
@Service
public class BackupIntegrityService {
@Value("${backup.dir}")
private String backupDir;
/**
* 备份数据库并生成摘要文件
*/
public void backupWithIntegrity(String dbName) throws Exception {
// 1. 执行数据库备份
String sqlFile = backupDir + "/" + dbName + "_" +
DateUtils.now("yyyyMMdd_HHmmss") + ".sql";
executeBackupCommand(dbName, sqlFile);
// 2. 计算备份文件摘要
SHA sha256 = DigestUtils.shaWith256();
String backupHash = sha256.digestToHex(new File(sqlFile));
// 3. 保存摘要到 .sha256 文件
String hashFile = sqlFile + ".sha256";
FileUtils.writeStringToFile(new File(hashFile), backupHash, "UTF-8");
// 4. 压缩备份文件
String zipFile = sqlFile + ".gz";
compressFile(sqlFile, zipFile);
new File(sqlFile).delete(); // 删除原始 SQL
log.info("备份完成: {}, 摘要: {}", zipFile, backupHash);
}
/**
* 恢复前验证备份完整性
*/
public boolean verifyBackupIntegrity(String zipFile) throws IOException {
// 1. 解压备份文件
String sqlFile = zipFile.replace(".gz", "");
decompressFile(zipFile, sqlFile);
// 2. 读取存储的摘要
String hashFile = sqlFile + ".sha256";
String expectedHash = FileUtils.readFileToString(new File(hashFile), "UTF-8");
// 3. 计算实际摘要
SHA sha256 = DigestUtils.shaWith256();
String actualHash = sha256.digestToHex(new File(sqlFile));
// 4. 验证
boolean valid = sha256.verifyHex(actualHash, expectedHash);
if (!valid) {
log.error("备份文件已被篡改!期望: {}, 实际: {}", expectedHash, actualHash);
}
return valid;
}
}
使用流程:
# 备份
java -jar app.jar backup --db=mydb
# 生成: mydb_20250604_143000.sql.gz 和 mydb_20250604_143000.sql.gz.sha256
# 恢复前验证
java -jar app.jar restore --backup=mydb_20250604_143000.sql.gz
# 自动验证完整性,失败则中止恢复
五、HMac 消息认证
5.1 基础用法
// 使用默认生成的密钥
HMac hmacAuto = DigestUtils.hmacWithSha256();
byte[] hmacKey = hmacAuto.getKeyBytes(); // 获取密钥,需保存用于验证
byte[] mac = hmacAuto.digest("message".getBytes());
String macHex = hmacAuto.digestToHex("message");
// 使用自定义密钥重新创建 HMac 实例进行验证
HMac hmacVerify = DigestUtils.hmac(hmacKey, DigestType.HMAC_SHA256);
boolean valid = hmacVerify.verify("message".getBytes(), mac);
System.out.println("认证结果: " + valid); // true
5.2 密钥管理
// 获取密钥信息
HMac hmac = DigestUtils.hmacWithSha256();
byte[] keyBytes = hmac.getKeyBytes(); // 密钥字节数组
String keyHex = hmac.getKeyToHex(); // 密钥 Hex 字符串
int macLength = hmac.getMacLength(); // MAC 长度(32 字节 for SHA-256)
// 存储密钥
properties.setProperty("hmac.key", keyHex);
try (FileOutputStream fos = new FileOutputStream("hmac.properties")) {
properties.store(fos, "HMac key configuration");
}
5.3 实战:API 请求认证(HMac 签名)
@Service
public class ApiAuthService {
@Value("${api.hmac.key}")
private String hmacKeyHex;
/**
* 对 API 请求生成 HMac 签名
*/
public String generateSignature(Map<String, Object> params) {
// 1. 参数排序并拼接
String sortedParams = params.entrySet().stream()
.sorted(Map.Entry.comparingByKey())
.map(e -> e.getKey() + "=" + e.getValue())
.collect(Collectors.joining("&"));
// 2. 用 HMac 计算签名
byte[] key = HexUtils.decode(hmacKeyHex);
HMac hmac = DigestUtils.hmac(key, DigestType.HMAC_SHA256);
return hmac.digestToHex(sortedParams);
}
/**
* 验证 API 请求签名
*/
public boolean verifySignature(Map<String, String> params, String signature) {
// 1. 移除签名字段
params.remove("signature");
// 2. 参数排序并拼接
String sortedParams = params.entrySet().stream()
.sorted(Map.Entry.comparingByKey())
.map(e -> e.getKey() + "=" + e.getValue())
.collect(Collectors.joining("&"));
// 3. 用 HMac 验证
byte[] key = HexUtils.decode(hmacKeyHex);
HMac hmac = DigestUtils.hmac(key, DigestType.HMAC_SHA256);
return hmac.verifyHex(sortedParams, signature);
}
}
使用示例:
@RestController
public class ApiController {
@Autowired
private ApiAuthService apiAuthService;
/**
* 客户端发送请求
*/
@GetMapping("/api/data")
public Map<String, Object> getData(@RequestParam Map<String, String> params,
@RequestHeader("X-Signature") String signature) {
// 验证签名
if (!apiAuthService.verifySignature(new HashMap<>(params), signature)) {
throw new UnauthorizedException("签名验证失败");
}
// 处理业务
return dataService.getData(params);
}
}
// 客户端调用
@Service
public class ApiClient {
@Autowired
private ApiAuthService apiAuthService;
@Autowired
private RestTemplate restTemplate;
public String callApi(String userId) {
// 1. 构造参数
Map<String, Object> params = new HashMap<>();
params.put("user_id", userId);
params.put("timestamp", System.currentTimeMillis());
// 2. 生成签名
String signature = apiAuthService.generateSignature(params);
// 3. 发送请求
UriComponentsBuilder builder = UriComponentsBuilder.fromHttpUrl("https://api.example.com/data")
.queryParam("user_id", userId)
.queryParam("timestamp", params.get("timestamp"));
HttpHeaders headers = new HttpHeaders();
headers.set("X-Signature", signature);
HttpEntity<?> entity = new HttpEntity<>(headers);
ResponseEntity<String> response = restTemplate.exchange(
builder.toUriString(), HttpMethod.GET, entity, String.class);
return response.getBody();
}
}
5.4 实战:Webhook 回调验证(钉钉/企业微信)
@RestController
@RequestMapping("/webhook")
public class WebhookController {
@Value("${dingtalk.webhook.secret}")
private String webhookSecret;
/**
* 钉钉机器人回调验证
*/
@PostMapping("/dingtalk")
public String handleDingtalkCallback(
@RequestBody String requestBody,
@RequestHeader("X-DingTalk-Timestamp") String timestamp,
@RequestHeader("X-DingTalk-Sign") String sign) {
// 1. 验证时间戳(防止重放攻击,5 分钟内有效)
long ts = Long.parseLong(timestamp);
if (System.currentTimeMillis() - ts > 5 * 60 * 1000) {
return "timestamp expired";
}
// 2. 构造待签名字符串:timestamp + "\n" + secret
String stringToSign = timestamp + "\n" + webhookSecret;
// 3. 用 HMac-SHA256 计算签名
byte[] key = webhookSecret.getBytes(StandardCharsets.UTF_8);
HMac hmac = DigestUtils.hmac(key, DigestType.HMAC_SHA256);
String calculatedSign = hmac.digestToBase64Str(stringToSign);
// 4. 验证签名
if (!calculatedSign.equals(sign)) {
log.warn("钉钉回调签名验证失败");
return "signature invalid";
}
// 5. 处理回调消息
DingtalkMessage message = JSON.parseObject(requestBody, DingtalkMessage.class);
messageService.handleDingtalkMessage(message);
return "success";
}
}
六、摘要验证
6.1 多种验证方式
String plain = "Hello";
String hexDigest = md5.digestToHex(plain);
String base64Digest = md5.digestToBase64Str(plain);
// 验证原文与 Hex 摘要
boolean valid1 = md5.verifyHex(plain, hexDigest);
boolean valid2 = md5.verifyHex(plain, hexDigest.toUpperCase()); // 忽略大小写
// 验证原文与 Base64 摘要
boolean valid3 = md5.verifyBase64(plain, base64Digest);
// 验证字节数组与摘要字节数组
byte[] digestBytes = md5.digest(plain);
boolean valid4 = md5.verify(plain.getBytes(), digestBytes);
6.2 文件验证
File file1 = new File("data.txt");
File file2 = new File("data_copy.txt");
// 计算文件摘要
String file1Hex = md5.digestToHex(file1); // 自动流式处理
// 验证两个文件摘要是否一致
boolean filesMatch = md5.verify(file1, file2);
// 验证文件与已知摘要字符串
String knownDigest = "e10adc3949ba59abbe56e057f20f883e"; // 示例 MD5
boolean isValid = md5.verifyHex(file1, knownDigest); // 忽略大小写
6.3 实战:软件包发布与验证
@Service
public class SoftwareReleaseService {
/**
* 发布软件包(生成摘要文件)
*/
public ReleaseInfo publishRelease(MultipartFile file, String version) throws IOException {
// 1. 保存软件包
String packageName = "app-" + version + ".zip";
String packagePath = "/releases/" + packageName;
file.transferTo(new File(packagePath));
// 2. 计算多种摘要(兼容不同验证工具)
MD5 md5 = DigestUtils.mD5();
SHA sha256 = DigestUtils.shaWith256();
String md5Hex = md5.digestToHex(new File(packagePath));
String sha256Hex = sha256.digestToHex(new File(packagePath));
// 3. 生成摘要文件
String checksumContent = String.format(
"MD5(%s) = %s\nSHA256(%s) = %s\n",
packageName, md5Hex, packageName, sha256Hex
);
FileUtils.writeStringToFile(
new File("/releases/" + packageName + ".sha256"),
checksumContent, "UTF-8"
);
// 4. 返回发布信息
ReleaseInfo info = new ReleaseInfo();
info.setVersion(version);
info.setPackageName(packageName);
info.setDownloadUrl("https://example.com/releases/" + packageName);
info.setMd5(md5Hex);
info.setSha256(sha256Hex);
info.setFileSize(file.getSize());
return info;
}
}
// 用户下载后验证
@Service
public class DownloadVerificationService {
public boolean verifyDownloadedPackage(File downloadedFile, String expectedSha256) {
SHA sha256 = DigestUtils.shaWith256();
String actualSha256 = sha256.digestToHex(downloadedFile);
return sha256.verifyHex(actualSha256, expectedSha256);
}
}
用户验证示例(Linux):
# 下载软件包
wget https://example.com/releases/app-1.0.0.zip
wget https://example.com/releases/app-1.0.0.zip.sha256
# 验证 SHA256
sha256sum -c app-1.0.0.zip.sha256
# 输出: app-1.0.0.zip: OK
# 或手动验证
sha256sum app-1.0.0.zip
# 对比输出的哈希值是否与 .sha256 文件中的一致
七、SM3 国密摘要实战
7.1 基础用法
SM3 sm3 = DigestUtils.sm3();
// 计算摘要
String data = "国密摘要算法";
String hex = sm3.digestToHex(data);
String base64 = sm3.digestToBase64Str(data);
// 验证
boolean valid = sm3.verifyHex(data, hex);
7.2 实战:政务系统数据完整性校验
@Service
public class GovDataIntegrityService {
/**
* 向政务平台提交数据(附带 SM3 摘要)
*/
public Map<String, Object> submitData(Map<String, Object> data) {
// 1. 将数据转为 JSON
String jsonData = JSON.toJSONString(data);
// 2. 计算 SM3 摘要
SM3 sm3 = DigestUtils.sm3();
String dataHash = sm3.digestToHex(jsonData);
// 3. 构造请求
Map<String, Object> request = new HashMap<>();
request.put("data", data);
request.put("hash", dataHash);
request.put("hash_algorithm", "SM3");
// 4. 提交到政务平台
return httpClient.post("https://gov-api.example.com/submit", request);
}
/**
* 接收政务平台数据(验证 SM3 摘要)
*/
public boolean receiveAndVerify(Map<String, Object> response) {
// 1. 提取数据和摘要
Map<String, Object> data = (Map<String, Object>) response.get("data");
String receivedHash = (String) response.get("hash");
// 2. 计算数据摘要
String jsonData = JSON.toJSONString(data);
SM3 sm3 = DigestUtils.sm3();
String calculatedHash = sm3.digestToHex(jsonData);
// 3. 验证
return sm3.verifyHex(calculatedHash, receivedHash);
}
}
八、安全注意事项
8.1 密码存储不要用 MD5/SHA-1
// 错误:MD5 存储密码(已被破解)
MD5 md5 = DigestUtils.mD5();
String hash = md5.digestToHex(password);
// 正确:SHA-256 + 加盐
SHA sha256 = DigestUtils.shaWith256();
byte[] salt = KeyUtils.generate128();
String hash = sha256.digestSaltToHex(password, salt);
// 更好:使用 bcrypt/PBKDF2(更慢,抗暴力破解)
String hash = BCrypt.hashpw(password, BCrypt.gensalt(12));
8.2 盐值必须随机且唯一
// 错误:固定盐值
byte[] salt = "fixed_salt".getBytes(); // 所有用户用相同盐值
// 正确:每个用户生成不同的随机盐值
byte[] salt = KeyUtils.generate128(); // 16 字节随机盐值
8.3 HMac 密钥管理
// ❌ 错误:硬编码密钥
byte[] key = "my_key".getBytes();
// ✅ 正确:密钥通过配置文件或 KMS 管理
@Value("${hmac.key}")
private String hmacKeyHex;
byte[] key = HexUtils.decode(hmacKeyHex);
8.4 摘要算法选择
| 场景 | 推荐算法 | 说明 |
|---|---|---|
| 密码存储 | SHA-256 + 加盐 | 或 bcrypt/PBKDF2 |
| 文件校验 | SHA-256 或 SHA3-256 | 抗碰撞性强 |
| 国密合规 | SM3 | 政务/金融场景 |
| 消息认证 | HMac-SHA256 | 带密钥的摘要 |
| 旧系统兼容 | MD5/SHA-1 | 仅用于非安全场景 |
九、总结
pan-common 的摘要工具覆盖了 MD5/SHA/SHA3/SM3/HMac 等主流算法,提供统一的 API 设计。核心价值:
- API 统一:所有算法的摘要方法命名一致,切换算法只需改类名
- 便捷方法:
digestToHex/digestToBase64Str等方法减少编码转换 - 流式处理:支持 GB 级大文件摘要,内存占用恒定
- 加盐支持:
digestSalt方法简化密码存储 - 验证便捷:
verifyHex/verifyBase64等方法自动忽略大小写 - 国密合规:SM3 算法支持政务/金融场景
生产环境 Checklist:
- 密码存储使用 SHA-256 + 加盐(或 bcrypt)
- 文件校验使用 SHA-256 或 SHA3-256
- 每个用户使用不同的随机盐值
- HMac 密钥通过 KMS 或安全渠道管理
- 国密场景使用 SM3
- 绝对不要用 MD5/SHA-1 存储密码
项目地址:https://gitee.com/apanlh/pan-common
更多推荐
所有评论(0)