package jnpf.service;
|
|
import cn.hutool.crypto.digest.DigestUtil;
|
import jnpf.base.LogApi;
|
import jnpf.base.UserInfo;
|
import jnpf.base.model.logmodel.LoginSessionEventModel;
|
import jnpf.base.model.logmodel.LoginSessionEventResult;
|
import jnpf.util.DateUtil;
|
import jnpf.util.IpUtil;
|
import jnpf.util.JsonUtil;
|
import jnpf.util.RandomUtil;
|
import jnpf.util.RedisUtil;
|
import jnpf.util.ServletUtil;
|
import jnpf.util.StringUtil;
|
import lombok.RequiredArgsConstructor;
|
import lombok.extern.slf4j.Slf4j;
|
import org.springframework.beans.factory.DisposableBean;
|
import org.springframework.beans.factory.InitializingBean;
|
import org.springframework.data.redis.core.RedisTemplate;
|
import org.springframework.stereotype.Service;
|
|
import java.util.Date;
|
import java.util.Set;
|
import java.util.concurrent.Executors;
|
import java.util.concurrent.ScheduledExecutorService;
|
import java.util.concurrent.ThreadFactory;
|
import java.util.concurrent.TimeUnit;
|
|
/**
|
* Authentication-side login session event recorder with Redis retry fallback.
|
*/
|
@Service
|
@Slf4j
|
@RequiredArgsConstructor
|
public class LoginSessionLogRecorder implements InitializingBean, DisposableBean {
|
|
private static final String PENDING_PREFIX = "jnpf:login-session:pending:";
|
public static final String TOKEN_MAPPING_PREFIX = "jnpf:login-session:token:";
|
public static final String TENANT_REGISTRY_KEY = "jnpf:login-session:tenants";
|
private static final long PENDING_TTL_SECONDS = 7 * 24 * 60 * 60L;
|
|
private final LogApi logApi;
|
private final RedisUtil redisUtil;
|
private final RedisTemplate<String, Object> redisTemplate;
|
private ScheduledExecutorService retryExecutor;
|
|
public void open(UserInfo userInfo, Integer loginMode) {
|
if (userInfo == null || StringUtil.isEmpty(userInfo.getToken())) {
|
return;
|
}
|
LoginSessionEventModel event = baseEvent(userInfo.getToken(), userInfo)
|
.eventType(LoginSessionEventModel.OPEN)
|
.loginMode(loginMode)
|
.source("NEW_LOGIN")
|
.eventTime(DateUtil.getNowDate())
|
.expireTime(userInfo.getOverdueTime())
|
.build();
|
saveTokenMapping(event.getSessionId(), userInfo, userInfo.getOverdueTime());
|
record(event);
|
}
|
|
public void close(String token, UserInfo userInfo, String reason, String description) {
|
if (StringUtil.isEmpty(token)) {
|
return;
|
}
|
LoginSessionEventModel event = baseEvent(token, userInfo)
|
.eventType(LoginSessionEventModel.CLOSE)
|
.logoutReason(reason)
|
.description(description)
|
.eventTime(DateUtil.getNowDate())
|
.build();
|
record(event);
|
redisUtil.remove(TOKEN_MAPPING_PREFIX + event.getSessionId());
|
}
|
|
public void touch(String token, UserInfo userInfo, Date expireTime) {
|
if (StringUtil.isEmpty(token)) {
|
return;
|
}
|
LoginSessionEventModel event = baseEvent(token, userInfo)
|
.eventType(LoginSessionEventModel.TOUCH)
|
.eventTime(DateUtil.getNowDate())
|
.expireTime(expireTime)
|
.build();
|
saveTokenMapping(event.getSessionId(), userInfo, expireTime);
|
record(event);
|
}
|
|
public String sessionId(String token) {
|
String value = token == null ? "" : token.trim();
|
if (value.regionMatches(true, 0, "bearer", 0, "bearer".length())) {
|
value = value.substring("bearer".length()).trim();
|
}
|
return DigestUtil.sha256Hex(value);
|
}
|
|
private LoginSessionEventModel.LoginSessionEventModelBuilder baseEvent(String token, UserInfo userInfo) {
|
String eventId = RandomUtil.uuId();
|
LoginSessionEventModel.LoginSessionEventModelBuilder builder = LoginSessionEventModel.builder()
|
.eventId(eventId)
|
.transitionId(eventId)
|
.sessionId(sessionId(token))
|
.userInfo(userInfo)
|
.requestUrl(safeServletPath())
|
.requestMethod(safeRequestMethod());
|
if (userInfo != null) {
|
builder.ipAddress(userInfo.getLoginIpAddress())
|
.ipAddressName(userInfo.getLoginIpAddressName())
|
.browser(userInfo.getBrowser())
|
.platform(userInfo.getLoginPlatForm());
|
} else {
|
builder.ipAddress(safeIp());
|
}
|
return builder;
|
}
|
|
private void record(LoginSessionEventModel event) {
|
try {
|
LoginSessionEventResult result = logApi.recordLoginSessionEvent(event);
|
if (result != null && result.isSuccess()) {
|
redisUtil.remove(pendingKey(event.getEventId()));
|
return;
|
}
|
} catch (Exception e) {
|
log.warn("登录会话日志写入失败,已进入补偿队列,sessionId: {}, eventType: {}",
|
event.getSessionId(), event.getEventType(), e);
|
}
|
enqueue(event);
|
}
|
|
private void enqueue(LoginSessionEventModel event) {
|
try {
|
redisUtil.insert(pendingKey(event.getEventId()), JsonUtil.getObjectToStringAsDate(event), PENDING_TTL_SECONDS);
|
} catch (Exception e) {
|
log.error("登录会话日志主通道和补偿通道均失败,sessionId: {}, eventType: {}",
|
event.getSessionId(), event.getEventType(), e);
|
}
|
}
|
|
private void retryPending() {
|
try {
|
Set<String> keys = redisUtil.findKeysContaining(PENDING_PREFIX + "*");
|
if (keys == null || keys.isEmpty()) {
|
return;
|
}
|
int count = 0;
|
for (String key : keys) {
|
if (count++ >= 200) {
|
break;
|
}
|
Object json = redisUtil.getString(key);
|
if (json == null) {
|
continue;
|
}
|
LoginSessionEventModel event = JsonUtil.getJsonToBean(String.valueOf(json), LoginSessionEventModel.class);
|
LoginSessionEventResult result = logApi.recordLoginSessionEvent(event);
|
if (result != null && result.isSuccess()) {
|
redisUtil.remove(key);
|
}
|
}
|
} catch (Exception e) {
|
log.warn("重试登录会话日志失败", e);
|
}
|
}
|
|
@Override
|
public void afterPropertiesSet() {
|
ThreadFactory factory = runnable -> {
|
Thread thread = new Thread(runnable, "login-session-log-retry");
|
thread.setDaemon(true);
|
return thread;
|
};
|
retryExecutor = Executors.newSingleThreadScheduledExecutor(factory);
|
retryExecutor.scheduleWithFixedDelay(this::retryPending, 30, 30, TimeUnit.SECONDS);
|
}
|
|
@Override
|
public void destroy() {
|
if (retryExecutor != null) {
|
retryExecutor.shutdownNow();
|
}
|
}
|
|
private String pendingKey(String eventId) {
|
return PENDING_PREFIX + eventId;
|
}
|
|
private void saveTokenMapping(String sessionId, UserInfo userInfo, Date expireTime) {
|
if (expireTime == null) {
|
return;
|
}
|
long ttl = Math.max(1, (expireTime.getTime() - System.currentTimeMillis()) / 1000L + 120L);
|
try {
|
redisUtil.insert(TOKEN_MAPPING_PREFIX + sessionId, "1", ttl);
|
if (userInfo != null && StringUtil.isNotEmpty(userInfo.getTenantId())) {
|
redisTemplate.opsForSet().add(TENANT_REGISTRY_KEY, userInfo.getTenantId());
|
}
|
} catch (Exception e) {
|
log.warn("保存登录会话 Token 映射失败,sessionId: {}", sessionId, e);
|
}
|
}
|
|
private String safeServletPath() {
|
try {
|
return ServletUtil.getServletPath();
|
} catch (Exception ignored) {
|
return null;
|
}
|
}
|
|
private String safeRequestMethod() {
|
try {
|
return ServletUtil.getRequest().getMethod();
|
} catch (Exception ignored) {
|
return null;
|
}
|
}
|
|
private String safeIp() {
|
try {
|
return IpUtil.getIpAddr();
|
} catch (Exception ignored) {
|
return null;
|
}
|
}
|
}
|