feat: async login access validation
This commit is contained in:
parent
ed4a95da29
commit
25c4152325
@ -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<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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,41 +1,32 @@
|
|||||||
package com.red.circle.auth.endpoint;
|
package com.red.circle.auth.endpoint;
|
||||||
|
|
||||||
import com.alibaba.fastjson.JSON;
|
import com.red.circle.auth.common.LoginAccessValidationService;
|
||||||
import com.alibaba.fastjson.JSONObject;
|
import com.red.circle.auth.common.ProcessToken;
|
||||||
import com.red.circle.auth.common.ProcessToken;
|
import com.red.circle.auth.dto.TokenCredentialCO;
|
||||||
import com.red.circle.auth.dto.TokenCredentialCO;
|
import com.red.circle.auth.response.AuthErrorCode;
|
||||||
import com.red.circle.auth.response.AuthErrorCode;
|
import com.red.circle.auth.storage.RedCircleCredentialService;
|
||||||
import com.red.circle.auth.storage.RedCircleCredentialService;
|
import com.red.circle.component.redis.service.RedisService;
|
||||||
import com.red.circle.component.redis.service.RedisService;
|
|
||||||
import com.red.circle.framework.core.asserts.ResponseAssert;
|
import com.red.circle.framework.core.asserts.ResponseAssert;
|
||||||
import com.red.circle.framework.core.dto.CommonCommand;
|
import com.red.circle.framework.core.dto.CommonCommand;
|
||||||
import com.red.circle.framework.core.request.RequestClientEnum;
|
import com.red.circle.framework.core.request.RequestClientEnum;
|
||||||
import com.red.circle.framework.core.security.UserCredential;
|
import com.red.circle.framework.core.security.UserCredential;
|
||||||
import com.red.circle.framework.web.spring.ApplicationRequestUtils;
|
import com.red.circle.framework.web.spring.ApplicationRequestUtils;
|
||||||
import com.red.circle.other.inner.asserts.user.UserErrorCode;
|
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.sys.SysCountryCodeClient;
|
import com.red.circle.other.inner.endpoint.user.user.AppUserAccountClient;
|
||||||
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.enums.user.AuthTypeEnum;
|
import com.red.circle.other.inner.model.cmd.user.UserChannelCredentialCmd;
|
||||||
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.AccountLoginCmd;
|
|
||||||
import com.red.circle.other.inner.model.cmd.user.account.CreateAccountCmd;
|
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.cmd.user.account.MobileCredentialCmd;
|
||||||
import com.red.circle.other.inner.model.dto.sys.SysCountryCodeDTO;
|
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.other.inner.model.dto.user.account.UserAccountDTO;
|
||||||
import com.red.circle.tool.core.text.StringUtils;
|
import com.red.circle.tool.core.text.StringUtils;
|
||||||
import jakarta.servlet.http.HttpServletRequest;
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.*;
|
import java.util.List;
|
||||||
|
import java.util.Objects;
|
||||||
import java.util.concurrent.TimeUnit;
|
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.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
import org.springframework.validation.annotation.Validated;
|
import org.springframework.validation.annotation.Validated;
|
||||||
@ -60,30 +51,24 @@ public class EndpointRestController {
|
|||||||
|
|
||||||
private static final Logger log = LoggerFactory.getLogger(EndpointRestController.class);
|
private static final Logger log = LoggerFactory.getLogger(EndpointRestController.class);
|
||||||
|
|
||||||
|
private final LoginAccessValidationService loginAccessValidationService;
|
||||||
private final ProcessToken processToken;
|
private final ProcessToken processToken;
|
||||||
private final AppUserAccountClient appUserAccountClient;
|
private final AppUserAccountClient appUserAccountClient;
|
||||||
private final RedCircleCredentialService redCircleCredentialService;
|
private final RedCircleCredentialService redCircleCredentialService;
|
||||||
private final SysCountryCodeClient sysCountryCodeService;
|
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;
|
private final RedisService redisService;
|
||||||
|
|
||||||
public EndpointRestController(ProcessToken processToken,
|
public EndpointRestController(LoginAccessValidationService loginAccessValidationService,
|
||||||
|
ProcessToken processToken,
|
||||||
AppUserAccountClient appUserAccountClient,
|
AppUserAccountClient appUserAccountClient,
|
||||||
RedCircleCredentialService redCircleCredentialService,
|
RedCircleCredentialService redCircleCredentialService,
|
||||||
SysCountryCodeClient sysCountryCodeService,
|
SysCountryCodeClient sysCountryCodeService,
|
||||||
EnumConfigClient enumConfigClient,
|
|
||||||
RedisService redisService) {
|
RedisService redisService) {
|
||||||
|
this.loginAccessValidationService = loginAccessValidationService;
|
||||||
this.processToken = processToken;
|
this.processToken = processToken;
|
||||||
this.appUserAccountClient = appUserAccountClient;
|
this.appUserAccountClient = appUserAccountClient;
|
||||||
this.redCircleCredentialService = redCircleCredentialService;
|
this.redCircleCredentialService = redCircleCredentialService;
|
||||||
this.sysCountryCodeService = sysCountryCodeService;
|
this.sysCountryCodeService = sysCountryCodeService;
|
||||||
this.enumConfigClient = enumConfigClient;
|
|
||||||
this.redisService = redisService;
|
this.redisService = redisService;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -159,12 +144,11 @@ public class EndpointRestController {
|
|||||||
if (cmd.getType().equalsIgnoreCase(AuthTypeEnum.IMEI.getKey())&& cmd.getReqClient().isAndroid()) {
|
if (cmd.getType().equalsIgnoreCase(AuthTypeEnum.IMEI.getKey())&& cmd.getReqClient().isAndroid()) {
|
||||||
//设备禁止登录
|
//设备禁止登录
|
||||||
ResponseAssert.failure(AuthErrorCode.AUTH_DEVICE_NOT_SUPPORT);
|
ResponseAssert.failure(AuthErrorCode.AUTH_DEVICE_NOT_SUPPORT);
|
||||||
}
|
}
|
||||||
UserAccountDTO account = ResponseAssert.requiredSuccess(appUserAccountClient.create(cmd));
|
UserAccountDTO account = ResponseAssert.requiredSuccess(appUserAccountClient.create(cmd));
|
||||||
ResponseAssert.notNull(UserErrorCode.REGISTRATION_FAILED, account);
|
ResponseAssert.notNull(UserErrorCode.REGISTRATION_FAILED, account);
|
||||||
TokenCredentialCO tokenCredentialCO = processToken.createUserCredential(account);
|
TokenCredentialCO tokenCredentialCO = processToken.createUserCredential(account);
|
||||||
checkLoginCreate(cmd.requireReqSysOrigin(), cmd.getReqZoneId(), tokenCredentialCO.getUserProfile().getAccount(), getIpAddr(request));
|
return submitLoginAccessValidation(tokenCredentialCO, cmd.requireReqSysOrigin(), cmd.getReqZoneId(), request);
|
||||||
return tokenCredentialCO;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -188,15 +172,15 @@ public class EndpointRestController {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
redisService.increment(key, 1);
|
redisService.increment(key, 1);
|
||||||
} else {
|
} else {
|
||||||
redisService.increment(key, 1, 1, TimeUnit.DAYS);
|
redisService.increment(key, 1, 1, TimeUnit.DAYS);
|
||||||
}
|
}
|
||||||
String s = checkIpAddress(ipAddr);
|
String s = loginAccessValidationService.getCountryId(ipAddr);
|
||||||
List<SysCountryCodeDTO> countryList = sysCountryCodeService.listOpenCountry().getBody();
|
List<SysCountryCodeDTO> countryList = sysCountryCodeService.listOpenCountry().getBody();
|
||||||
return countryList.stream()
|
return countryList.stream()
|
||||||
.filter(action -> action.getAlphaTwo().equals(s))
|
.filter(action -> action.getAlphaTwo().equals(s))
|
||||||
.findFirst()
|
.findFirst()
|
||||||
.orElse(null);
|
.orElse(null);
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
@ -212,13 +196,12 @@ public class EndpointRestController {
|
|||||||
@PostMapping("/account/login/mobile")
|
@PostMapping("/account/login/mobile")
|
||||||
public TokenCredentialCO loginMobile(@RequestBody @Validated MobileCredentialCmd cmd, HttpServletRequest request) throws IOException {
|
public TokenCredentialCO loginMobile(@RequestBody @Validated MobileCredentialCmd cmd, HttpServletRequest request) throws IOException {
|
||||||
// 兼容iOS审核
|
// 兼容iOS审核
|
||||||
if (Objects.equals(cmd.getPhoneNumber(), "13684965043")) {
|
if (Objects.equals(cmd.getPhoneNumber(), "13684965043")) {
|
||||||
cmd.setPhonePrefix(86);
|
cmd.setPhonePrefix(86);
|
||||||
}
|
}
|
||||||
TokenCredentialCO tokenCredentialCO = processToken.createUserCredential(ResponseAssert.requiredSuccess(
|
TokenCredentialCO tokenCredentialCO = processToken.createUserCredential(ResponseAssert.requiredSuccess(
|
||||||
appUserAccountClient.mobileCredential(cmd)));
|
appUserAccountClient.mobileCredential(cmd)));
|
||||||
checkLoginCreate(cmd.requireReqSysOrigin(), cmd.getReqZoneId(), tokenCredentialCO.getUserProfile().getAccount(), getIpAddr(request));
|
return submitLoginAccessValidation(tokenCredentialCO, cmd.requireReqSysOrigin(), cmd.getReqZoneId(), request);
|
||||||
return tokenCredentialCO;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -229,35 +212,48 @@ public class EndpointRestController {
|
|||||||
* @eo.method post
|
* @eo.method post
|
||||||
* @eo.request-type json
|
* @eo.request-type json
|
||||||
*/
|
*/
|
||||||
@PostMapping("/account/login/channel")
|
@PostMapping("/account/login/channel")
|
||||||
public TokenCredentialCO login(@RequestBody @Validated UserChannelCredentialCmd cmd, HttpServletRequest request) {
|
public TokenCredentialCO login(@RequestBody @Validated UserChannelCredentialCmd cmd, HttpServletRequest request) {
|
||||||
TokenCredentialCO tokenCredentialCO = processToken.createUserCredential(ResponseAssert.requiredSuccess(
|
TokenCredentialCO tokenCredentialCO = processToken.createUserCredential(ResponseAssert.requiredSuccess(
|
||||||
appUserAccountClient.channelCredential(cmd)));
|
appUserAccountClient.channelCredential(cmd)));
|
||||||
checkLoginCreate(cmd.requireReqSysOrigin(), cmd.getReqZoneId(), tokenCredentialCO.getUserProfile().getAccount(), getIpAddr(request));
|
return submitLoginAccessValidation(tokenCredentialCO, cmd.requireReqSysOrigin(), cmd.getReqZoneId(), request);
|
||||||
return tokenCredentialCO;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 账号登录.
|
* 账号登录.
|
||||||
*/
|
*/
|
||||||
@PostMapping(value = "/account/login")
|
@PostMapping(value = "/account/login")
|
||||||
public TokenCredentialCO accountLogin(@RequestBody @Validated AccountLoginCmd cmd, HttpServletRequest request) {
|
public TokenCredentialCO accountLogin(@RequestBody @Validated AccountLoginCmd cmd, HttpServletRequest request) {
|
||||||
TokenCredentialCO tokenCredentialCO = processToken.createUserCredential(
|
TokenCredentialCO tokenCredentialCO = processToken.createUserCredential(
|
||||||
ResponseAssert.requiredSuccess(appUserAccountClient.accountLogin(cmd)));
|
ResponseAssert.requiredSuccess(appUserAccountClient.accountLogin(cmd)));
|
||||||
checkLoginCreate(cmd.requireReqSysOrigin(), cmd.getReqZoneId(), tokenCredentialCO.getUserProfile().getAccount(), getIpAddr(request));
|
return submitLoginAccessValidation(tokenCredentialCO, cmd.requireReqSysOrigin(), cmd.getReqZoneId(), request);
|
||||||
return tokenCredentialCO;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 账号登录.
|
* 账号登录.
|
||||||
*/
|
*/
|
||||||
@PostMapping(value = "/account/losgin")
|
@PostMapping(value = "/account/losgin")
|
||||||
public TokenCredentialCO accountLogin1(@RequestBody @Validated AccountLoginCmd cmd, HttpServletRequest request) {
|
public TokenCredentialCO accountLogin1(@RequestBody @Validated AccountLoginCmd cmd, HttpServletRequest request) {
|
||||||
TokenCredentialCO tokenCredentialCO = processToken.createUserCredential(
|
TokenCredentialCO tokenCredentialCO = processToken.createUserCredential(
|
||||||
ResponseAssert.requiredSuccess(appUserAccountClient.accountLogin(cmd)));
|
ResponseAssert.requiredSuccess(appUserAccountClient.accountLogin(cmd)));
|
||||||
checkLoginCreate(cmd.requireReqSysOrigin(), cmd.getReqZoneId(), tokenCredentialCO.getUserProfile().getAccount(), getIpAddr(request));
|
return submitLoginAccessValidation(tokenCredentialCO, cmd.requireReqSysOrigin(), cmd.getReqZoneId(), request);
|
||||||
return tokenCredentialCO;
|
}
|
||||||
}
|
|
||||||
|
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) {
|
public static String getIpAddr(HttpServletRequest request) {
|
||||||
// nginx代理获取的真实用户ip
|
// nginx代理获取的真实用户ip
|
||||||
@ -282,62 +278,7 @@ public class EndpointRestController {
|
|||||||
if (ip.indexOf(SYMBOL_COMMA) > 0) {
|
if (ip.indexOf(SYMBOL_COMMA) > 0) {
|
||||||
ip = ip.substring(0, ip.indexOf(","));
|
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 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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -29,15 +29,20 @@ public interface RedCircleCredentialService {
|
|||||||
*/
|
*/
|
||||||
UserCredential save(UserCredential userCredential);
|
UserCredential save(UserCredential userCredential);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 移除token.
|
* 移除token.
|
||||||
*/
|
*/
|
||||||
boolean removeByUserId(Long userId);
|
boolean removeByUserId(Long userId);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取用户凭证.
|
* 仅当当前登录凭证签名匹配时移除token,避免异步任务误删新的登录态.
|
||||||
*/
|
*/
|
||||||
UserCredential getByUserId(Long userId);
|
boolean removeByUserIdIfSignMatches(Long userId, String sign);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取用户凭证.
|
||||||
|
*/
|
||||||
|
UserCredential getByUserId(Long userId);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取用户凭证.
|
* 获取用户凭证.
|
||||||
|
|||||||
@ -3,13 +3,14 @@ package com.red.circle.auth.storage.impl;
|
|||||||
import com.red.circle.auth.storage.RedCircleCredentialService;
|
import com.red.circle.auth.storage.RedCircleCredentialService;
|
||||||
import com.red.circle.component.redis.service.RedisService;
|
import com.red.circle.component.redis.service.RedisService;
|
||||||
import com.red.circle.framework.core.security.UserCredential;
|
import com.red.circle.framework.core.security.UserCredential;
|
||||||
import com.red.circle.tool.core.date.DateUtils;
|
import com.red.circle.tool.core.date.DateUtils;
|
||||||
import com.red.circle.tool.core.json.JacksonUtils;
|
import com.red.circle.tool.core.json.JacksonUtils;
|
||||||
import com.red.circle.tool.core.text.StringUtils;
|
import com.red.circle.tool.core.text.StringUtils;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.Collections;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
import org.springframework.stereotype.Service;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 凭证存储 实现.
|
* 凭证存储 实现.
|
||||||
@ -44,14 +45,36 @@ public record RedCircleCredentialServiceImpl(
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean removeByUserId(Long userId) {
|
public boolean removeByUserId(Long userId) {
|
||||||
log.info("removeByUserId: userId={}", userId);
|
log.info("removeByUserId: userId={}", userId);
|
||||||
return redisService.delete(getAccessTokenCacheKey(userId));
|
return redisService.delete(getAccessTokenCacheKey(userId));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public UserCredential getByUserId(Long userId) {
|
public boolean removeByUserIdIfSignMatches(Long userId, String sign) {
|
||||||
String val = redisService.getString(getAccessTokenCacheKey(userId));
|
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)) {
|
if (StringUtils.isBlank(val)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user