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>
This commit is contained in:
@@ -0,0 +1,84 @@
|
||||
package com.imutil.common;
|
||||
|
||||
import javax.crypto.SecretKeyFactory;
|
||||
import javax.crypto.spec.PBEKeySpec;
|
||||
import java.security.SecureRandom;
|
||||
import java.util.Base64;
|
||||
|
||||
/**
|
||||
* 密码哈希工具(PBKDF2WithHmacSHA256,JDK 内置,无额外依赖)
|
||||
* <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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user