Java摘要算法实战:pan common 从MD5到国密SM3全方位指南

选题说明:摘要算法在项目中的应用远比想象中广泛——密码存储、文件完整性校验、API 签名、消息认证、数据去重等场景都离不开它。但"MD5 已不安全"“加盐方式不对”"HMac 密钥管理混乱"等问题经常导致安全隐患。本文从业务视角出发,覆盖 MD5/SHA/SM3/HMac 的完整 API 和真实业务场景。


一、摘要算法的典型业务场景

摘要算法(Hash)在项目中的核心价值:

  1. 密码存储:用户密码不能明文存储,需要加盐哈希后存入数据库
  2. 文件完整性校验:下载文件后计算摘要,与官方发布的摘要对比,确保文件未被篡改
  3. API 签名:对请求参数计算摘要,防止参数被篡改
  4. 消息认证(HMac):带密钥的摘要,验证消息来源和完整性
  5. 数据去重:计算文件摘要,相同摘要的文件只存一份(秒传功能)
  6. 区块链/数字证书:基于摘要算法实现数据不可篡改

摘要算法 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 设计。核心价值:

  1. API 统一:所有算法的摘要方法命名一致,切换算法只需改类名
  2. 便捷方法digestToHex/digestToBase64Str 等方法减少编码转换
  3. 流式处理:支持 GB 级大文件摘要,内存占用恒定
  4. 加盐支持digestSalt 方法简化密码存储
  5. 验证便捷verifyHex/verifyBase64 等方法自动忽略大小写
  6. 国密合规:SM3 算法支持政务/金融场景

生产环境 Checklist:

  • 密码存储使用 SHA-256 + 加盐(或 bcrypt)
  • 文件校验使用 SHA-256 或 SHA3-256
  • 每个用户使用不同的随机盐值
  • HMac 密钥通过 KMS 或安全渠道管理
  • 国密场景使用 SM3
  • 绝对不要用 MD5/SHA-1 存储密码

项目地址:https://gitee.com/apanlh/pan-common


更多推荐