mirror of
https://gitee.com/dromara/dax-pay
synced 2026-08-12 15:35:38 +08:00
refactor(auth): 重构 capability-auth 与 service-iam 模块边界, 修复 i18n 不规范
- 认证 SPI: AbstractAuthentication → Authenticator 接口 + AuthenticationTemplate 编排 - 路由鉴权: 新增 AccessPolicy SPI, PasswordStatusCheck 改实现 AccessPolicy - 认证挑战: 新增 PostAuthenticationChallenge SPI, 2FA 逻辑解耦到 TwoFactorAuthenticationChallenge - 登录路由: adaptation 双键匹配(clientCode+loginType), 抽取 AbstractPasswordLoginHandler 消除重复 - 异常下沉: 6 个 IAM 业务异常从 capability-auth 移至 service-iam, 处理迁移到 IamAuthExceptionHandler - 死代码清理: 删除 SecurityConfigService 及 3 个零引用异常 - i18n 修复: NotLoginException 双重前缀 / RouterCheckException+LoginRetryService 直传中文 / LoginFailureException 补 args 构造器
This commit is contained in:
@@ -1,143 +1,31 @@
|
||||
package cn.daxpay.open.payment.merchant.auth;
|
||||
|
||||
import cn.daxpay.open.platform.core.entity.UserDetail;
|
||||
import cn.daxpay.open.platform.core.enums.client.ClientEnum;
|
||||
import cn.daxpay.open.platform.iam.auth.login.AbstractPasswordLoginHandler;
|
||||
import cn.daxpay.open.platform.iam.auth.service.CaptchaService;
|
||||
import cn.daxpay.open.platform.iam.auth.service.IamSecurityConfigService;
|
||||
import cn.daxpay.open.platform.iam.auth.service.LoginRetryService;
|
||||
import cn.daxpay.open.platform.iam.auth.service.PasswordDecryptService;
|
||||
import cn.daxpay.open.platform.iam.entity.user.UserInfo;
|
||||
import cn.daxpay.open.platform.iam.result.user.UserInfoResult;
|
||||
import cn.daxpay.open.platform.iam.service.user.UserQueryService;
|
||||
import cn.daxpay.open.platform.capability.auth.authentication.AbstractAuthentication;
|
||||
import cn.daxpay.open.platform.capability.auth.code.AuthLoginTypeCode;
|
||||
import cn.daxpay.open.platform.capability.auth.entity.AuthInfoResult;
|
||||
import cn.daxpay.open.platform.capability.auth.entity.LoginAuthContext;
|
||||
import cn.daxpay.open.platform.capability.auth.exception.LoginFailureException;
|
||||
import cn.daxpay.open.platform.capability.auth.exception.UserNotFoundException;
|
||||
import cn.daxpay.open.payment.common.context.PaymentContext;
|
||||
import cn.daxpay.open.platform.system.entity.config.platform.security.PlatformLoginSecurityConfig;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.hutool.crypto.digest.BCrypt;
|
||||
import jakarta.annotation.Nullable;
|
||||
import jakarta.annotation.Resource;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import lombok.Getter;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
/// # 商户端密码登录处理器
|
||||
/// # 商户端账号密码登录
|
||||
///
|
||||
/// 仅声明终端编码(merchant), 通用流程继承 [AbstractPasswordLoginHandler]。
|
||||
///
|
||||
@Slf4j
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class MchPasswordLoginHandler implements AbstractAuthentication {
|
||||
|
||||
@Getter
|
||||
private final String ACCOUNT_PARAMETER = "account";
|
||||
|
||||
@Getter
|
||||
private final String PASSWORD_PARAMETER = "password";
|
||||
|
||||
@Getter
|
||||
private final String CAPTCHA_KEY_PARAMETER = "captchaKey";
|
||||
|
||||
@Getter
|
||||
private final String CAPTCHA_CODE_PARAMETER = "captchaCode";
|
||||
|
||||
@Resource
|
||||
@Getter
|
||||
private UserQueryService userQueryService;
|
||||
|
||||
private final LoginRetryService loginRetryService;
|
||||
|
||||
private final CaptchaService captchaService;
|
||||
|
||||
private final IamSecurityConfigService iamSecurityConfigService;
|
||||
|
||||
private final PasswordDecryptService passwordDecryptService;
|
||||
|
||||
private final PaymentContext apiContext;
|
||||
public class MchPasswordLoginHandler extends AbstractPasswordLoginHandler {
|
||||
|
||||
public MchPasswordLoginHandler(LoginRetryService loginRetryService, CaptchaService captchaService,
|
||||
IamSecurityConfigService iamSecurityConfigService,
|
||||
PasswordDecryptService passwordDecryptService) {
|
||||
super(loginRetryService, captchaService, iamSecurityConfigService, passwordDecryptService);
|
||||
}
|
||||
|
||||
/// 商户端终端编码
|
||||
@Override
|
||||
public String getLoginType() {
|
||||
return AuthLoginTypeCode.PASSWORD;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull AuthInfoResult attemptAuthentication(LoginAuthContext context) {
|
||||
String account = this.obtainAccount(context.getRequest());
|
||||
String password = this.obtainPassword(context.getRequest());
|
||||
String captchaKey = this.obtainCaptchaKey(context.getRequest());
|
||||
String captchaCode = this.obtainCaptchaCode(context.getRequest());
|
||||
String clientCode = context.getClientCode();
|
||||
|
||||
UserInfoResult userInfoResult = this.loadUserByClientCodeAndAccount(clientCode, account);
|
||||
UserDetail userDetail = userInfoResult.toUserDetail();
|
||||
|
||||
// 检查验证码(如果需要)
|
||||
this.checkCaptcha(userDetail.getId(), captchaKey, captchaCode);
|
||||
loginRetryService.checkBeforeLogin(userDetail);
|
||||
|
||||
// 比对密码未通过
|
||||
if (!BCrypt.checkpw(password, userInfoResult.getPassword())) {
|
||||
// 必须携带 userId, 否则 LoginRetryService.onLoginFailure 因 userId 为空直接跳过, 失败计数始终为 0
|
||||
throw new LoginFailureException(userDetail.getId(), userDetail.getAccount(), "账号或密码不正确");
|
||||
}
|
||||
|
||||
// 设置密码状态到 UserDetail(超级管理员不设置密码状态限制)
|
||||
if (!userDetail.isAdmin()) {
|
||||
loginRetryService.setPasswordStatusToUserDetail(userDetail);
|
||||
}
|
||||
|
||||
return new AuthInfoResult().setId(userDetail.getId()).setUserDetail(userDetail);
|
||||
}
|
||||
|
||||
/// 检查验证码
|
||||
private void checkCaptcha(Long userId, String captchaKey, String captchaCode) {
|
||||
PlatformLoginSecurityConfig config = iamSecurityConfigService.getLoginSecurity();
|
||||
if (!Boolean.TRUE.equals(config.getCaptchaEnabled())) {
|
||||
return;
|
||||
}
|
||||
int triggerAttempts = config.getCaptchaTriggerAttempts() == null ? 3 : config.getCaptchaTriggerAttempts();
|
||||
int errorCount = loginRetryService.getErrorCount(userId);
|
||||
captchaService.checkOrValidateCaptcha(errorCount, triggerAttempts, captchaKey, captchaCode);
|
||||
}
|
||||
|
||||
/// 根据终端编码+账号加载用户
|
||||
public UserInfoResult loadUserByClientCodeAndAccount(String clientCode, String account) throws UserNotFoundException {
|
||||
// 回退到默认查找方式(终端维度)
|
||||
UserInfoResult userInfoResult = userQueryService.findByClientCodeAndAccount(clientCode, account);
|
||||
if (Objects.isNull(userInfoResult)) {
|
||||
throw new UserNotFoundException(account);
|
||||
}
|
||||
return userInfoResult;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
protected String obtainPassword(HttpServletRequest request) {
|
||||
String password = request.getParameter(this.PASSWORD_PARAMETER);
|
||||
return passwordDecryptService.decryptPassword(password);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
protected String obtainAccount(HttpServletRequest request) {
|
||||
return request.getParameter(this.ACCOUNT_PARAMETER);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
protected String obtainCaptchaKey(HttpServletRequest request) {
|
||||
return request.getParameter(this.CAPTCHA_KEY_PARAMETER);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
protected String obtainCaptchaCode(HttpServletRequest request) {
|
||||
return request.getParameter(this.CAPTCHA_CODE_PARAMETER);
|
||||
public String getClientCode() {
|
||||
return ClientEnum.MERCHANT.getCode();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,56 +0,0 @@
|
||||
package cn.daxpay.open.platform.capability.auth.authentication;
|
||||
|
||||
import cn.daxpay.open.platform.capability.auth.entity.AuthInfoResult;
|
||||
import cn.daxpay.open.platform.capability.auth.entity.LoginAuthContext;
|
||||
import cn.hutool.extra.spring.SpringUtil;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
/// # 抽象认证器
|
||||
///
|
||||
public interface AbstractAuthentication {
|
||||
|
||||
/// 获取终端编码
|
||||
String getLoginType();
|
||||
|
||||
/// 获取用户状态检查接口的实现类
|
||||
default List<UserInfoStatusCheck> getUserInfoStatusCheck() {
|
||||
return SpringUtil.getBeansOfType(UserInfoStatusCheck.class).values().stream().toList();
|
||||
}
|
||||
|
||||
/// 登录类型是否匹配
|
||||
default boolean adaptation(String loginType) {
|
||||
return Objects.equals(getLoginType(), loginType);
|
||||
}
|
||||
|
||||
/// 认证前操作
|
||||
default void authenticationBefore(LoginAuthContext context) {
|
||||
|
||||
}
|
||||
|
||||
/// 尝试认证, 必须重写
|
||||
@NotNull
|
||||
AuthInfoResult attemptAuthentication(LoginAuthContext context);
|
||||
|
||||
/// 认证后处理
|
||||
default void authenticationAfter(AuthInfoResult authInfoResult, LoginAuthContext context) {
|
||||
}
|
||||
|
||||
/// 认证流程
|
||||
default AuthInfoResult authentication(LoginAuthContext context) {
|
||||
this.authenticationBefore(context);
|
||||
// 认证逻辑
|
||||
AuthInfoResult authInfoResult = this.attemptAuthentication(context);
|
||||
// 添加用户信息到上下文中
|
||||
context.setUserDetail(authInfoResult.getUserDetail());
|
||||
// 检查用户信息和状态
|
||||
for (var userInfoStatusCheck : this.getUserInfoStatusCheck()) {
|
||||
userInfoStatusCheck.check(authInfoResult, context);
|
||||
}
|
||||
// 认证后处理
|
||||
this.authenticationAfter(authInfoResult, context);
|
||||
return authInfoResult;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package cn.daxpay.open.platform.capability.auth.authentication;
|
||||
|
||||
import cn.daxpay.open.platform.capability.auth.exception.LoginFailureException;
|
||||
|
||||
/// # 认证挑战异常基类
|
||||
///
|
||||
/// 认证通过后, 但需要额外挑战(如双因素认证)时抛出。
|
||||
/// 继承 [LoginFailureException] 以复用其 userId / account 字段, 但语义上属"非失败"流程,
|
||||
/// 不应触发登录失败计数与失败日志。
|
||||
///
|
||||
public class AuthenticationChallengeException extends LoginFailureException {
|
||||
|
||||
public AuthenticationChallengeException(Long userId, String account, String messageKey) {
|
||||
super(userId, account, messageKey);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package cn.daxpay.open.platform.capability.auth.authentication;
|
||||
|
||||
import cn.daxpay.open.platform.capability.auth.entity.AuthInfoResult;
|
||||
import cn.daxpay.open.platform.capability.auth.entity.LoginAuthContext;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/// # 认证流程编排模板
|
||||
///
|
||||
/// 将"认证前 -> 尝试认证 -> 用户状态校验 -> 认证后"的模板流程从认证器接口中抽离, 统一在此编排。
|
||||
/// 通过构造注入一次性解析所有 [UserInfoStatusCheck] (替代原 SpringUtil.getBeansOfType 的运行时反复查询)。
|
||||
///
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class AuthenticationTemplate {
|
||||
|
||||
private final List<UserInfoStatusCheck> userInfoStatusChecks;
|
||||
|
||||
/// 执行完整认证流程
|
||||
public AuthInfoResult authenticate(Authenticator authenticator, LoginAuthContext context) {
|
||||
// 认证前
|
||||
authenticator.authenticationBefore(context);
|
||||
// 尝试认证
|
||||
AuthInfoResult authInfoResult = authenticator.attemptAuthentication(context);
|
||||
// 添加用户信息到上下文中
|
||||
context.setUserDetail(authInfoResult.getUserDetail());
|
||||
// 检查用户信息和状态
|
||||
for (UserInfoStatusCheck check : userInfoStatusChecks) {
|
||||
check.check(authInfoResult, context);
|
||||
}
|
||||
// 认证后
|
||||
authenticator.authenticationAfter(authInfoResult, context);
|
||||
return authInfoResult;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package cn.daxpay.open.platform.capability.auth.authentication;
|
||||
|
||||
import cn.daxpay.open.platform.capability.auth.entity.AuthInfoResult;
|
||||
import cn.daxpay.open.platform.capability.auth.entity.LoginAuthContext;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
|
||||
/// # 认证器
|
||||
///
|
||||
/// 认证流程编排( before -> attempt -> 用户状态校验 -> after )由 [AuthenticationTemplate] 统一执行,
|
||||
/// 本接口仅声明终端、登录方式与各步骤钩子。每个"终端( clientCode ) + 登录方式( loginType )"组合
|
||||
/// 由一个具体认证器承载, [AuthenticationTemplate] 据双键路由到唯一实现。
|
||||
public interface Authenticator {
|
||||
|
||||
/// 终端编码(对应 [cn.daxpay.open.platform.core.enums.client.ClientEnum] 的 code)
|
||||
String getClientCode();
|
||||
|
||||
/// 获取登录类型
|
||||
String getLoginType();
|
||||
|
||||
/// 是否匹配终端与登录方式(双键路由)
|
||||
default boolean adaptation(String clientCode, String loginType) {
|
||||
return getClientCode().equals(clientCode) && getLoginType().equals(loginType);
|
||||
}
|
||||
|
||||
/// 认证前操作
|
||||
default void authenticationBefore(LoginAuthContext context) {
|
||||
}
|
||||
|
||||
/// 尝试认证, 必须重写
|
||||
@NotNull
|
||||
AuthInfoResult attemptAuthentication(LoginAuthContext context);
|
||||
|
||||
/// 认证后处理
|
||||
default void authenticationAfter(AuthInfoResult authInfoResult, LoginAuthContext context) {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package cn.daxpay.open.platform.capability.auth.authentication;
|
||||
|
||||
import cn.daxpay.open.platform.capability.auth.entity.AuthInfoResult;
|
||||
import cn.daxpay.open.platform.capability.auth.entity.LoginAuthContext;
|
||||
|
||||
/// # 认证后挑战 SPI
|
||||
///
|
||||
/// 认证(凭证校验 + 用户状态校验)通过后、建立会话前, 判断是否需要额外挑战(如双因素认证 / 设备验证 / 风控)。
|
||||
/// 需要挑战时由 [createChallenge] 颁发挑战凭证并返回挑战异常, 由全局处理器返回前端。
|
||||
/// 挑战不属于登录失败, 不触发失败计数与失败日志。
|
||||
///
|
||||
public interface PostAuthenticationChallenge {
|
||||
|
||||
/// 是否需要挑战
|
||||
/// @param context 认证上下文
|
||||
/// @param authInfoResult 认证结果
|
||||
boolean required(LoginAuthContext context, AuthInfoResult authInfoResult);
|
||||
|
||||
/// 颁发挑战凭证并返回挑战异常
|
||||
/// @return 挑战异常, 由调用方抛出
|
||||
AuthenticationChallengeException createChallenge(LoginAuthContext context, AuthInfoResult authInfoResult);
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
package cn.daxpay.open.platform.capability.auth.exception;
|
||||
|
||||
import cn.daxpay.open.platform.core.exception.BizException;
|
||||
|
||||
/// # 应用被停用
|
||||
///
|
||||
public class ApplicationNotEnableException extends BizException {
|
||||
|
||||
public ApplicationNotEnableException() {
|
||||
// 指定应用已被停用
|
||||
super("指定应用已被停用");
|
||||
initMessageKey("error.auth.auth.applicationNotEnable");
|
||||
}
|
||||
|
||||
|
||||
public ApplicationNotEnableException(int code, String messageKey, Object... args) {
|
||||
super(code, messageKey, args);
|
||||
}
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
package cn.daxpay.open.platform.capability.auth.exception;
|
||||
|
||||
import cn.daxpay.open.platform.core.exception.BizException;
|
||||
|
||||
/// # 终端方式被停用
|
||||
///
|
||||
public class ClientNotEnableException extends BizException {
|
||||
|
||||
public ClientNotEnableException() {
|
||||
// 指定终端方式已被停用
|
||||
super("指定终端方式已被停用");
|
||||
initMessageKey("error.auth.auth.clientNotEnable");
|
||||
}
|
||||
|
||||
|
||||
public ClientNotEnableException(int code, String messageKey, Object... args) {
|
||||
super(code, messageKey, args);
|
||||
}
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
package cn.daxpay.open.platform.capability.auth.exception;
|
||||
|
||||
import cn.daxpay.open.platform.core.exception.BizException;
|
||||
|
||||
/// # 终端不存在
|
||||
///
|
||||
public class ClientNotFoundException extends BizException {
|
||||
|
||||
public ClientNotFoundException() {
|
||||
// 未找到对应的终端
|
||||
super("未找到对应的终端");
|
||||
initMessageKey("error.auth.auth.clientNotFound");
|
||||
}
|
||||
|
||||
|
||||
public ClientNotFoundException(int code, String messageKey, Object... args) {
|
||||
super(code, messageKey, args);
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
package cn.daxpay.open.platform.capability.auth.exception;
|
||||
|
||||
import cn.daxpay.open.platform.core.code.CommonCode;
|
||||
import cn.daxpay.open.platform.core.exception.BizInfoException;
|
||||
import lombok.Getter;
|
||||
|
||||
@@ -31,6 +32,12 @@ public class LoginFailureException extends BizInfoException {
|
||||
this.userId = userId;
|
||||
}
|
||||
|
||||
/// 使用 i18n messageKey 与占位符参数(携带账号)
|
||||
public LoginFailureException(String account, String messageKey, Object... args) {
|
||||
super(CommonCode.FAIL_CODE, messageKey, args);
|
||||
this.account = account;
|
||||
this.userId = null;
|
||||
}
|
||||
|
||||
public LoginFailureException(int code, String messageKey, Object... args) {
|
||||
super(code, messageKey, args);
|
||||
|
||||
@@ -14,7 +14,7 @@ public class NotLoginException extends BizException {
|
||||
|
||||
public NotLoginException() {
|
||||
super(AUTHENTICATION_FAIL, "用户未登录");
|
||||
initMessageKey("error.auth.auth.notLogin");
|
||||
initMessageKey("error.auth.notLogin");
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ import cn.daxpay.open.platform.core.exception.BizInfoException;
|
||||
public class RouterCheckException extends BizInfoException {
|
||||
|
||||
public RouterCheckException() {
|
||||
super("没有对应请求路径的权限");
|
||||
super("error.auth.routerCheck");
|
||||
}
|
||||
public RouterCheckException(String message) {
|
||||
super(message);
|
||||
|
||||
@@ -2,6 +2,7 @@ package cn.daxpay.open.platform.capability.auth.handler;
|
||||
|
||||
import cn.daxpay.open.platform.common.spring.util.WebServletUtil;
|
||||
import cn.daxpay.open.platform.capability.auth.exception.RouterCheckException;
|
||||
import cn.daxpay.open.platform.capability.auth.service.AccessPolicy;
|
||||
import cn.daxpay.open.platform.capability.auth.service.RouterCheck;
|
||||
import cn.daxpay.open.platform.capability.auth.util.SecurityUtil;
|
||||
import cn.dev33.satoken.fun.SaFunction;
|
||||
@@ -18,16 +19,19 @@ import java.util.List;
|
||||
///
|
||||
/// 【执行语义 - 统一为"命中放行,未命中拒绝"】
|
||||
/// - 所有实现 RouterCheck SPI 的 Bean 在启动时按 sortNo 升序排列。
|
||||
/// - 请求进入时遍历 RouterCheck 列表,任意一个返回 true 即调用 SaRouter.stop() 放行。
|
||||
/// - 全部未命中:
|
||||
/// - 未登录 → SecurityUtil.getUserId() 抛 NotLoginException
|
||||
/// - 已登录 → 记录 WARN 日志并抛出 RouterCheckException(403 无权限)
|
||||
/// - 请求进入时:
|
||||
/// 1. 已登录用户先依次执行 [AccessPolicy] (如密码过期强制改密), 不通过即抛异常阻断;
|
||||
/// 2. 再遍历 RouterCheck 列表, 任意一个返回 true 即调用 SaRouter.stop() 放行。
|
||||
/// - RouterCheck 全部未命中:
|
||||
/// - 未登录 → SecurityUtil.getUserId() 抛 NotLoginException
|
||||
/// - 已登录 → 记录 WARN 日志并抛出 RouterCheckException(403 无权限)
|
||||
/// </li>
|
||||
///
|
||||
/// 【stop 条件语义】:SaRouter.stop() 会终止后续拦截器执行,但不终止整个过滤器链,
|
||||
/// 仅结束 Sa-Token 鉴权阶段的处理。
|
||||
///
|
||||
/// @see cn.daxpay.open.platform.capability.auth.service.RouterCheck
|
||||
/// @see cn.daxpay.open.platform.capability.auth.service.AccessPolicy
|
||||
@Slf4j
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
@@ -35,6 +39,8 @@ public class SaRouteHandler implements InitializingBean {
|
||||
|
||||
private final List<RouterCheck> routerChecks;
|
||||
|
||||
private final List<AccessPolicy> accessPolicies;
|
||||
|
||||
@Override
|
||||
public void afterPropertiesSet() {
|
||||
// 排序
|
||||
@@ -47,6 +53,12 @@ public class SaRouteHandler implements InitializingBean {
|
||||
public SaFunction check(Object handler) {
|
||||
return () -> {
|
||||
String path = WebServletUtil.getPath();
|
||||
// 已登录用户: 先执行访问策略(如密码过期强制改密), 不通过则抛异常阻断
|
||||
SecurityUtil.getCurrentUser().ifPresent(user -> {
|
||||
for (AccessPolicy policy : accessPolicies) {
|
||||
policy.check(WebServletUtil.getRequest(), user);
|
||||
}
|
||||
});
|
||||
// 遍历所有 RouterCheck,命中即放行(按 sortNo 顺序执行)
|
||||
for (RouterCheck routerCheck : routerChecks) {
|
||||
if (routerCheck.check(handler)) {
|
||||
@@ -67,5 +79,3 @@ public class SaRouteHandler implements InitializingBean {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
package cn.daxpay.open.platform.capability.auth.service;
|
||||
|
||||
import cn.daxpay.open.platform.core.entity.UserDetail;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
|
||||
/// # 已认证请求访问策略 SPI
|
||||
///
|
||||
/// 在路由鉴权通过、对已登录用户执行的访问策略, 允许抛异常阻断请求(如密码过期强制改密)。
|
||||
/// 与 [RouterCheck] 的区别: RouterCheck 仅做"是否放行"的布尔判断且不应抛异常;
|
||||
/// AccessPolicy 专用于"已认证但需限制访问"的场景, 允许抛业务异常。
|
||||
/// 实现由 [cn.daxpay.open.platform.capability.auth.handler.SaRouteHandler] 在鉴权链最前面对已登录用户依次调用。
|
||||
///
|
||||
public interface AccessPolicy {
|
||||
|
||||
/// 检查访问策略, 不通过则抛异常阻断请求
|
||||
/// @param request 当前请求
|
||||
/// @param userDetail 当前登录用户(非空)
|
||||
void check(HttpServletRequest request, UserDetail userDetail);
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
{
|
||||
"accountOrPasswordError": "Incorrect account or password",
|
||||
"notLogin": "User not logged in",
|
||||
"passwordExpired": "Password has expired, please change it first",
|
||||
"captchaError": "Captcha error",
|
||||
@@ -18,5 +19,6 @@
|
||||
"userDisabled": "User has been disabled",
|
||||
"twoFactorRequired": "Two-factor authentication required",
|
||||
"twoFactorCodeError": "Incorrect verification code or backup code",
|
||||
"twoFactorPreAuthExpired": "Pre-auth token is invalid or expired, please log in again"
|
||||
"twoFactorPreAuthExpired": "Pre-auth token is invalid or expired, please log in again",
|
||||
"loginRetryLock": "Too many failed login attempts, please try again in {0} minutes"
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
{
|
||||
"accountOrPasswordError": "账号或密码不正确",
|
||||
"notLogin": "用户未登录",
|
||||
"passwordExpired": "密码已过期,请先修改密码",
|
||||
"captchaError": "验证码错误",
|
||||
@@ -18,5 +19,6 @@
|
||||
"userDisabled": "该用户已被禁用",
|
||||
"twoFactorRequired": "需要双因素认证",
|
||||
"twoFactorCodeError": "动态码或备用码错误",
|
||||
"twoFactorPreAuthExpired": "预认证令牌无效或已过期,请重新登录"
|
||||
"twoFactorPreAuthExpired": "预认证令牌无效或已过期,请重新登录",
|
||||
"loginRetryLock": "登录失败次数过多,请{0}分钟后重试"
|
||||
}
|
||||
|
||||
@@ -0,0 +1,135 @@
|
||||
package cn.daxpay.open.platform.iam.auth.login;
|
||||
|
||||
import cn.daxpay.open.platform.capability.auth.authentication.Authenticator;
|
||||
import cn.daxpay.open.platform.capability.auth.code.AuthLoginTypeCode;
|
||||
import cn.daxpay.open.platform.capability.auth.entity.AuthInfoResult;
|
||||
import cn.daxpay.open.platform.capability.auth.entity.LoginAuthContext;
|
||||
import cn.daxpay.open.platform.capability.auth.exception.LoginFailureException;
|
||||
import cn.daxpay.open.platform.core.entity.UserDetail;
|
||||
import cn.daxpay.open.platform.iam.auth.service.CaptchaService;
|
||||
import cn.daxpay.open.platform.iam.auth.service.IamSecurityConfigService;
|
||||
import cn.daxpay.open.platform.iam.auth.service.LoginRetryService;
|
||||
import cn.daxpay.open.platform.iam.auth.service.PasswordDecryptService;
|
||||
import cn.daxpay.open.platform.iam.exception.auth.UserNotFoundException;
|
||||
import cn.daxpay.open.platform.iam.result.user.UserInfoResult;
|
||||
import cn.daxpay.open.platform.iam.service.user.UserQueryService;
|
||||
import cn.daxpay.open.platform.system.entity.config.platform.security.PlatformLoginSecurityConfig;
|
||||
import cn.hutool.crypto.digest.BCrypt;
|
||||
import jakarta.annotation.Nullable;
|
||||
import jakarta.annotation.Resource;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
/// # 账号密码登录基类
|
||||
///
|
||||
/// 承载密码登录的通用流程(用户定位、验证码校验、密码比对、登录重试状态注入)。
|
||||
/// 子类只需声明终端编码 [getClientCode], 由认证框架按"终端 + 登录方式"双键路由,
|
||||
/// 从而消除多终端同登录方式(如平台端/商户端均为 password)的 findFirst 歧义与重复代码。
|
||||
///
|
||||
@Slf4j
|
||||
@RequiredArgsConstructor
|
||||
public abstract class AbstractPasswordLoginHandler implements Authenticator {
|
||||
|
||||
protected static final String ACCOUNT_PARAMETER = "account";
|
||||
|
||||
protected static final String PASSWORD_PARAMETER = "password";
|
||||
|
||||
protected static final String CAPTCHA_KEY_PARAMETER = "captchaKey";
|
||||
|
||||
protected static final String CAPTCHA_CODE_PARAMETER = "captchaCode";
|
||||
|
||||
@Resource
|
||||
protected UserQueryService userQueryService;
|
||||
|
||||
protected final LoginRetryService loginRetryService;
|
||||
|
||||
protected final CaptchaService captchaService;
|
||||
|
||||
protected final IamSecurityConfigService iamSecurityConfigService;
|
||||
|
||||
protected final PasswordDecryptService passwordDecryptService;
|
||||
|
||||
/// 登录方式固定为账号密码
|
||||
@Override
|
||||
public final String getLoginType() {
|
||||
return AuthLoginTypeCode.PASSWORD;
|
||||
}
|
||||
|
||||
/// 认证
|
||||
@Override
|
||||
public @NotNull AuthInfoResult attemptAuthentication(LoginAuthContext context) {
|
||||
HttpServletRequest request = context.getRequest();
|
||||
String account = this.obtainAccount(request);
|
||||
String password = this.obtainPassword(request);
|
||||
String captchaKey = this.obtainCaptchaKey(request);
|
||||
String captchaCode = this.obtainCaptchaCode(request);
|
||||
// 从请求上下文获取终端编码,按终端+账号查询用户
|
||||
String clientCode = context.getClientCode();
|
||||
|
||||
UserInfoResult userInfoResult = this.loadUserByClientCodeAndAccount(clientCode, account);
|
||||
UserDetail userDetail = userInfoResult.toUserDetail();
|
||||
|
||||
// 检查验证码(如果需要)
|
||||
this.checkCaptcha(userDetail.getId(), captchaKey, captchaCode);
|
||||
loginRetryService.checkBeforeLogin(userDetail);
|
||||
|
||||
// 比对密码未通过
|
||||
if (!BCrypt.checkpw(password, userInfoResult.getPassword())) {
|
||||
// 必须携带 userId, 否则 LoginRetryService.onLoginFailure 因 userId 为空直接跳过, 失败计数始终为 0
|
||||
throw new LoginFailureException(userDetail.getId(), userDetail.getAccount(), "error.auth.accountOrPasswordError");
|
||||
}
|
||||
|
||||
// 设置密码状态到 UserDetail(超级管理员不设置密码状态限制)
|
||||
if (!userDetail.isAdmin()) {
|
||||
loginRetryService.setPasswordStatusToUserDetail(userDetail);
|
||||
}
|
||||
|
||||
return new AuthInfoResult().setId(userDetail.getId()).setUserDetail(userDetail);
|
||||
}
|
||||
|
||||
/// 检查验证码
|
||||
protected void checkCaptcha(Long userId, String captchaKey, String captchaCode) {
|
||||
PlatformLoginSecurityConfig config = iamSecurityConfigService.getLoginSecurity();
|
||||
if (!Boolean.TRUE.equals(config.getCaptchaEnabled())) {
|
||||
return;
|
||||
}
|
||||
int triggerAttempts = config.getCaptchaTriggerAttempts() == null ? 3 : config.getCaptchaTriggerAttempts();
|
||||
int errorCount = loginRetryService.getErrorCount(userId);
|
||||
captchaService.checkOrValidateCaptcha(errorCount, triggerAttempts, captchaKey, captchaCode);
|
||||
}
|
||||
|
||||
/// 根据终端编码+账号加载用户(终端维度用户定位)
|
||||
protected UserInfoResult loadUserByClientCodeAndAccount(String clientCode, String account) {
|
||||
// 按终端+账号查询用户,跨终端同名账号不会命中
|
||||
UserInfoResult userInfoResult = userQueryService.findByClientCodeAndAccount(clientCode, account);
|
||||
if (Objects.isNull(userInfoResult)) {
|
||||
throw new UserNotFoundException(account);
|
||||
}
|
||||
return userInfoResult;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
protected String obtainPassword(HttpServletRequest request) {
|
||||
String password = request.getParameter(PASSWORD_PARAMETER);
|
||||
return passwordDecryptService.decryptPassword(password);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
protected String obtainAccount(HttpServletRequest request) {
|
||||
return request.getParameter(ACCOUNT_PARAMETER);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
protected String obtainCaptchaKey(HttpServletRequest request) {
|
||||
return request.getParameter(CAPTCHA_KEY_PARAMETER);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
protected String obtainCaptchaCode(HttpServletRequest request) {
|
||||
return request.getParameter(CAPTCHA_CODE_PARAMETER);
|
||||
}
|
||||
}
|
||||
@@ -1,140 +1,28 @@
|
||||
package cn.daxpay.open.platform.iam.auth.login;
|
||||
|
||||
import cn.daxpay.open.platform.core.entity.UserDetail;
|
||||
import cn.daxpay.open.platform.core.enums.client.ClientEnum;
|
||||
import cn.daxpay.open.platform.iam.auth.service.CaptchaService;
|
||||
import cn.daxpay.open.platform.iam.auth.service.IamSecurityConfigService;
|
||||
import cn.daxpay.open.platform.iam.auth.service.LoginRetryService;
|
||||
import cn.daxpay.open.platform.iam.auth.service.PasswordDecryptService;
|
||||
import cn.daxpay.open.platform.iam.result.user.UserInfoResult;
|
||||
import cn.daxpay.open.platform.iam.service.user.UserQueryService;
|
||||
import cn.daxpay.open.platform.capability.auth.authentication.AbstractAuthentication;
|
||||
import cn.daxpay.open.platform.capability.auth.code.AuthLoginTypeCode;
|
||||
import cn.daxpay.open.platform.capability.auth.entity.AuthInfoResult;
|
||||
import cn.daxpay.open.platform.capability.auth.entity.LoginAuthContext;
|
||||
import cn.daxpay.open.platform.capability.auth.exception.LoginFailureException;
|
||||
import cn.daxpay.open.platform.capability.auth.exception.UserNotFoundException;
|
||||
import cn.daxpay.open.platform.system.entity.config.platform.security.PlatformLoginSecurityConfig;
|
||||
import cn.hutool.crypto.digest.BCrypt;
|
||||
import jakarta.annotation.Resource;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import lombok.Getter;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import jakarta.annotation.Nullable;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
/// # 账号密码登陆方式实现
|
||||
/// # 平台运营端账号密码登录
|
||||
///
|
||||
@Slf4j
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
@SuppressWarnings("FieldCanBeLocal")
|
||||
public class PasswordLoginHandler implements AbstractAuthentication {
|
||||
public class PasswordLoginHandler extends AbstractPasswordLoginHandler {
|
||||
|
||||
@Getter
|
||||
private final String ACCOUNT_PARAMETER = "account";
|
||||
public PasswordLoginHandler(LoginRetryService loginRetryService, CaptchaService captchaService,
|
||||
IamSecurityConfigService iamSecurityConfigService,
|
||||
PasswordDecryptService passwordDecryptService) {
|
||||
super(loginRetryService, captchaService, iamSecurityConfigService, passwordDecryptService);
|
||||
}
|
||||
|
||||
@Getter
|
||||
private final String PASSWORD_PARAMETER = "password";
|
||||
|
||||
@Getter
|
||||
private final String CAPTCHA_KEY_PARAMETER = "captchaKey";
|
||||
|
||||
@Getter
|
||||
private final String CAPTCHA_CODE_PARAMETER = "captchaCode";
|
||||
|
||||
@Resource
|
||||
@Getter
|
||||
private UserQueryService userQueryService;
|
||||
|
||||
private final LoginRetryService loginRetryService;
|
||||
|
||||
private final CaptchaService captchaService;
|
||||
|
||||
private final IamSecurityConfigService iamSecurityConfigService;
|
||||
|
||||
private final PasswordDecryptService passwordDecryptService;
|
||||
|
||||
/// 获取终端编码
|
||||
/// 平台运营端终端编码
|
||||
@Override
|
||||
public String getLoginType() {
|
||||
return AuthLoginTypeCode.PASSWORD;
|
||||
}
|
||||
|
||||
/// 认证
|
||||
@Override
|
||||
public @NotNull AuthInfoResult attemptAuthentication(LoginAuthContext context) {
|
||||
String account = this.obtainAccount(context.getRequest());
|
||||
String password = this.obtainPassword(context.getRequest());
|
||||
String captchaKey = this.obtainCaptchaKey(context.getRequest());
|
||||
String captchaCode = this.obtainCaptchaCode(context.getRequest());
|
||||
// 从请求上下文获取终端编码,按终端+账号查询用户
|
||||
String clientCode = context.getClientCode();
|
||||
|
||||
UserInfoResult userInfoResult = this.loadUserByClientCodeAndAccount(clientCode, account);
|
||||
UserDetail userDetail = userInfoResult.toUserDetail();
|
||||
|
||||
// 检查验证码(如果需要)
|
||||
this.checkCaptcha(userDetail.getId(), captchaKey, captchaCode);
|
||||
loginRetryService.checkBeforeLogin(userDetail);
|
||||
|
||||
// 比对密码未通过
|
||||
if (!BCrypt.checkpw(password, userInfoResult.getPassword())) {
|
||||
// 非系统管理员进行错误处理, 包括记录错误次数和账号锁定等操作
|
||||
// 必须携带 userId, 否则 LoginRetryService.onLoginFailure 因 userId 为空直接跳过, 失败计数始终为 0
|
||||
throw new LoginFailureException(userDetail.getId(), userDetail.getAccount(), "账号或密码不正确");
|
||||
}
|
||||
|
||||
// 设置密码状态到 UserDetail(超级管理员不设置密码状态限制)
|
||||
if (!userDetail.isAdmin()) {
|
||||
loginRetryService.setPasswordStatusToUserDetail(userDetail);
|
||||
}
|
||||
|
||||
return new AuthInfoResult().setId(userDetail.getId()).setUserDetail(userDetail);
|
||||
}
|
||||
|
||||
/// 检查验证码
|
||||
private void checkCaptcha(Long userId, String captchaKey, String captchaCode) {
|
||||
PlatformLoginSecurityConfig config = iamSecurityConfigService.getLoginSecurity();
|
||||
if (!Boolean.TRUE.equals(config.getCaptchaEnabled())) {
|
||||
return;
|
||||
}
|
||||
int triggerAttempts = config.getCaptchaTriggerAttempts() == null ? 3 : config.getCaptchaTriggerAttempts();
|
||||
int errorCount = loginRetryService.getErrorCount(userId);
|
||||
captchaService.checkOrValidateCaptcha(errorCount, triggerAttempts, captchaKey, captchaCode);
|
||||
}
|
||||
|
||||
/// 根据终端编码+账号加载用户(终端维度用户定位)
|
||||
public UserInfoResult loadUserByClientCodeAndAccount(String clientCode, String account) throws UserNotFoundException {
|
||||
// 按终端+账号查询用户,跨终端同名账号不会命中
|
||||
UserInfoResult userInfoResult = userQueryService.findByClientCodeAndAccount(clientCode, account);
|
||||
if (Objects.isNull(userInfoResult)) {
|
||||
throw new UserNotFoundException(account);
|
||||
}
|
||||
return userInfoResult;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
protected String obtainPassword(HttpServletRequest request) {
|
||||
String password = request.getParameter(this.PASSWORD_PARAMETER);
|
||||
return passwordDecryptService.decryptPassword(password);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
protected String obtainAccount(HttpServletRequest request) {
|
||||
return request.getParameter(this.ACCOUNT_PARAMETER);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
protected String obtainCaptchaKey(HttpServletRequest request) {
|
||||
return request.getParameter(this.CAPTCHA_KEY_PARAMETER);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
protected String obtainCaptchaCode(HttpServletRequest request) {
|
||||
return request.getParameter(this.CAPTCHA_CODE_PARAMETER);
|
||||
public String getClientCode() {
|
||||
return ClientEnum.ADMIN.getCode();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package cn.daxpay.open.platform.iam.auth.service;
|
||||
|
||||
import cn.daxpay.open.platform.capability.auth.exception.CaptchaErrorException;
|
||||
import cn.daxpay.open.platform.capability.auth.exception.CaptchaRequiredException;
|
||||
import cn.daxpay.open.platform.iam.exception.auth.CaptchaErrorException;
|
||||
import cn.daxpay.open.platform.iam.exception.auth.CaptchaRequiredException;
|
||||
import cn.daxpay.open.platform.iam.result.captcha.CaptchaDataResult;
|
||||
import cn.hutool.captcha.CaptchaUtil;
|
||||
import cn.hutool.captcha.LineCaptcha;
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
package cn.daxpay.open.platform.iam.auth.service;
|
||||
|
||||
import cn.daxpay.open.platform.capability.auth.authentication.Authenticator;
|
||||
import cn.daxpay.open.platform.core.enums.client.ClientEnum;
|
||||
import cn.daxpay.open.platform.iam.exception.auth.ApplicationNotFoundException;
|
||||
import cn.daxpay.open.platform.iam.param.auth.LoginContentParam;
|
||||
import cn.daxpay.open.platform.iam.result.auth.LoginContentResult;
|
||||
import cn.daxpay.open.platform.capability.auth.authentication.AbstractAuthentication;
|
||||
import cn.daxpay.open.platform.capability.auth.exception.ApplicationNotFoundException;
|
||||
import cn.daxpay.open.platform.system.entity.config.platform.security.PlatformLoginSecurityConfig;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Service;
|
||||
@@ -17,7 +17,7 @@ import java.util.List;
|
||||
@RequiredArgsConstructor
|
||||
public class LoginContentService {
|
||||
|
||||
private final List<AbstractAuthentication> abstractAuthentications;
|
||||
private final List<Authenticator> authenticators;
|
||||
|
||||
private final IamSecurityConfigService iamSecurityConfigService;
|
||||
|
||||
@@ -29,8 +29,8 @@ public class LoginContentService {
|
||||
}
|
||||
PlatformLoginSecurityConfig loginSecurity = iamSecurityConfigService.getLoginSecurity();
|
||||
return new LoginContentResult()
|
||||
.setLoginTypes(abstractAuthentications.stream()
|
||||
.map(AbstractAuthentication::getLoginType)
|
||||
.setLoginTypes(authenticators.stream()
|
||||
.map(Authenticator::getLoginType)
|
||||
.distinct()
|
||||
.toList())
|
||||
// 是否启用验证码触发(登录失败达阈值后要求输入验证码)
|
||||
@@ -39,4 +39,3 @@ public class LoginContentService {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -32,7 +32,7 @@ public class LoginRetryService {
|
||||
/// 登录前检查
|
||||
public void checkBeforeLogin(UserDetail userDetail) {
|
||||
if (!UserStatusEnum.NORMAL.getCode().equals(userDetail.getStatus())) {
|
||||
throw new LoginFailureException(userDetail.getAccount(), "用户状态异常");
|
||||
throw new LoginFailureException(userDetail.getAccount(), "error.auth.userStatusError");
|
||||
}
|
||||
|
||||
LoginRetryPolicyConfig config = this.getPolicyConfig();
|
||||
@@ -62,7 +62,7 @@ public class LoginRetryService {
|
||||
}
|
||||
|
||||
long remainingMinutes = Math.max(1, java.time.Duration.between(now, lockTime).toMinutes() + 1);
|
||||
throw new LoginFailureException(userDetail.getAccount(), "登录失败次数过多,请" + remainingMinutes + "分钟后重试");
|
||||
throw new LoginFailureException(userDetail.getAccount(), "error.auth.loginRetryLock", remainingMinutes);
|
||||
}
|
||||
|
||||
/// 检查并重置失败计数
|
||||
|
||||
@@ -8,7 +8,7 @@ import org.springframework.stereotype.Service;
|
||||
/// # 二次校验信息服务
|
||||
///
|
||||
/// 供登录页预知当前是否启用了双因素认证(平台级开关), 前端据此决定是否展示相关提示。
|
||||
/// 具体某用户是否需要二次验证, 在密码校验通过后由 [cn.daxpay.open.platform.iam.endpoint.TokenService] 判定。
|
||||
/// 具体某用户是否需要二次验证, 在密码校验通过后由 [TokenService] 判定。
|
||||
///
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
|
||||
@@ -1,47 +0,0 @@
|
||||
package cn.daxpay.open.platform.iam.auth.service;
|
||||
|
||||
import cn.daxpay.open.platform.system.entity.config.platform.security.PlatformLoginSecurityConfig;
|
||||
import cn.daxpay.open.platform.system.entity.config.platform.security.PlatformPasswordPolicyConfig;
|
||||
import cn.daxpay.open.platform.system.entity.config.platform.security.PlatformSessionManagementConfig;
|
||||
import cn.daxpay.open.platform.system.enums.PlatformConfigTypeEnum;
|
||||
import cn.daxpay.open.platform.system.service.config.SystemPlatformConfigService;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
/// # 安全策略配置服务
|
||||
///
|
||||
/// 读取平台安全配置
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class SecurityConfigService {
|
||||
|
||||
private final SystemPlatformConfigService systemPlatformConfigService;
|
||||
|
||||
/// 获取平台密码策略配置
|
||||
public PlatformPasswordPolicyConfig getPlatformPasswordPolicy() {
|
||||
return systemPlatformConfigService.getOrCreateConfig(
|
||||
PlatformConfigTypeEnum.SECURITY_PASSWORD_POLICY,
|
||||
PlatformPasswordPolicyConfig.class,
|
||||
new PlatformPasswordPolicyConfig());
|
||||
}
|
||||
|
||||
/// 获取平台登录安全配置
|
||||
public PlatformLoginSecurityConfig getPlatformLoginSecurity() {
|
||||
return systemPlatformConfigService.getOrCreateConfig(
|
||||
PlatformConfigTypeEnum.SECURITY_LOGIN,
|
||||
PlatformLoginSecurityConfig.class,
|
||||
new PlatformLoginSecurityConfig());
|
||||
}
|
||||
|
||||
/// 获取平台会话管理配置
|
||||
public PlatformSessionManagementConfig getPlatformSessionManagement() {
|
||||
return systemPlatformConfigService.getOrCreateConfig(
|
||||
PlatformConfigTypeEnum.SECURITY_SESSION,
|
||||
PlatformSessionManagementConfig.class,
|
||||
new PlatformSessionManagementConfig());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,19 +1,21 @@
|
||||
package cn.daxpay.open.platform.iam.endpoint;
|
||||
package cn.daxpay.open.platform.iam.auth.service;
|
||||
|
||||
import cn.daxpay.open.platform.core.code.CommonCode;
|
||||
import cn.daxpay.open.platform.core.entity.UserDetail;
|
||||
import cn.daxpay.open.platform.core.enums.client.ClientEnum;
|
||||
import cn.daxpay.open.platform.capability.auth.authentication.AbstractAuthentication;
|
||||
import cn.daxpay.open.platform.common.config.properties.PlatformStarterProperties;
|
||||
import cn.daxpay.open.platform.capability.auth.entity.AuthInfoResult;
|
||||
import cn.daxpay.open.platform.capability.auth.entity.LoginAuthContext;
|
||||
import cn.daxpay.open.platform.capability.auth.exception.ApplicationNotFoundException;
|
||||
import cn.daxpay.open.platform.capability.auth.exception.LoginFailureException;
|
||||
import cn.daxpay.open.platform.capability.auth.exception.TwoFactorRequiredException;
|
||||
import cn.daxpay.open.platform.capability.auth.authentication.AuthenticationChallengeException;
|
||||
import cn.daxpay.open.platform.capability.auth.authentication.AuthenticationTemplate;
|
||||
import cn.daxpay.open.platform.capability.auth.authentication.Authenticator;
|
||||
import cn.daxpay.open.platform.capability.auth.authentication.PostAuthenticationChallenge;
|
||||
import cn.daxpay.open.platform.capability.auth.handler.LoginFailureHandler;
|
||||
import cn.daxpay.open.platform.capability.auth.handler.LoginSuccessHandler;
|
||||
import cn.daxpay.open.platform.capability.auth.util.SecurityUtil;
|
||||
import cn.daxpay.open.platform.common.config.properties.PlatformStarterProperties;
|
||||
import cn.daxpay.open.platform.core.code.CommonCode;
|
||||
import cn.daxpay.open.platform.core.entity.UserDetail;
|
||||
import cn.daxpay.open.platform.core.enums.client.ClientEnum;
|
||||
import cn.daxpay.open.platform.capability.auth.entity.AuthInfoResult;
|
||||
import cn.daxpay.open.platform.capability.auth.entity.LoginAuthContext;
|
||||
import cn.daxpay.open.platform.capability.auth.exception.LoginFailureException;
|
||||
import cn.daxpay.open.platform.iam.auth.service.twofactor.TwoFactorPreAuthService;
|
||||
import cn.daxpay.open.platform.iam.exception.auth.ApplicationNotFoundException;
|
||||
import cn.daxpay.open.platform.iam.result.user.UserInfoResult;
|
||||
import cn.daxpay.open.platform.iam.service.twofactor.UserTwoFactorService;
|
||||
import cn.daxpay.open.platform.iam.service.user.UserQueryService;
|
||||
@@ -38,7 +40,11 @@ public class TokenService {
|
||||
|
||||
private final PlatformStarterProperties platformStarterProperties;
|
||||
|
||||
private final List<AbstractAuthentication> abstractAuthentications;
|
||||
private final List<Authenticator> authenticators;
|
||||
|
||||
private final AuthenticationTemplate authenticationTemplate;
|
||||
|
||||
private final List<PostAuthenticationChallenge> postAuthenticationChallenges;
|
||||
|
||||
private final List<LoginSuccessHandler> loginSuccessHandlers;
|
||||
|
||||
@@ -61,22 +67,17 @@ public class TokenService {
|
||||
.setAuthProperties(platformStarterProperties.getAuth())
|
||||
.setAuthLoginType(loginType)
|
||||
.setClientCode(clientCode);
|
||||
// 校验登录终端
|
||||
this.validateClientCode(loginAuthContext);
|
||||
// 校验该终端是否支持此种登录方式(按 clientCode + loginType 双键匹配)
|
||||
this.validateClient(loginAuthContext);
|
||||
// 认证并获取结果
|
||||
authInfoResult = this.authentication(loginAuthContext);
|
||||
// 双因素认证: 平台已开启且用户已绑定则颁发预认证令牌, 抛出挑战异常(不计入登录失败)
|
||||
Long userId = toLong(authInfoResult.getId());
|
||||
if (userId != null && userTwoFactorService.isTwoFactorRequired(userId)) {
|
||||
String preAuthToken = twoFactorPreAuthService.create(userId, clientCode, loginType);
|
||||
String account = authInfoResult.getUserDetail() == null ? null : authInfoResult.getUserDetail().getAccount();
|
||||
throw new TwoFactorRequiredException(userId, account, preAuthToken);
|
||||
}
|
||||
// 认证后挑战(双因素/设备验证等), 任一需要则抛挑战异常(不计入登录失败)
|
||||
this.applyChallenges(loginAuthContext, authInfoResult);
|
||||
// 登录处理
|
||||
this.doSaLogin(authInfoResult, clientCode, loginType);
|
||||
}
|
||||
catch (TwoFactorRequiredException e) {
|
||||
// 双因素认证挑战: 不触发失败回调, 交由全局处理器返回预认证令牌
|
||||
catch (AuthenticationChallengeException e) {
|
||||
// 挑战流程: 不触发失败回调, 交由全局处理器返回挑战结果
|
||||
throw e;
|
||||
}
|
||||
catch (LoginFailureException e) {
|
||||
@@ -124,6 +125,15 @@ public class TokenService {
|
||||
return StpUtil.getTokenValue();
|
||||
}
|
||||
|
||||
/// 认证后挑战: 任一挑战需要则抛出挑战异常(不计入登录失败)
|
||||
private void applyChallenges(LoginAuthContext context, AuthInfoResult authInfoResult) {
|
||||
for (PostAuthenticationChallenge challenge : postAuthenticationChallenges) {
|
||||
if (challenge.required(context, authInfoResult)) {
|
||||
throw challenge.createChallenge(context, authInfoResult);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 成功处理
|
||||
private void loginSuccessHandler(HttpServletRequest request, HttpServletResponse response,
|
||||
AuthInfoResult authInfoResult) {
|
||||
@@ -150,7 +160,7 @@ public class TokenService {
|
||||
}
|
||||
}
|
||||
|
||||
/// 获取终端编码
|
||||
/// 获取并校验终端编码
|
||||
private String getClientCode(HttpServletRequest request) {
|
||||
String clientCode = SecurityUtil.getClient(request);
|
||||
ClientEnum.findByCode(clientCode)
|
||||
@@ -158,24 +168,26 @@ public class TokenService {
|
||||
return clientCode;
|
||||
}
|
||||
|
||||
/// 校验该终端是否支持此种登录方式
|
||||
private void validateClientCode(LoginAuthContext loginAuthContext) {
|
||||
/// 校验该终端是否支持此种登录方式(双键匹配)
|
||||
private void validateClient(LoginAuthContext loginAuthContext) {
|
||||
String clientCode = loginAuthContext.getClientCode();
|
||||
String loginType = loginAuthContext.getAuthLoginType();
|
||||
boolean supported = abstractAuthentications.stream()
|
||||
.anyMatch(authentication -> authentication.adaptation(loginType));
|
||||
boolean supported = authenticators.stream()
|
||||
.anyMatch(auth -> auth.adaptation(clientCode, loginType));
|
||||
if (!supported) {
|
||||
// 认证: 当前终端不支持该登录方式
|
||||
throw new LoginFailureException("error.auth.loginMethodNotSupported");
|
||||
}
|
||||
}
|
||||
|
||||
/// 认证
|
||||
/// 认证(双键路由到唯一认证器, 由模板执行流程)
|
||||
private @NotNull AuthInfoResult authentication(LoginAuthContext context) {
|
||||
String clientCode = context.getClientCode();
|
||||
String loginType = context.getAuthLoginType();
|
||||
return abstractAuthentications.stream()
|
||||
.filter(o -> o.adaptation(loginType))
|
||||
return authenticators.stream()
|
||||
.filter(auth -> auth.adaptation(clientCode, loginType))
|
||||
.findFirst()
|
||||
.map(o -> o.authentication(context))
|
||||
.map(auth -> authenticationTemplate.authenticate(auth, context))
|
||||
// 认证: 未找到对应的登录认证器
|
||||
.orElseThrow(() -> new LoginFailureException("error.auth.loginAuthenticatorNotFound"));
|
||||
}
|
||||
@@ -194,32 +206,9 @@ public class TokenService {
|
||||
session.set(CommonCode.USER, userDetail);
|
||||
}
|
||||
|
||||
/// 认证结果 id(Object) 转 Long, 无法转换返回 null
|
||||
private Long toLong(Object id) {
|
||||
if (id == null) {
|
||||
return null;
|
||||
}
|
||||
if (id instanceof Long l) {
|
||||
return l;
|
||||
}
|
||||
if (id instanceof Number n) {
|
||||
return n.longValue();
|
||||
}
|
||||
try {
|
||||
return Long.valueOf(id.toString());
|
||||
}
|
||||
catch (NumberFormatException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// 退出
|
||||
public void logout() {
|
||||
StpUtil.logout();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
package cn.daxpay.open.platform.iam.auth.service.twofactor;
|
||||
|
||||
import cn.daxpay.open.platform.capability.auth.authentication.AuthenticationChallengeException;
|
||||
import cn.daxpay.open.platform.capability.auth.authentication.PostAuthenticationChallenge;
|
||||
import cn.daxpay.open.platform.capability.auth.entity.AuthInfoResult;
|
||||
import cn.daxpay.open.platform.capability.auth.entity.LoginAuthContext;
|
||||
import cn.daxpay.open.platform.iam.exception.auth.TwoFactorRequiredException;
|
||||
import cn.daxpay.open.platform.iam.service.twofactor.UserTwoFactorService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/// # 双因素认证挑战
|
||||
///
|
||||
/// 认证通过后, 若用户已启用 TOTP 双因素认证, 颁发一次性预认证令牌并返回挑战异常。
|
||||
/// 令牌由 [TwoFactorPreAuthService] 存入 Redis, 5 分钟过期, 二次验证通过后单次消费。
|
||||
///
|
||||
@Slf4j
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class TwoFactorAuthenticationChallenge implements PostAuthenticationChallenge {
|
||||
|
||||
private final UserTwoFactorService userTwoFactorService;
|
||||
|
||||
private final TwoFactorPreAuthService twoFactorPreAuthService;
|
||||
|
||||
@Override
|
||||
public boolean required(LoginAuthContext context, AuthInfoResult authInfoResult) {
|
||||
Long userId = toLong(authInfoResult.getId());
|
||||
// 平台已开启且用户已绑定 2FA 时需要挑战
|
||||
return userId != null && userTwoFactorService.isTwoFactorRequired(userId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public AuthenticationChallengeException createChallenge(LoginAuthContext context, AuthInfoResult authInfoResult) {
|
||||
Long userId = toLong(authInfoResult.getId());
|
||||
String account = authInfoResult.getUserDetail() == null ? null : authInfoResult.getUserDetail().getAccount();
|
||||
String preAuthToken = twoFactorPreAuthService.create(userId, context.getClientCode(), context.getAuthLoginType());
|
||||
return new TwoFactorRequiredException(userId, account, preAuthToken);
|
||||
}
|
||||
|
||||
/// 认证结果 id(Object) 转 Long, 无法转换返回 null
|
||||
private Long toLong(Object id) {
|
||||
if (id == null) {
|
||||
return null;
|
||||
}
|
||||
if (id instanceof Long l) {
|
||||
return l;
|
||||
}
|
||||
if (id instanceof Number n) {
|
||||
return n.longValue();
|
||||
}
|
||||
try {
|
||||
return Long.valueOf(id.toString());
|
||||
}
|
||||
catch (NumberFormatException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -11,6 +11,7 @@ import cn.daxpay.open.platform.iam.param.auth.LoginContentParam;
|
||||
import cn.daxpay.open.platform.iam.param.auth.SecondVerifyParam;
|
||||
import cn.daxpay.open.platform.iam.result.auth.LoginContentResult;
|
||||
import cn.daxpay.open.platform.iam.result.auth.SecondCheckResult;
|
||||
import cn.daxpay.open.platform.iam.auth.service.TokenService;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package cn.daxpay.open.platform.capability.auth.exception;
|
||||
package cn.daxpay.open.platform.iam.exception.auth;
|
||||
|
||||
import cn.daxpay.open.platform.core.exception.BizInfoException;
|
||||
|
||||
@@ -7,7 +7,7 @@ import cn.daxpay.open.platform.core.exception.BizInfoException;
|
||||
public class ApplicationNotFoundException extends BizInfoException {
|
||||
|
||||
public ApplicationNotFoundException() {
|
||||
super("未找到对应的应用");
|
||||
super("error.auth.applicationNotFound");
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package cn.daxpay.open.platform.capability.auth.exception;
|
||||
package cn.daxpay.open.platform.iam.exception.auth;
|
||||
|
||||
import cn.daxpay.open.platform.core.exception.BizInfoException;
|
||||
|
||||
@@ -7,8 +7,7 @@ import cn.daxpay.open.platform.core.exception.BizInfoException;
|
||||
public class CaptchaErrorException extends BizInfoException {
|
||||
|
||||
public CaptchaErrorException() {
|
||||
super(40002, "验证码错误");
|
||||
initMessageKey("error.auth.auth.captchaError");
|
||||
super(40002, "error.auth.captchaError");
|
||||
}
|
||||
|
||||
public CaptchaErrorException(int code, String messageKey, Object... args) {
|
||||
@@ -1,4 +1,4 @@
|
||||
package cn.daxpay.open.platform.capability.auth.exception;
|
||||
package cn.daxpay.open.platform.iam.exception.auth;
|
||||
|
||||
import cn.daxpay.open.platform.core.exception.BizInfoException;
|
||||
|
||||
@@ -9,7 +9,7 @@ public class CaptchaRequiredException extends BizInfoException {
|
||||
private final String captchaKey;
|
||||
|
||||
public CaptchaRequiredException(String captchaKey) {
|
||||
super(40001, "error.auth.auth.captchaRequired");
|
||||
super(40001, "error.auth.captchaRequired");
|
||||
this.captchaKey = captchaKey;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package cn.daxpay.open.platform.capability.auth.exception;
|
||||
package cn.daxpay.open.platform.iam.exception.auth;
|
||||
|
||||
import cn.daxpay.open.platform.core.exception.BizInfoException;
|
||||
|
||||
@@ -7,12 +7,7 @@ import cn.daxpay.open.platform.core.exception.BizInfoException;
|
||||
public class PasswordExpiredAccessException extends BizInfoException {
|
||||
|
||||
public PasswordExpiredAccessException() {
|
||||
super(40301, "密码已过期,请先修改密码");
|
||||
initMessageKey("error.auth.auth.passwordExpired");
|
||||
}
|
||||
|
||||
public PasswordExpiredAccessException(String message) {
|
||||
super(40301, message);
|
||||
super(40301, "error.auth.passwordExpired");
|
||||
}
|
||||
|
||||
public PasswordExpiredAccessException(int code, String messageKey, Object... args) {
|
||||
@@ -1,5 +1,6 @@
|
||||
package cn.daxpay.open.platform.capability.auth.exception;
|
||||
package cn.daxpay.open.platform.iam.exception.auth;
|
||||
|
||||
import cn.daxpay.open.platform.capability.auth.authentication.AuthenticationChallengeException;
|
||||
import lombok.Getter;
|
||||
|
||||
/// # 需要双因素认证异常
|
||||
@@ -8,7 +9,7 @@ import lombok.Getter;
|
||||
/// 该异常不属于登录失败, 不应触发失败计数与失败日志, 由全局处理器返回挑战结果给前端。
|
||||
///
|
||||
@Getter
|
||||
public class TwoFactorRequiredException extends LoginFailureException {
|
||||
public class TwoFactorRequiredException extends AuthenticationChallengeException {
|
||||
|
||||
/// 需要双因素认证的响应码(前端据此识别并切换到二次验证界面)
|
||||
public static final int CODE = 40101;
|
||||
@@ -1,4 +1,6 @@
|
||||
package cn.daxpay.open.platform.capability.auth.exception;
|
||||
package cn.daxpay.open.platform.iam.exception.auth;
|
||||
|
||||
import cn.daxpay.open.platform.capability.auth.exception.LoginFailureException;
|
||||
|
||||
/// # 用户未找到异常
|
||||
///
|
||||
@@ -6,12 +8,12 @@ public class UserNotFoundException extends LoginFailureException {
|
||||
|
||||
public UserNotFoundException(String account) {
|
||||
super(account, "用户未找到");
|
||||
initMessageKey("error.auth.auth.userNotFound");
|
||||
initMessageKey("error.auth.userNotFound");
|
||||
}
|
||||
|
||||
public UserNotFoundException() {
|
||||
super("用户未找到");
|
||||
initMessageKey("error.auth.auth.userNotFound");
|
||||
initMessageKey("error.auth.userNotFound");
|
||||
}
|
||||
|
||||
|
||||
@@ -1,19 +1,22 @@
|
||||
package cn.daxpay.open.platform.iam.handler;
|
||||
|
||||
import cn.daxpay.open.platform.capability.auth.exception.PasswordExpiredAccessException;
|
||||
import cn.daxpay.open.platform.capability.auth.service.RouterCheck;
|
||||
import cn.daxpay.open.platform.capability.auth.service.AccessPolicy;
|
||||
import cn.daxpay.open.platform.capability.auth.util.SecurityUtil;
|
||||
import cn.daxpay.open.platform.core.entity.UserDetail;
|
||||
import cn.daxpay.open.platform.common.spring.util.WebServletUtil;
|
||||
import cn.daxpay.open.platform.core.entity.UserDetail;
|
||||
import cn.daxpay.open.platform.iam.exception.auth.PasswordExpiredAccessException;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/// # 密码状态检查
|
||||
/// # 密码状态访问策略
|
||||
///
|
||||
/// 检查已登录用户密码是否过期或是否为初始密码,若是则限制接口访问(仅放行改密等白名单路径)。
|
||||
/// 实现 [AccessPolicy] (而非 RouterCheck), 允许抛 [PasswordExpiredAccessException] 阻断请求。
|
||||
///
|
||||
/// 检查用户密码是否过期或是否为初始密码,如果是则限制接口访问
|
||||
@Component
|
||||
public class PasswordStatusCheck implements RouterCheck {
|
||||
public class PasswordStatusCheck implements AccessPolicy {
|
||||
|
||||
private static final List<String> ALLOWED_PATHS = List.of(
|
||||
"/user/auth/update-password",
|
||||
@@ -23,34 +26,21 @@ public class PasswordStatusCheck implements RouterCheck {
|
||||
);
|
||||
|
||||
@Override
|
||||
public int sortNo() {
|
||||
return 100;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean check(Object handler) {
|
||||
if (SecurityUtil.notLogin()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
UserDetail user = SecurityUtil.getUser();
|
||||
|
||||
public void check(HttpServletRequest request, UserDetail userDetail) {
|
||||
// 超级管理员跳过密码状态检查
|
||||
if (user.isAdmin()) {
|
||||
return false;
|
||||
if (userDetail.isAdmin()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!user.needChangePassword()) {
|
||||
return false;
|
||||
if (!userDetail.needChangePassword()) {
|
||||
return;
|
||||
}
|
||||
|
||||
String path = WebServletUtil.getPath();
|
||||
for (String allowedPath : ALLOWED_PATHS) {
|
||||
if (path.startsWith(allowedPath)) {
|
||||
return true;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// 密码已过期或为初始密码, 强制改密
|
||||
throw new PasswordExpiredAccessException();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
package cn.daxpay.open.platform.iam.handler.exception;
|
||||
|
||||
import cn.daxpay.open.platform.common.i18n.util.I18nUtil;
|
||||
import cn.daxpay.open.platform.core.rest.Res;
|
||||
import cn.daxpay.open.platform.core.rest.result.Result;
|
||||
import cn.daxpay.open.platform.iam.exception.auth.TwoFactorRequiredException;
|
||||
import cn.daxpay.open.platform.iam.result.auth.TwoFactorChallengeResult;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.core.Ordered;
|
||||
import org.springframework.core.annotation.Order;
|
||||
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||
import org.springframework.web.bind.annotation.RestControllerAdvice;
|
||||
|
||||
/// # IAM 认证异常处理
|
||||
///
|
||||
/// 集中处理双因素认证挑战等 IAM 认证流程异常, 返回前端可识别的结构化结果。
|
||||
/// 2FA 属 IAM 业务, 其异常处理内聚于本模块, 避免下层 service-system 反向依赖 service-iam。
|
||||
///
|
||||
@Slf4j
|
||||
@Order(Ordered.HIGHEST_PRECEDENCE)
|
||||
@RestControllerAdvice
|
||||
public class IamAuthExceptionHandler {
|
||||
|
||||
/// 双因素认证挑战: 密码通过但需二次验证, 返回预认证令牌, 不计入登录失败
|
||||
@ExceptionHandler(TwoFactorRequiredException.class)
|
||||
public Result<TwoFactorChallengeResult> handleTwoFactorRequired(TwoFactorRequiredException ex) {
|
||||
log.info(ex.getMessage());
|
||||
String message = I18nUtil.get(ex.getMessageKey(), ex.getArgs());
|
||||
return Res.response(TwoFactorRequiredException.CODE, message, new TwoFactorChallengeResult(ex.getPreAuthToken()));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package cn.daxpay.open.platform.capability.auth.entity;
|
||||
package cn.daxpay.open.platform.iam.result.auth;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
@@ -3,7 +3,7 @@ package cn.daxpay.open.platform.iam.service.user;
|
||||
import cn.daxpay.open.platform.iam.dao.user.UserInfoManager;
|
||||
import cn.daxpay.open.platform.iam.entity.user.UserInfo;
|
||||
import cn.daxpay.open.platform.iam.result.user.UserInfoResult;
|
||||
import cn.daxpay.open.platform.capability.auth.exception.UserNotFoundException;
|
||||
import cn.daxpay.open.platform.iam.exception.auth.UserNotFoundException;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
@@ -10,8 +10,6 @@ import cn.daxpay.open.platform.core.exception.BizInfoException;
|
||||
import cn.daxpay.open.platform.core.exception.BizWarnException;
|
||||
import cn.daxpay.open.platform.core.rest.Res;
|
||||
import cn.daxpay.open.platform.core.rest.result.Result;
|
||||
import cn.daxpay.open.platform.capability.auth.entity.TwoFactorChallengeResult;
|
||||
import cn.daxpay.open.platform.capability.auth.exception.TwoFactorRequiredException;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import jakarta.validation.ConstraintViolation;
|
||||
import jakarta.validation.ConstraintViolationException;
|
||||
@@ -84,18 +82,6 @@ public class RestExceptionHandler {
|
||||
return Res.response(ex.getCode(), message, MDC.get(CommonCode.TRACE_ID));
|
||||
}
|
||||
|
||||
/// 双因素认证挑战: 密码通过但需二次验证, 返回预认证令牌, 不计入登录失败
|
||||
@ExceptionHandler(TwoFactorRequiredException.class)
|
||||
public Object handleTwoFactorRequiredException(TwoFactorRequiredException ex, HttpServletResponse response) {
|
||||
if (isSseStream(response)) {
|
||||
logSse(ex);
|
||||
return ResponseEntity.ok().build();
|
||||
}
|
||||
log.info(ex.getMessage());
|
||||
String message = getMessage(ex);
|
||||
return Res.response(TwoFactorRequiredException.CODE, message, new TwoFactorChallengeResult(ex.getPreAuthToken()));
|
||||
}
|
||||
|
||||
/// 警告业务异常, 如果量多需要关注
|
||||
@ExceptionHandler(BizWarnException.class)
|
||||
public Object handleBizWarnException(BizWarnException ex, HttpServletResponse response) {
|
||||
|
||||
Reference in New Issue
Block a user