diff --git a/rc-auth/src/main/java/com/red/circle/auth/common/LoginAccessValidationService.java b/rc-auth/src/main/java/com/red/circle/auth/common/LoginAccessValidationService.java new file mode 100644 index 0000000..71c16b5 --- /dev/null +++ b/rc-auth/src/main/java/com/red/circle/auth/common/LoginAccessValidationService.java @@ -0,0 +1,181 @@ +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 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 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; + } +} diff --git a/rc-auth/src/main/java/com/red/circle/auth/endpoint/EndpointRestController.java b/rc-auth/src/main/java/com/red/circle/auth/endpoint/EndpointRestController.java index 4b7e6de..e3b18d0 100644 --- a/rc-auth/src/main/java/com/red/circle/auth/endpoint/EndpointRestController.java +++ b/rc-auth/src/main/java/com/red/circle/auth/endpoint/EndpointRestController.java @@ -1,41 +1,32 @@ -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; +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; 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.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.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.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.*; +import java.util.List; +import java.util.Objects; 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; @@ -60,30 +51,24 @@ 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 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 RedCircleCredentialService redCircleCredentialService; + private final SysCountryCodeClient sysCountryCodeService; private final RedisService redisService; - public EndpointRestController(ProcessToken processToken, + public EndpointRestController(LoginAccessValidationService loginAccessValidationService, + 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; } @@ -159,12 +144,11 @@ 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); - checkLoginCreate(cmd.requireReqSysOrigin(), cmd.getReqZoneId(), tokenCredentialCO.getUserProfile().getAccount(), getIpAddr(request)); - return tokenCredentialCO; + } + 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); } /** @@ -188,15 +172,15 @@ public class EndpointRestController { return null; } redisService.increment(key, 1); - } else { - redisService.increment(key, 1, 1, TimeUnit.DAYS); - } - String s = checkIpAddress(ipAddr); - List 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 = loginAccessValidationService.getCountryId(ipAddr); + List countryList = sysCountryCodeService.listOpenCountry().getBody(); + return countryList.stream() + .filter(action -> action.getAlphaTwo().equals(s)) + .findFirst() + .orElse(null); } @@ -212,13 +196,12 @@ 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))); - checkLoginCreate(cmd.requireReqSysOrigin(), cmd.getReqZoneId(), tokenCredentialCO.getUserProfile().getAccount(), getIpAddr(request)); - return tokenCredentialCO; + 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); } /** @@ -229,35 +212,48 @@ 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))); - checkLoginCreate(cmd.requireReqSysOrigin(), cmd.getReqZoneId(), tokenCredentialCO.getUserProfile().getAccount(), getIpAddr(request)); - return tokenCredentialCO; + @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(value = "/account/login") - 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; + 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); } /** * 账号登录. */ @PostMapping(value = "/account/losgin") - 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 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 static String getIpAddr(HttpServletRequest request) { // nginx代理获取的真实用户ip @@ -282,62 +278,7 @@ 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 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 dataObject != null ? dataObject.getString("country_id") : null; + return ip; } - - public void checkZone(String reqZone) { - log.info("reqZone:{}", reqZone); - if ("Asia/Shanghai".equals(reqZone)) { - ResponseAssert.isTrue(UserErrorCode.REGISTRATION_FAILED, false); - } - } -} +} diff --git a/rc-auth/src/main/java/com/red/circle/auth/framework/LoginValidationExecutorConfig.java b/rc-auth/src/main/java/com/red/circle/auth/framework/LoginValidationExecutorConfig.java new file mode 100644 index 0000000..1d4996b --- /dev/null +++ b/rc-auth/src/main/java/com/red/circle/auth/framework/LoginValidationExecutorConfig.java @@ -0,0 +1,36 @@ +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; + } +} diff --git a/rc-auth/src/main/java/com/red/circle/auth/storage/RedCircleCredentialService.java b/rc-auth/src/main/java/com/red/circle/auth/storage/RedCircleCredentialService.java index 0009382..680d276 100644 --- a/rc-auth/src/main/java/com/red/circle/auth/storage/RedCircleCredentialService.java +++ b/rc-auth/src/main/java/com/red/circle/auth/storage/RedCircleCredentialService.java @@ -29,15 +29,20 @@ public interface RedCircleCredentialService { */ UserCredential save(UserCredential userCredential); - /** - * 移除token. - */ - boolean removeByUserId(Long userId); - - /** - * 获取用户凭证. - */ - UserCredential getByUserId(Long userId); + /** + * 移除token. + */ + boolean removeByUserId(Long userId); + + /** + * 仅当当前登录凭证签名匹配时移除token,避免异步任务误删新的登录态. + */ + boolean removeByUserIdIfSignMatches(Long userId, String sign); + + /** + * 获取用户凭证. + */ + UserCredential getByUserId(Long userId); /** * 获取用户凭证. diff --git a/rc-auth/src/main/java/com/red/circle/auth/storage/impl/RedCircleCredentialServiceImpl.java b/rc-auth/src/main/java/com/red/circle/auth/storage/impl/RedCircleCredentialServiceImpl.java index 7e71cb1..197cb2f 100644 --- a/rc-auth/src/main/java/com/red/circle/auth/storage/impl/RedCircleCredentialServiceImpl.java +++ b/rc-auth/src/main/java/com/red/circle/auth/storage/impl/RedCircleCredentialServiceImpl.java @@ -3,13 +3,14 @@ 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.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.Collections; +import java.util.concurrent.TimeUnit; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; /** * 凭证存储 实现. @@ -44,14 +45,36 @@ public record RedCircleCredentialServiceImpl( } @Override - 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)); + 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)); if (StringUtils.isBlank(val)) { return null; }