Files
xiu_b/src/main/java/com/tailbet/openim/OpenIMAdminTokenManager.java
T
2026-08-08 14:56:37 +08:00

171 lines
6.2 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.tailbet.openim;
import com.tailbet.common.enums.ErrorCodeEnum;
import com.tailbet.common.exception.BusinessException;
import com.tailbet.config.OpenIMProperties;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import org.springframework.web.client.RestTemplate;
import jakarta.annotation.PostConstruct;
import jakarta.annotation.Resource;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.atomic.AtomicReference;
/**
* OpenIM adminToken 管理器
* <p>
* 负责 OpenIM 管理员 Token 的获取与定期刷新:服务启动时通过 OpenIM
* {@code /auth/get_admin_token} 接口获取一次,之后每 23 小时自动刷新一次
* OpenIM 管理员 Token 默认 24 小时过期,提前 1 小时刷新以规避临界失效)。
* </p>
* <p>
* 本类不依赖 {@link OpenIMApiClient},避免出现
* OpenIMApiClient → OpenIMAdminTokenManager → OpenIMApiClient 的循环依赖,
* 内部直接使用专用 RestTemplate 调用 OpenIM 接口。
* </p>
*
* @author socialapp团队
* @since 1.0.0
*/
@RequiredArgsConstructor
@Slf4j
@Component
public class OpenIMAdminTokenManager {
/**
* 定时刷新周期:23 小时(毫秒)
*/
private static final long REFRESH_INTERVAL_MS = 23 * 60 * 60 * 1000L;
private final RestTemplate restTemplate;
/**
* OpenIM 集成配置属性
*/
private final OpenIMProperties openIMProperties;
/**
* 当前有效的 adminToken,使用原子引用保证多线程读取的可见性与线程安全
*/
private final AtomicReference<String> tokenRef = new AtomicReference<>("");
/**
* 服务启动时初始化 adminToken
* <p>
* 若启动时获取失败,仅记录错误日志,不阻塞服务启动;
* 后续定时任务会持续尝试刷新,业务调用方在 adminToken 为空时会因鉴权失败而收到明确异常。
* </p>
*/
@PostConstruct
public void init() {
try {
refreshToken();
} catch (Exception e) {
log.error("【OpenIM】服务启动时获取 adminToken 失败: {}", e.getMessage(), e);
}
}
/**
* 每 23 小时自动刷新 adminToken(避免 24 小时过期)
*/
@Scheduled(fixedDelay = REFRESH_INTERVAL_MS)
public void scheduledRefresh() {
try {
refreshToken();
} catch (Exception e) {
log.error("【OpenIM】定时刷新 adminToken 失败: {}", e.getMessage(), e);
}
}
/**
* 获取当前有效的 adminToken
* <p>
* 若 token 尚未初始化(null 或空),则尝试立即刷新一次,防止 NPE 及空 token 导致的鉴权失败。
* </p>
*
* @return adminToken 字符串,刷新失败时返回空字符串
*/
public String getAdminToken() {
String token = tokenRef.get();
if (token == null || token.isEmpty()) {
log.warn("【OpenIM】adminToken 尚未初始化,尝试立即刷新");
refreshToken();
token = tokenRef.get();
}
return token != null ? token : "";
}
/**
* 调用 OpenIM {@code /auth/get_admin_token} 接口刷新 adminToken
* <p>
* 获取 adminToken 属于鉴权前置操作,请求本身不能携带 adminToken 请求头。
* 方法加锁,避免同进程内并发刷新互相顶掉(OpenIM 同 user 新 token 会使旧 token 变为 TokenKicked)。
* </p>
*/
public synchronized void refreshToken() {
String url = openIMProperties.resolveServerApiUrl() + "/auth/get_admin_token";
Map<String, Object> body = new HashMap<>();
body.put("secret", openIMProperties.getSecret());
body.put("userID", openIMProperties.getAdminUserId());
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
headers.set("operationID", UUID.randomUUID().toString());
HttpEntity<Map<String, Object>> entity = new HttpEntity<>(body, headers);
OpenIMApiClient.OpenIMBaseResp resp;
try {
ResponseEntity<OpenIMApiClient.OpenIMBaseResp> response =
restTemplate.postForEntity(url, entity, OpenIMApiClient.OpenIMBaseResp.class);
resp = response.getBody();
} catch (org.springframework.web.client.HttpStatusCodeException e) {
// OpenIM 偶发以 HTTP 4xx + 业务 JSON 返回,尽量解析 errMsg
String raw = e.getResponseBodyAsString();
log.error("【OpenIM】获取 adminToken HTTP 失败: status={}, body={}", e.getStatusCode().value(), raw);
throw new BusinessException(ErrorCodeEnum.IM_OPENIM_GET_ADMIN_TOKEN_FAILED,
raw != null && !raw.isBlank() ? raw : e.getMessage());
}
if (resp == null || resp.getErrCode() != 0) {
String errMsg = resp != null ? resp.getErrMsg() : "响应为空";
log.error("【OpenIM】获取 adminToken 失败: errMsg={}", errMsg);
throw new BusinessException(ErrorCodeEnum.IM_OPENIM_GET_ADMIN_TOKEN_FAILED, errMsg);
}
@SuppressWarnings("unchecked")
Map<String, Object> data = (Map<String, Object>) resp.getData();
String newToken = data != null ? (String) data.get("token") : null;
if (newToken == null) {
throw new BusinessException(ErrorCodeEnum.IM_OPENIM_GET_ADMIN_TOKEN_FAILED, "响应中缺少 token 字段");
}
tokenRef.set(newToken);
log.info("【OpenIM】adminToken 刷新成功: token={}", maskString(newToken));
}
/**
* 对敏感字符串进行脱敏处理
* 只显示前3位和后3位,中间用星号代替
*
* @param str 原始字符串
* @return 脱敏后的字符串
*/
private String maskString(String str) {
if (str == null || str.length() <= 6) {
return "***";
}
return str.substring(0, 3) + "****" + str.substring(str.length() - 3);
}
}