Files
tencent-im-util/src/main/java/com/imutil/common/PasswordUtil.java
T
yixiongandClaude d22fdcfaad feat: 初始化腾讯IM分发工具骨架,含回调分发链路与历史消息补拉
- 应用骨架: Solon + MyBatis-Plus + PostgreSQL + Redis + Sa-Token
- 核心链路: 回调网关 → 消息落库(幂等) → 分发队列(FOR UPDATE SKIP LOCKED) → worker消费/重试/死信
- 多租户隔离: 前缀法账号映射, 所有业务表带 tenant_id
- 管理后台: FreeMarker 渲染, 租户/授权/队列/用量管理页面
- 历史消息补拉: 水位线驱动, getRoamMsg/getGroupMsg 增量拉取, 兜底回调丢失
- msg_key 统一算法(回调/补拉共享, 跨路径去重)

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-08 20:28:32 +08:00

85 lines
2.7 KiB
Java
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package com.imutil.common;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.PBEKeySpec;
import java.security.SecureRandom;
import java.util.Base64;
/**
* 密码哈希工具(PBKDF2WithHmacSHA256JDK 内置,无额外依赖)
* <p>
* 存储格式:{@code iterations:saltBase64:hashBase64}。
* verify 时按存储的 iterations/salt 重新推导并常量时间比对。
* 每次哈希用随机 salt,防彩虹表。
*
* @author imutil
*/
public final class PasswordUtil {
/** 迭代次数(OWASP 2023 建议 PBKDF2-HMAC-SHA256 ≥ 120000 */
private static final int ITERATIONS = 120_000;
private static final int KEY_BITS = 256;
private static final int SALT_BYTES = 16;
private static final String ALGO = "PBKDF2WithHmacSHA256";
private PasswordUtil() {
}
/**
* 对明文密码哈希,返回 "iterations:salt:hash"
*/
public static String hash(String rawPassword) {
byte[] salt = new byte[SALT_BYTES];
new SecureRandom().nextBytes(salt);
byte[] dk = derive(rawPassword, salt, ITERATIONS);
return ITERATIONS + ":" + b64(salt) + ":" + b64(dk);
}
/**
* 校验明文密码与存储hash是否匹配
*/
public static boolean verify(String rawPassword, String stored) {
if (rawPassword == null || stored == null) {
return false;
}
String[] parts = stored.split(":");
if (parts.length != 3) {
return false;
}
try {
int iter = Integer.parseInt(parts[0]);
byte[] salt = Base64.getDecoder().decode(parts[1]);
byte[] expected = Base64.getDecoder().decode(parts[2]);
byte[] actual = derive(rawPassword, salt, iter);
return constantTimeEquals(expected, actual);
} catch (Exception e) {
return false;
}
}
private static byte[] derive(String rawPassword, byte[] salt, int iter) {
try {
PBEKeySpec spec = new PBEKeySpec(rawPassword.toCharArray(), salt, iter, KEY_BITS);
return SecretKeyFactory.getInstance(ALGO).generateSecret(spec).getEncoded();
} catch (Exception e) {
throw new IllegalStateException("PBKDF2 推导失败", e);
}
}
private static String b64(byte[] b) {
return Base64.getEncoder().encodeToString(b);
}
/** 常量时间比较,防时序攻击 */
private static boolean constantTimeEquals(byte[] a, byte[] b) {
if (a.length != b.length) {
return false;
}
int r = 0;
for (int i = 0; i < a.length; i++) {
r |= a[i] ^ b[i];
}
return r == 0;
}
}