Compare commits

..

No commits in common. "7331cfea51e314f5029aecf9be224d22bfd93880" and "ed4a95da29bff0196b350320ca2ffc4c636041bd" have entirely different histories.

7 changed files with 244 additions and 474 deletions

View File

@ -1,181 +0,0 @@
package com.red.circle.auth.common;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.red.circle.auth.storage.RedCircleCredentialService;
import com.red.circle.component.redis.service.RedisService;
import com.red.circle.framework.core.security.UserCredential;
import com.red.circle.other.inner.endpoint.sys.EnumConfigClient;
import com.red.circle.tool.core.text.StringUtils;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Executor;
import java.util.concurrent.RejectedExecutionException;
import lombok.extern.slf4j.Slf4j;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.util.EntityUtils;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Component;
/**
* 登录后访问校验异步执行避免阻塞登录响应.
*/
@Slf4j
@Component
public class LoginAccessValidationService {
private static final String LOGIN_WHITE_CONFIG = "LOGIN_WHITE_CONFIG";
private static final String APPCODE_CONFIG = "APPCODE ******";
private static final String REQUEST_URL = "https://c2ba.api.huachen.cn/ip";
private static final List<String> ALLOWED_CHINA_REGIONS = Arrays.asList("香港", "澳门", "台湾");
private final EnumConfigClient enumConfigClient;
private final RedCircleCredentialService redCircleCredentialService;
private final RedisService redisService;
private final Executor loginValidationExecutor;
public LoginAccessValidationService(
EnumConfigClient enumConfigClient,
RedCircleCredentialService redCircleCredentialService,
RedisService redisService,
@Qualifier("loginValidationExecutor") Executor loginValidationExecutor
) {
this.enumConfigClient = enumConfigClient;
this.redCircleCredentialService = redCircleCredentialService;
this.redisService = redisService;
this.loginValidationExecutor = loginValidationExecutor;
}
public void validateLoginAccessAsync(
UserCredential userCredential,
String reqSysOrigin,
String reqZoneId,
String account,
String ipAddr
) {
if (userCredential == null) {
log.warn("validateLoginAccessAsync skipped: userCredential is null, account={}, ipAddr={}", account, ipAddr);
return;
}
try {
CompletableFuture.runAsync(
() -> validateLoginAccess(userCredential, reqSysOrigin, reqZoneId, account, ipAddr),
loginValidationExecutor
);
} catch (RejectedExecutionException e) {
log.error(
"validateLoginAccessAsync rejected userId={} account={} ipAddr={}",
userCredential == null ? null : userCredential.getUserId(),
account,
ipAddr,
e
);
}
}
public String getCountryId(String ip) {
JSONObject dataObject = loadIpData(ip);
return dataObject != null ? dataObject.getString("country_id") : null;
}
private void validateLoginAccess(
UserCredential userCredential,
String reqSysOrigin,
String reqZoneId,
String account,
String ipAddr
) {
try {
log.info(
"checkLoginCreate reqSysOrigin {} reqZoneId {} account {} ipAddr {}",
reqSysOrigin,
reqZoneId,
account,
ipAddr
);
String whiteConfig = enumConfigClient.getValue(LOGIN_WHITE_CONFIG, reqSysOrigin).getBody();
List<String> whiteIds = StringUtils.isNotBlank(whiteConfig)
? Arrays.asList(whiteConfig.split(","))
: Collections.emptyList();
if (whiteIds.contains(account)) {
return;
}
if (isIpValidationFailed(ipAddr) || isZoneValidationFailed(reqZoneId)) {
boolean removed = redCircleCredentialService.removeByUserIdIfSignMatches(
userCredential.getUserId(),
userCredential.getSign()
);
log.warn(
"login access validation failed, revoke token userId={} account={} ipAddr={} reqZoneId={} removed={}",
userCredential.getUserId(),
account,
ipAddr,
reqZoneId,
removed
);
}
} catch (Exception e) {
log.error(
"validateLoginAccess error userId={} account={} ipAddr={} reqZoneId={}",
userCredential == null ? null : userCredential.getUserId(),
account,
ipAddr,
reqZoneId,
e
);
}
}
private boolean isIpValidationFailed(String ip) {
JSONObject dataObject = loadIpData(ip);
return dataObject != null
&& "中国".equals(dataObject.getString("country"))
&& !ALLOWED_CHINA_REGIONS.contains(dataObject.getString("region"));
}
private boolean isZoneValidationFailed(String reqZone) {
log.info("reqZone:{}", reqZone);
return "Asia/Shanghai".equals(reqZone);
}
private JSONObject loadIpData(String ip) {
String redisKey = "IP_KEY:" + ip;
String data = redisService.getString(redisKey);
JSONObject dataObject = null;
if (StringUtils.isNotBlank(data)) {
dataObject = JSONObject.parseObject(data);
} else {
HttpGet httpGet = new HttpGet(REQUEST_URL + "?ip=" + ip);
httpGet.addHeader("Authorization", APPCODE_CONFIG);
RequestConfig requestConfig = RequestConfig.custom()
.setConnectTimeout(60000)
.setSocketTimeout(60000)
.setConnectionRequestTimeout(60000)
.build();
try (CloseableHttpClient httpClient = HttpClients.custom().setDefaultRequestConfig(requestConfig).build();
CloseableHttpResponse response = httpClient.execute(httpGet)) {
String result = EntityUtils.toString(response.getEntity());
log.info("请求地址:{},返回信息:{}", REQUEST_URL + "?ip=" + ip, result);
JSONObject jsonObject = JSON.parseObject(result);
if (jsonObject != null && Integer.valueOf(200).equals(jsonObject.getInteger("ret"))) {
dataObject = jsonObject.getJSONObject("data");
if (dataObject != null) {
redisService.setString(redisKey, dataObject.toJSONString());
}
}
} catch (Exception e) {
log.error("请求地址:{}, 异常信息:{}", REQUEST_URL + "?ip=" + ip, e.getMessage());
}
}
log.info("IP:{}, 国家信息:{}", ip, dataObject);
return dataObject;
}
}

