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:
yixiong
2026-07-08 20:28:32 +08:00
co-authored by Claude
parent 20bd78baa8
commit d22fdcfaad
80 changed files with 4955 additions and 0 deletions
@@ -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;
/**
* 密码哈希工具(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;
}
}