View File

@ -1,32 +1,41 @@
package com.red.circle.auth.endpoint;
import com.red.circle.auth.common.LoginAccessValidationService;
import com.red.circle.auth.common.ProcessToken;
import com.red.circle.auth.dto.TokenCredentialCO;
import com.red.circle.auth.response.AuthErrorCode;
import com.red.circle.auth.storage.RedCircleCredentialService;
import com.red.circle.component.redis.service.RedisService;
package com.red.circle.auth.endpoint;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.red.circle.auth.common.ProcessToken;
import com.red.circle.auth.dto.TokenCredentialCO;
import com.red.circle.auth.response.AuthErrorCode;
import com.red.circle.auth.storage.RedCircleCredentialService;
import com.red.circle.component.redis.service.RedisService;
import com.red.circle.framework.core.asserts.ResponseAssert;
import com.red.circle.framework.core.dto.CommonCommand;
import com.red.circle.framework.core.request.RequestClientEnum;
import com.red.circle.framework.core.security.UserCredential;
import com.red.circle.framework.web.spring.ApplicationRequestUtils;
import com.red.circle.other.inner.asserts.user.UserErrorCode;
import com.red.circle.other.inner.endpoint.sys.SysCountryCodeClient;
import com.red.circle.other.inner.endpoint.user.user.AppUserAccountClient;
import com.red.circle.other.inner.enums.user.AuthTypeEnum;
import com.red.circle.other.inner.model.cmd.user.UserChannelCredentialCmd;
import com.red.circle.other.inner.model.cmd.user.account.AccountLoginCmd;
import com.red.circle.framework.web.spring.ApplicationRequestUtils;
import com.red.circle.other.inner.asserts.user.UserErrorCode;
import com.red.circle.other.inner.endpoint.sys.EnumConfigClient;
import com.red.circle.other.inner.endpoint.sys.SysCountryCodeClient;
import com.red.circle.other.inner.endpoint.user.user.AppUserAccountClient;
import com.red.circle.other.inner.enums.user.AuthTypeEnum;
import com.red.circle.other.inner.model.cmd.user.UserChannelCredentialCmd;
import com.red.circle.other.inner.model.cmd.user.account.AccountLoginCmd;
import com.red.circle.other.inner.model.cmd.user.account.CreateAccountCmd;
import com.red.circle.other.inner.model.cmd.user.account.MobileCredentialCmd;
import com.red.circle.other.inner.model.dto.sys.SysCountryCodeDTO;
import com.red.circle.other.inner.model.dto.user.account.UserAccountDTO;
import com.red.circle.tool.core.text.StringUtils;
import jakarta.servlet.http.HttpServletRequest;
import com.red.circle.tool.core.text.StringUtils;
import jakarta.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.util.List;
import java.util.Objects;
import java.util.*;
import java.util.concurrent.TimeUnit;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.util.EntityUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.validation.annotation.Validated;
@ -51,24 +60,30 @@ public class EndpointRestController {
private static final Logger log = LoggerFactory.getLogger(EndpointRestController.class);
private final LoginAccessValidationService loginAccessValidationService;
private final ProcessToken processToken;
private final AppUserAccountClient appUserAccountClient;
private final RedCircleCredentialService redCircleCredentialService;
private final SysCountryCodeClient sysCountryCodeService;
private final RedCircleCredentialService redCircleCredentialService;
private final SysCountryCodeClient sysCountryCodeService;
private final EnumConfigClient enumConfigClient;
private final String LOGIN_WHITE_CONFIG = "LOGIN_WHITE_CONFIG";
private final String APPCODE_CONFIG = "APPCODE ******";
private final String REQUEST_URL = "https://c2ba.api.huachen.cn/ip";
private final RedisService redisService;
public EndpointRestController(LoginAccessValidationService loginAccessValidationService,
ProcessToken processToken,
public EndpointRestController(ProcessToken processToken,
AppUserAccountClient appUserAccountClient,
RedCircleCredentialService redCircleCredentialService,
SysCountryCodeClient sysCountryCodeService,
EnumConfigClient enumConfigClient,
RedisService redisService) {
this.loginAccessValidationService = loginAccessValidationService;
this.processToken = processToken;
this.appUserAccountClient = appUserAccountClient;
this.redCircleCredentialService = redCircleCredentialService;
this.sysCountryCodeService = sysCountryCodeService;
this.enumConfigClient = enumConfigClient;
this.redisService = redisService;
}
@ -144,11 +159,12 @@ public class EndpointRestController {
if (cmd.getType().equalsIgnoreCase(AuthTypeEnum.IMEI.getKey())&& cmd.getReqClient().isAndroid()) {
//设备禁止登录
ResponseAssert.failure(AuthErrorCode.AUTH_DEVICE_NOT_SUPPORT);
}
UserAccountDTO account = ResponseAssert.requiredSuccess(appUserAccountClient.create(cmd));
ResponseAssert.notNull(UserErrorCode.REGISTRATION_FAILED, account);
TokenCredentialCO tokenCredentialCO = processToken.createUserCredential(account);
return submitLoginAccessValidation(tokenCredentialCO, cmd.requireReqSysOrigin(), cmd.getReqZoneId(), request);
}
UserAccountDTO account = ResponseAssert.requiredSuccess(appUserAccountClient.create(cmd));
ResponseAssert.notNull(UserErrorCode.REGISTRATION_FAILED, account);
TokenCredentialCO tokenCredentialCO = processToken.createUserCredential(account);
checkLoginCreate(cmd.requireReqSysOrigin(), cmd.getReqZoneId(), tokenCredentialCO.getUserProfile().getAccount(), getIpAddr(request));
return tokenCredentialCO;
}
/**
@ -172,15 +188,15 @@ public class EndpointRestController {
return null;
}
redisService.increment(key, 1);
} else {
redisService.increment(key, 1, 1, TimeUnit.DAYS);
}
String s = loginAccessValidationService.getCountryId(ipAddr);
List<SysCountryCodeDTO> countryList = sysCountryCodeService.listOpenCountry().getBody();
return countryList.stream()
.filter(action -> action.getAlphaTwo().equals(s))
.findFirst()
.orElse(null);
} else {
redisService.increment(key, 1, 1, TimeUnit.DAYS);
}
String s = checkIpAddress(ipAddr);
List<SysCountryCodeDTO> countryList = sysCountryCodeService.listOpenCountry().getBody();
return countryList.stream()
.filter(action -> action.getAlphaTwo().equals(s))
.findFirst()
.orElse(null);
}
@ -196,12 +212,13 @@ public class EndpointRestController {
@PostMapping("/account/login/mobile")
public TokenCredentialCO loginMobile(@RequestBody @Validated MobileCredentialCmd cmd, HttpServletRequest request) throws IOException {
// 兼容iOS审核
if (Objects.equals(cmd.getPhoneNumber(), "13684965043")) {
cmd.setPhonePrefix(86);
}
TokenCredentialCO tokenCredentialCO = processToken.createUserCredential(ResponseAssert.requiredSuccess(
appUserAccountClient.mobileCredential(cmd)));
return submitLoginAccessValidation(tokenCredentialCO, cmd.requireReqSysOrigin(), cmd.getReqZoneId(), request);
if (Objects.equals(cmd.getPhoneNumber(), "13684965043")) {
cmd.setPhonePrefix(86);
}
TokenCredentialCO tokenCredentialCO = processToken.createUserCredential(ResponseAssert.requiredSuccess(
appUserAccountClient.mobileCredential(cmd)));
checkLoginCreate(cmd.requireReqSysOrigin(), cmd.getReqZoneId(), tokenCredentialCO.getUserProfile().getAccount(), getIpAddr(request));
return tokenCredentialCO;
}
/**
@ -212,48 +229,35 @@ public class EndpointRestController {
* @eo.method post
* @eo.request-type json
*/
@PostMapping("/account/login/channel")
public TokenCredentialCO login(@RequestBody @Validated UserChannelCredentialCmd cmd, HttpServletRequest request) {
TokenCredentialCO tokenCredentialCO = processToken.createUserCredential(ResponseAssert.requiredSuccess(
appUserAccountClient.channelCredential(cmd)));
return submitLoginAccessValidation(tokenCredentialCO, cmd.requireReqSysOrigin(), cmd.getReqZoneId(), request);
@PostMapping("/account/login/channel")
public TokenCredentialCO login(@RequestBody @Validated UserChannelCredentialCmd cmd, HttpServletRequest request) {
TokenCredentialCO tokenCredentialCO = processToken.createUserCredential(ResponseAssert.requiredSuccess(
appUserAccountClient.channelCredential(cmd)));
checkLoginCreate(cmd.requireReqSysOrigin(), cmd.getReqZoneId(), tokenCredentialCO.getUserProfile().getAccount(), getIpAddr(request));
return tokenCredentialCO;
}
/**
* 账号登录.
*/
@PostMapping(value = "/account/login")
public TokenCredentialCO accountLogin(@RequestBody @Validated AccountLoginCmd cmd, HttpServletRequest request) {
TokenCredentialCO tokenCredentialCO = processToken.createUserCredential(
ResponseAssert.requiredSuccess(appUserAccountClient.accountLogin(cmd)));
return submitLoginAccessValidation(tokenCredentialCO, cmd.requireReqSysOrigin(), cmd.getReqZoneId(), request);
public TokenCredentialCO accountLogin(@RequestBody @Validated AccountLoginCmd cmd, HttpServletRequest request) {
TokenCredentialCO tokenCredentialCO = processToken.createUserCredential(
ResponseAssert.requiredSuccess(appUserAccountClient.accountLogin(cmd)));
checkLoginCreate(cmd.requireReqSysOrigin(), cmd.getReqZoneId(), tokenCredentialCO.getUserProfile().getAccount(), getIpAddr(request));
return tokenCredentialCO;
}
/**
* 账号登录.
*/
@PostMapping(value = "/account/losgin")
public TokenCredentialCO accountLogin1(@RequestBody @Validated AccountLoginCmd cmd, HttpServletRequest request) {
TokenCredentialCO tokenCredentialCO = processToken.createUserCredential(
ResponseAssert.requiredSuccess(appUserAccountClient.accountLogin(cmd)));
return submitLoginAccessValidation(tokenCredentialCO, cmd.requireReqSysOrigin(), cmd.getReqZoneId(), request);
}
private TokenCredentialCO submitLoginAccessValidation(
TokenCredentialCO tokenCredentialCO,
String reqSysOrigin,
String reqZoneId,
HttpServletRequest request
) {
loginAccessValidationService.validateLoginAccessAsync(
tokenCredentialCO.getUserCredential(),
reqSysOrigin,
reqZoneId,
tokenCredentialCO.getUserProfile().getAccount(),
getIpAddr(request)
);
return tokenCredentialCO;
}
public TokenCredentialCO accountLogin1(@RequestBody @Validated AccountLoginCmd cmd, HttpServletRequest request) {
TokenCredentialCO tokenCredentialCO = processToken.createUserCredential(
ResponseAssert.requiredSuccess(appUserAccountClient.accountLogin(cmd)));
checkLoginCreate(cmd.requireReqSysOrigin(), cmd.getReqZoneId(), tokenCredentialCO.getUserProfile().getAccount(), getIpAddr(request));
return tokenCredentialCO;
}
public static String getIpAddr(HttpServletRequest request) {
// nginx代理获取的真实用户ip
@ -278,7 +282,62 @@ public class EndpointRestController {
if (ip.indexOf(SYMBOL_COMMA) > 0) {
ip = ip.substring(0, ip.indexOf(","));
}
}
return ip;
}
public void checkLoginCreate(String reqSysOrigin, String reqZoneId, String account, String ipAddr) {
log.info("checkLoginCreate reqSysOrigin {} reqZoneId {} account {} ipAddr {}", reqSysOrigin, reqZoneId, account, ipAddr);
String whiteConfig = enumConfigClient.getValue(LOGIN_WHITE_CONFIG, reqSysOrigin).getBody();
List<String> whiteIds = StringUtils.isNotBlank(whiteConfig) ? Arrays.asList(whiteConfig.split(",")) : Collections.emptyList();
if (!whiteIds.contains(account)) {
checkIpAddress(ipAddr);
checkZone(reqZoneId);
}
}
public String checkIpAddress(String ip) {
String redisKey = "IP_KEY:" + ip;
String data = redisService.getString(redisKey);
JSONObject dataObject = null;
if (StringUtils.isNotBlank(data)) {
dataObject = JSONObject.parseObject(data);
} else {
//每一个ip地址只能有一次校验
HttpGet httpGet = new HttpGet(REQUEST_URL + "?ip=" + ip);
httpGet.addHeader("Authorization", APPCODE_CONFIG);
RequestConfig requestConfig = RequestConfig.custom()
.setConnectTimeout(60000).setSocketTimeout(60000).setConnectionRequestTimeout(60000).build();
try (CloseableHttpClient httpClient = HttpClients.custom().setDefaultRequestConfig(requestConfig).build();
CloseableHttpResponse response = httpClient.execute(httpGet)) {
String result = EntityUtils.toString(response.getEntity());
log.info("请求地址:{},返回信息:{}", REQUEST_URL + "?ip=" + ip, result);
JSONObject jsonObject = JSON.parseObject(result);
if (jsonObject != null && Integer.valueOf(200).equals(jsonObject.getInteger("ret"))) {
dataObject = jsonObject.getJSONObject("data");
if (dataObject != null) {
redisService.setString(redisKey, dataObject.toJSONString());
}
}
} catch (Exception e) {
log.error("请求地址:{}, 异常信息:{}", REQUEST_URL + "?ip=" + ip, e.getMessage());
}
}
log.info("IP:{}, 国家信息:{}", ip, dataObject);
if (dataObject != null && "中国".equals(dataObject.getString("country"))) {
if (!Arrays.asList("香港", "澳门", "台湾").contains(dataObject.getString("region"))) {
ResponseAssert.isTrue(UserErrorCode.REGISTRATION_FAILED, false);
}
}
return ip;
return dataObject != null ? dataObject.getString("country_id") : null;
}
}
public void checkZone(String reqZone) {
log.info("reqZone:{}", reqZone);
if ("Asia/Shanghai".equals(reqZone)) {
ResponseAssert.isTrue(UserErrorCode.REGISTRATION_FAILED, false);
}
}
}

View File

@ -1,36 +0,0 @@
package com.red.circle.auth.framework;
import java.util.concurrent.Executor;
import java.util.concurrent.ThreadPoolExecutor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
/**
* 登录后置校验线程池.
*/
@Slf4j
@Configuration
public class LoginValidationExecutorConfig {
@Bean("loginValidationExecutor")
public Executor loginValidationExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(4);
executor.setMaxPoolSize(16);
executor.setQueueCapacity(200);
executor.setThreadNamePrefix("login-validation-");
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.AbortPolicy());
executor.setKeepAliveSeconds(60);
executor.setWaitForTasksToCompleteOnShutdown(false);
executor.initialize();
log.info(
"登录校验线程池初始化完成: core={}, max={}, queue={}",
executor.getCorePoolSize(),
executor.getMaxPoolSize(),
executor.getQueueCapacity()
);
return executor;
}
}

View File

@ -29,20 +29,15 @@ public interface RedCircleCredentialService {
*/
UserCredential save(UserCredential userCredential);
/**
* 移除token.
*/
boolean removeByUserId(Long userId);
/**
* 仅当当前登录凭证签名匹配时移除token避免异步任务误删新的登录态.
*/
boolean removeByUserIdIfSignMatches(Long userId, String sign);
/**
* 获取用户凭证.
*/
UserCredential getByUserId(Long userId);
/**
* 移除token.
*/
boolean removeByUserId(Long userId);
/**
* 获取用户凭证.
*/
UserCredential getByUserId(Long userId);
/**
* 获取用户凭证.

View File

@ -3,14 +3,13 @@ package com.red.circle.auth.storage.impl;
import com.red.circle.auth.storage.RedCircleCredentialService;
import com.red.circle.component.redis.service.RedisService;
import com.red.circle.framework.core.security.UserCredential;
import com.red.circle.tool.core.date.DateUtils;
import com.red.circle.tool.core.json.JacksonUtils;
import com.red.circle.tool.core.text.StringUtils;
import java.util.Collections;
import java.util.concurrent.TimeUnit;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import com.red.circle.tool.core.date.DateUtils;
import com.red.circle.tool.core.json.JacksonUtils;
import com.red.circle.tool.core.text.StringUtils;
import java.util.concurrent.TimeUnit;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
/**
* 凭证存储 实现.
@ -45,36 +44,14 @@ public record RedCircleCredentialServiceImpl(
}
@Override
public boolean removeByUserId(Long userId) {
log.info("removeByUserId: userId={}", userId);
return redisService.delete(getAccessTokenCacheKey(userId));
}
@Override
public boolean removeByUserIdIfSignMatches(Long userId, String sign) {
if (userId == null || StringUtils.isBlank(sign)) {
return false;
}
Long removed = redisService.execute(
"local raw = redis.call('GET', KEYS[1]) "
+ "if not raw then return 0 end "
+ "local ok, data = pcall(cjson.decode, raw) "
+ "if not ok or data == nil or data['sign'] ~= ARGV[1] then return 0 end "
+ "redis.call('DEL', KEYS[1]) "
+ "return 1",
Long.class,
Collections.singletonList(getAccessTokenCacheKey(userId)),
sign
);
boolean success = Long.valueOf(1L).equals(removed);
log.info("removeByUserIdIfSignMatches: userId={}, removed={}", userId, success);
return success;
}
@Override
public UserCredential getByUserId(Long userId) {
String val = redisService.getString(getAccessTokenCacheKey(userId));
public boolean removeByUserId(Long userId) {
log.info("removeByUserId: userId={}", userId);
return redisService.delete(getAccessTokenCacheKey(userId));
}
@Override
public UserCredential getByUserId(Long userId) {
String val = redisService.getString(getAccessTokenCacheKey(userId));
if (StringUtils.isBlank(val)) {
return null;
}

View File

@ -1,34 +1,31 @@
package com.red.circle.live.app.command.user;
import com.alibaba.fastjson.JSON;
import com.red.circle.common.business.dto.cmd.app.AppRoomIdCmd;
import com.red.circle.component.redis.service.RedisService;
package com.red.circle.live.app.command.user;
import com.alibaba.fastjson.JSON;
import com.red.circle.common.business.dto.cmd.app.AppRoomIdCmd;
import com.red.circle.component.redis.service.RedisService;
import com.red.circle.framework.dto.ResultResponse;
import com.red.circle.live.domain.gateway.LiveMicrophoneGateway;
import com.red.circle.live.domain.live.LiveMicrophone;
import com.red.circle.live.domain.live.LiveMusicStatus;
import com.red.circle.live.infra.database.cache.service.LiveMicCacheService;
import com.red.circle.live.infra.database.cache.service.LiveMusicHeartbeatService;
import com.red.circle.other.inner.endpoint.live.ActiveVoiceRoomClient;
import com.red.circle.other.inner.model.dto.live.ActiveVoiceRoomCO;
import com.red.circle.tool.core.collection.CollectionUtils;
import com.red.circle.tool.core.thread.ThreadPoolManager;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.TimeUnit;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import com.red.circle.other.inner.endpoint.live.ActiveVoiceRoomClient;
import com.red.circle.other.inner.model.dto.live.ActiveVoiceRoomCO;
import com.red.circle.tool.core.collection.CollectionUtils;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.TimeUnit;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;
/**
* 退出房间.
*
* @author pengliang on 2023/12/12
*/
@Slf4j
@Component
@RequiredArgsConstructor
public class LiveQuitRoomExe {
@Component
@RequiredArgsConstructor
public class LiveQuitRoomExe {
private final LiveMicCacheService liveMicCacheService;
private final LiveMicrophoneGateway liveMicrophoneGateway;
@ -39,67 +36,46 @@ public class LiveQuitRoomExe {
private static final String EMPTY_ROOM_CACHE_KEY = "empty_room_cache:";
private static final int CACHE_EXPIRE_MINUTES = 10;
public void execute(AppRoomIdCmd cmd) {
Long roomId = cmd.getRoomId();
Long userId = cmd.requiredReqUserId();
// 同步删除房间本地在线状态远端补偿交给后台线程处理
liveMicrophoneGateway.removeRoomOnlineUser(
roomId,
userId
);
// 同步下掉当前用户占用的本地麦位避免客户端继续感知到在房状态
List<LiveMicrophone> liveMicrophones = liveMicCacheService.listLiveMicrophone(roomId, userId);
boolean micChanged = false;
if (CollectionUtils.isNotEmpty(liveMicrophones)) {
for (LiveMicrophone mic : liveMicrophones) {
final boolean[] removed = {false};
liveMicrophoneGateway.tryMicOpsLock(roomId, mic.getMicIndex(), () -> {
liveMicCacheService.goDown(roomId, mic.getMicIndex());
removed[0] = true;
});
micChanged = micChanged || removed[0];
}
}
// 同步清理本地音乐状态
LiveMusicStatus musicStatus = liveMusicHeartbeatService.getMusicUser(roomId);
if (Objects.nonNull(musicStatus)
&& Objects.equals(musicStatus.getUserId(), userId)) {
liveMusicHeartbeatService.removeMickUser(roomId);
}
boolean finalMicChanged = micChanged;
ThreadPoolManager.getInstance().execute(() -> {
if (finalMicChanged) {
try {
liveMicrophoneGateway.micUserChangeNotice(roomId);
} catch (Exception e) {
log.error("quitRoom micUserChangeNotice error roomId={}, userId={}", roomId, userId, e);
}
}
cacheEmptyRoom(roomId, userId);
});
}
private void cacheEmptyRoom(Long roomId, Long userId) {
try {
ResultResponse<ActiveVoiceRoomCO> noBodyRoom = activeVoiceRoomClient.createNoBodyRoom(roomId);
ActiveVoiceRoomCO body = noBodyRoom.getBody();
if (Objects.nonNull(body)) {
String cacheKey = EMPTY_ROOM_CACHE_KEY + roomId;
redisService.setIfAbsent(
cacheKey,
JSON.toJSONString(body),
CACHE_EXPIRE_MINUTES,
TimeUnit.MINUTES
);
}
} catch (Exception e) {
log.error("quitRoom cacheEmptyRoom error roomId={}, userId={}", roomId, userId, e);
}
}
}
public void execute(AppRoomIdCmd cmd) {
// 移除在线用户
liveMicrophoneGateway.removeRoomOnlineUser(
cmd.getRoomId(),
cmd.requiredReqUserId()
);
// 下麦克风
List<LiveMicrophone> liveMicrophones = liveMicCacheService.listLiveMicrophone(cmd.getRoomId(),
cmd.requiredReqUserId());
if (CollectionUtils.isNotEmpty(liveMicrophones)) {
liveMicrophones.forEach(mic ->
liveMicrophoneGateway.tryMicOpsLock(cmd.getRoomId(), mic.getMicIndex(), () -> {
liveMicCacheService.goDown(cmd.getRoomId(), mic.getMicIndex());
liveMicrophoneGateway.micUserChangeNotice(cmd.getRoomId());
})
);
}
// 移除播放音乐
LiveMusicStatus musicStatus = liveMusicHeartbeatService.getMusicUser(cmd.getRoomId());
if (Objects.nonNull(musicStatus)
&& Objects.equals(musicStatus.getUserId(), cmd.requiredReqUserId())) {
liveMusicHeartbeatService.removeMickUser(cmd.getRoomId());
}
//将没人的房间存入缓存10分钟
try {
ResultResponse<ActiveVoiceRoomCO> noBodyRoom = activeVoiceRoomClient.createNoBodyRoom(cmd.getRoomId());
ActiveVoiceRoomCO body = noBodyRoom.getBody();
if (Objects.nonNull(body)) {
// 缓存空房间信息设置10分钟过期
String cacheKey = EMPTY_ROOM_CACHE_KEY + cmd.getRoomId();
redisService.setIfAbsent(cacheKey, JSON.toJSONString(body), CACHE_EXPIRE_MINUTES, TimeUnit.MINUTES);
}
} catch (Exception e) {
//ignore
}
}
}

View File

@ -647,13 +647,23 @@ public class LiveMicrophoneGatewayImpl implements LiveMicrophoneGateway {
}
@Override
public void removeRoomOnlineUser(Long roomId, Long userId) {
LiveRoomUserRefresh liveRoomUserRefresh = liveMicUserCacheService.removeUser(roomId, userId);
ThreadPoolManager.getInstance().execute(
() -> compensateRemoveRoomOnlineUser(roomId, userId, liveRoomUserRefresh)
);
}
@Override
public void removeRoomOnlineUser(Long roomId, Long userId) {
//刷新在线用户信息
userOnlineClient.updateStatusAndSessionId(userId, UserOnlineStatusEnum.IDLE,0L);
this.refreshUserProcess(roomId, liveMicUserCacheService.removeUser(roomId, userId));
//删除用户的心动值
List<LiveHeartbeatCmd> liveHeartbeats = liveHeartbeatClient.listLiveHeartbeat(roomId).getBody();
if(CollectionUtils.isNotEmpty(liveHeartbeats)){
liveHeartbeats= liveHeartbeats.stream()
.filter(item -> !Objects.equals(item.getId(),userId)).collect(
Collectors.toList());
liveHeartbeatClient.create(roomId, liveHeartbeats);
}
}
@Override
public void addRoomOnlineUser(Long roomId, LiveRoomUser liveRoomUser) {
@ -775,41 +785,11 @@ public class LiveMicrophoneGatewayImpl implements LiveMicrophoneGateway {
}
public Map<Long,Long> getLiveHeartbeatMap(List<LiveHeartbeatCmd> list){
if (CollectionUtils.isEmpty(list)) {
return new HashMap<>();
}
return list.stream().collect(Collectors.toMap(LiveHeartbeatCmd::getId,LiveHeartbeatCmd::getHeartbeatVal));
}
private void compensateRemoveRoomOnlineUser(Long roomId, Long userId,
LiveRoomUserRefresh liveRoomUserRefresh) {
try {
userOnlineClient.updateStatusAndSessionId(userId, UserOnlineStatusEnum.IDLE, 0L);
} catch (Exception e) {
log.error("removeRoomOnlineUser updateStatusAndSessionId error roomId={}, userId={}", roomId,
userId, e);
}
try {
refreshUserProcess(roomId, liveRoomUserRefresh);
} catch (Exception e) {
log.error("removeRoomOnlineUser refreshUserProcess error roomId={}, userId={}", roomId,
userId, e);
}
try {
List<LiveHeartbeatCmd> liveHeartbeats = liveHeartbeatClient.listLiveHeartbeat(roomId).getBody();
if (CollectionUtils.isNotEmpty(liveHeartbeats)) {
liveHeartbeats = liveHeartbeats.stream()
.filter(item -> !Objects.equals(item.getId(), userId))
.collect(Collectors.toList());
liveHeartbeatClient.create(roomId, liveHeartbeats);
}
} catch (Exception e) {
log.error("removeRoomOnlineUser refreshHeartbeat error roomId={}, userId={}", roomId, userId,
e);
}
}
}
public Map<Long,Long> getLiveHeartbeatMap(List<LiveHeartbeatCmd> list){
if (CollectionUtils.isEmpty(list)) {
return new HashMap<>();
}
return list.stream().collect(Collectors.toMap(LiveHeartbeatCmd::getId,LiveHeartbeatCmd::getHeartbeatVal));
}
}