mirror of
				https://github.com/continew-org/continew-admin.git
				synced 2025-10-31 10:57:13 +08:00 
			
		
		
		
	refactor: 优化认证及客户端相关代码
This commit is contained in:
		| @@ -65,12 +65,12 @@ public class SysConstants { | ||||
|     public static final String ALL_PERMISSION = "*:*:*"; | ||||
|  | ||||
|     /** | ||||
|      * 账号登录 URI | ||||
|      * 登录 URI | ||||
|      */ | ||||
|     public static final String LOGIN_URI = "/auth/login"; | ||||
|  | ||||
|     /** | ||||
|      * 退出 URI | ||||
|      * 登出 URI | ||||
|      */ | ||||
|     public static final String LOGOUT_URI = "/auth/logout"; | ||||
|  | ||||
|   | ||||
| @@ -81,12 +81,12 @@ public class UserContext implements Serializable { | ||||
|     private Set<RoleContext> roles; | ||||
|  | ||||
|     /** | ||||
|      * 设备类型 | ||||
|      * 客户端类型 | ||||
|      */ | ||||
|     private String clientType; | ||||
|  | ||||
|     /** | ||||
|      * 客户端ID | ||||
|      * 客户端 ID | ||||
|      */ | ||||
|     private String clientId; | ||||
|  | ||||
|   | ||||
| @@ -14,23 +14,30 @@ | ||||
|  * limitations under the License. | ||||
|  */ | ||||
| 
 | ||||
| package top.continew.admin.auth.handler; | ||||
| package top.continew.admin.auth; | ||||
| 
 | ||||
| import cn.dev33.satoken.stp.SaLoginModel; | ||||
| import cn.dev33.satoken.stp.StpUtil; | ||||
| import cn.hutool.core.bean.BeanUtil; | ||||
| import jakarta.annotation.Resource; | ||||
| import lombok.RequiredArgsConstructor; | ||||
| import jakarta.servlet.http.HttpServletRequest; | ||||
| import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; | ||||
| import org.springframework.stereotype.Component; | ||||
| import top.continew.admin.auth.model.req.AuthReq; | ||||
| import top.continew.admin.common.context.RoleContext; | ||||
| import top.continew.admin.common.context.UserContext; | ||||
| import top.continew.admin.common.context.UserContextHolder; | ||||
| import top.continew.admin.common.context.UserExtraContext; | ||||
| import top.continew.admin.common.enums.DisEnableStatusEnum; | ||||
| import top.continew.admin.system.model.entity.DeptDO; | ||||
| import top.continew.admin.system.model.entity.UserDO; | ||||
| import top.continew.admin.system.model.resp.ClientResp; | ||||
| import top.continew.admin.system.service.DeptService; | ||||
| import top.continew.admin.system.service.OptionService; | ||||
| import top.continew.admin.system.service.RoleService; | ||||
| import top.continew.admin.system.service.UserService; | ||||
| import top.continew.starter.core.validation.CheckUtils; | ||||
| import top.continew.starter.core.validation.ValidationUtils; | ||||
| import top.continew.starter.web.util.SpringWebUtils; | ||||
| 
 | ||||
| import java.util.Set; | ||||
| @@ -39,35 +46,49 @@ import java.util.concurrent.CompletableFuture; | ||||
| import static top.continew.admin.system.enums.PasswordPolicyEnum.PASSWORD_EXPIRATION_DAYS; | ||||
| 
 | ||||
| /** | ||||
|  * 认证处理器抽象类 | ||||
|  * 认证处理器基类 | ||||
|  * | ||||
|  * @author KAI | ||||
|  * @author Charles7c | ||||
|  * @since 2024/12/22 14:52 | ||||
|  */ | ||||
| @Component | ||||
| @RequiredArgsConstructor | ||||
| public abstract class AbstractAuthHandler { | ||||
| public abstract class AbstractAuthHandler<T extends AuthReq> implements AuthHandler<T> { | ||||
| 
 | ||||
|     @Resource | ||||
|     private RoleService roleService; | ||||
|     protected OptionService optionService; | ||||
|     @Resource | ||||
|     private OptionService optionService; | ||||
|     protected UserService userService; | ||||
|     @Resource | ||||
|     protected RoleService roleService; | ||||
|     @Resource | ||||
|     private DeptService deptService; | ||||
|     @Resource | ||||
|     private ThreadPoolTaskExecutor threadPoolTaskExecutor; | ||||
| 
 | ||||
|     public static final String CAPTCHA_EXPIRED = "验证码已失效"; | ||||
|     public static final String CAPTCHA_ERROR = "验证码错误"; | ||||
|     public static final String CLIENT_ID = "clientId"; | ||||
|     protected static final String CAPTCHA_EXPIRED = "验证码已失效"; | ||||
|     protected static final String CAPTCHA_ERROR = "验证码错误"; | ||||
|     protected static final String CLIENT_ID = "clientId"; | ||||
| 
 | ||||
|     @Override | ||||
|     public void preLogin(T req, ClientResp client, HttpServletRequest request) { | ||||
|         // 参数校验 | ||||
|         ValidationUtils.validate(req); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void postLogin(T req, ClientResp client, HttpServletRequest request) { | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * 获取登录凭证 | ||||
|      * 认证 | ||||
|      * | ||||
|      * @param user       用户信息 | ||||
|      * @param clientResp 客户端信息 | ||||
|      * @return token 认证信息 | ||||
|      * @param user   用户信息 | ||||
|      * @param client 客户端信息 | ||||
|      * @return token 令牌信息 | ||||
|      */ | ||||
|     protected String authCertificate(UserDO user, ClientResp clientResp) { | ||||
|         preLogin(user, clientResp); | ||||
|         // 核心登录逻辑 | ||||
|     protected String authenticate(UserDO user, ClientResp client) { | ||||
|         // 获取权限、角色、密码过期天数 | ||||
|         Long userId = user.getId(); | ||||
|         CompletableFuture<Set<String>> permissionFuture = CompletableFuture.supplyAsync(() -> roleService | ||||
|             .listPermissionByUserId(userId), threadPoolTaskExecutor); | ||||
| @@ -76,62 +97,35 @@ public abstract class AbstractAuthHandler { | ||||
|         CompletableFuture<Integer> passwordExpirationDaysFuture = CompletableFuture.supplyAsync(() -> optionService | ||||
|             .getValueByCode2Int(PASSWORD_EXPIRATION_DAYS.name())); | ||||
|         CompletableFuture.allOf(permissionFuture, roleFuture, passwordExpirationDaysFuture); | ||||
| 
 | ||||
|         UserContext userContext = new UserContext(permissionFuture.join(), roleFuture | ||||
|             .join(), passwordExpirationDaysFuture.join()); | ||||
| 
 | ||||
|         BeanUtil.copyProperties(user, userContext); | ||||
|         SaLoginModel model = new SaLoginModel(); | ||||
|         // 设置登录 token 最低活跃频率 如未指定,则使用全局配置的 activeTimeout 值 | ||||
|         model.setActiveTimeout(clientResp.getActiveTimeout()); | ||||
|         // 设置登录 token 有效期,单位:秒 (如未指定,自动取全局配置的 timeout 值 | ||||
|         model.setTimeout(clientResp.getTimeout()); | ||||
|         // 设置设备类型 | ||||
|         model.setDevice(clientResp.getClientType()); | ||||
|         userContext.setClientType(clientResp.getClientType()); | ||||
|         // 设置客户端id | ||||
|         userContext.setClientId(clientResp.getClientId()); | ||||
|         model.setExtra(CLIENT_ID, clientResp.getClientId()); | ||||
|         // 自定义用户上下文处理 | ||||
|         customizeUserContext(userContext, user, clientResp); | ||||
| 
 | ||||
|         // 登录并缓存用户信息 | ||||
|         SaLoginModel model = new SaLoginModel(); | ||||
|         // 指定此次登录 token 最低活跃频率,单位:秒(如未指定,则使用全局配置的 activeTimeout 值) | ||||
|         model.setActiveTimeout(client.getActiveTimeout()); | ||||
|         // 指定此次登录 token 有效期,单位:秒 (如未指定,自动取全局配置的 timeout 值) | ||||
|         model.setTimeout(client.getTimeout()); | ||||
|         // 客户端类型 | ||||
|         model.setDevice(client.getClientType()); | ||||
|         userContext.setClientType(client.getClientType()); | ||||
|         // 客户端 ID | ||||
|         model.setExtra(CLIENT_ID, client.getClientId()); | ||||
|         userContext.setClientId(client.getClientId()); | ||||
|         StpUtil.login(userContext.getId(), model.setExtraData(BeanUtil.beanToMap(new UserExtraContext(SpringWebUtils | ||||
|             .getRequest())))); | ||||
|         UserContextHolder.setContext(userContext); | ||||
| 
 | ||||
|         // 后置处理 | ||||
|         String token = StpUtil.getTokenValue(); | ||||
|         postLogin(token, user, clientResp); | ||||
|         return token; | ||||
|         return StpUtil.getTokenValue(); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * 登录前置处理 | ||||
|      * 检查用户状态 | ||||
|      * | ||||
|      * @param user       用户信息 | ||||
|      * @param clientResp 客户端信息 | ||||
|      * @param user 用户信息 | ||||
|      */ | ||||
|     private void preLogin(UserDO user, ClientResp clientResp) { | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * 自定义用户上下文处理 | ||||
|      * | ||||
|      * @param userContext 用户上下文 | ||||
|      * @param user        用户信息 | ||||
|      * @param clientResp  客户端信息 | ||||
|      */ | ||||
|     protected void customizeUserContext(UserContext userContext, UserDO user, ClientResp clientResp) { | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * 登录后置处理 | ||||
|      * | ||||
|      * @param token      登录令牌 | ||||
|      * @param user       用户信息 | ||||
|      * @param clientResp 客户端信息 | ||||
|      */ | ||||
|     protected void postLogin(String token, UserDO user, ClientResp clientResp) { | ||||
|     protected void checkUserStatus(UserDO user) { | ||||
|         CheckUtils.throwIfEqual(DisEnableStatusEnum.DISABLE, user.getStatus(), "此账号已被禁用,如有疑问,请联系管理员"); | ||||
|         DeptDO dept = deptService.getById(user.getDeptId()); | ||||
|         CheckUtils.throwIfEqual(DisEnableStatusEnum.DISABLE, dept.getStatus(), "此账号所属部门已被禁用,如有疑问,请联系管理员"); | ||||
|     } | ||||
| } | ||||
| @@ -19,37 +19,52 @@ package top.continew.admin.auth; | ||||
| import jakarta.servlet.http.HttpServletRequest; | ||||
| import top.continew.admin.auth.enums.AuthTypeEnum; | ||||
| import top.continew.admin.auth.model.req.AuthReq; | ||||
| import top.continew.admin.auth.model.resp.LoginResp; | ||||
| import top.continew.admin.system.model.resp.ClientResp; | ||||
|  | ||||
| /** | ||||
|  * 认证接口 | ||||
|  * 认证处理器 | ||||
|  * | ||||
|  * @author KAI | ||||
|  * @since 2024/12/22 14:52:23 | ||||
|  * @author Charles7c | ||||
|  * @since 2024/12/22 14:52 | ||||
|  */ | ||||
| public interface AuthHandler<T extends AuthReq, R> { | ||||
| public interface AuthHandler<T extends AuthReq> { | ||||
|  | ||||
|     /** | ||||
|      * 执行登录 | ||||
|      * 登录 | ||||
|      * | ||||
|      * @param authReq 登录请求参数 | ||||
|      * @param request HTTP请求对象 | ||||
|      * @return 登录响应 | ||||
|      * @param req     登录请求参数 | ||||
|      * @param client  客户端信息 | ||||
|      * @param request 请求对象 | ||||
|      * @return 登录响应参数 | ||||
|      */ | ||||
|     R login(T authReq, ClientResp clientResp, HttpServletRequest request); | ||||
|     LoginResp login(T req, ClientResp client, HttpServletRequest request); | ||||
|  | ||||
|     /** | ||||
|      * 获取登录类型 | ||||
|      * 登录前置处理 | ||||
|      * | ||||
|      * @return 登录类型Enum | ||||
|      * | ||||
|      * @param req     登录请求参数 | ||||
|      * @param client  客户端信息 | ||||
|      * @param request 请求对象 | ||||
|      */ | ||||
|     void preLogin(T req, ClientResp client, HttpServletRequest request); | ||||
|  | ||||
|     /** | ||||
|      * 登录后置处理 | ||||
|      * | ||||
|      * | ||||
|      * @param req     登录请求参数 | ||||
|      * @param client  客户端信息 | ||||
|      * @param request 请求对象 | ||||
|      */ | ||||
|     void postLogin(T req, ClientResp client, HttpServletRequest request); | ||||
|  | ||||
|     /** | ||||
|      * 获取认证类型 | ||||
|      * | ||||
|      * @return 认证类型 | ||||
|      */ | ||||
|     AuthTypeEnum getAuthType(); | ||||
|  | ||||
|     /** | ||||
|      * 校验参数 | ||||
|      * | ||||
|      * @param authReq 登录请求参数 | ||||
|      */ | ||||
|     void validate(T authReq); | ||||
|  | ||||
| } | ||||
| @@ -14,40 +14,43 @@ | ||||
|  * limitations under the License. | ||||
|  */ | ||||
| 
 | ||||
| package top.continew.admin.auth.config; | ||||
| package top.continew.admin.auth; | ||||
| 
 | ||||
| import org.springframework.beans.factory.annotation.Autowired; | ||||
| import org.springframework.stereotype.Component; | ||||
| import top.continew.admin.auth.enums.AuthTypeEnum; | ||||
| import top.continew.admin.auth.model.req.AuthReq; | ||||
| import top.continew.admin.auth.AuthHandler; | ||||
| 
 | ||||
| import java.util.HashMap; | ||||
| import java.util.List; | ||||
| import java.util.Map; | ||||
| 
 | ||||
| /** | ||||
|  * 登录类型策略上文 | ||||
|  * 认证策略 | ||||
|  * | ||||
|  * @author KAI | ||||
|  * @since 2024/12/20 15:16:55 | ||||
|  * @author Charles7c | ||||
|  * @since 2024/12/20 15:16 | ||||
|  */ | ||||
| @Component | ||||
| public class AuthHandlerContext { | ||||
|     private final Map<String, AuthHandler<?, ?>> handlerMap = new HashMap<>(); | ||||
| public class AuthStrategy { | ||||
| 
 | ||||
|     private final Map<AuthTypeEnum, AuthHandler<? extends AuthReq>> handlerMap = new HashMap<>(); | ||||
| 
 | ||||
|     @Autowired | ||||
|     public AuthHandlerContext(List<AuthHandler<?, ?>> strategies) { | ||||
|         for (AuthHandler<?, ?> strategy : strategies) { | ||||
|             handlerMap.put(strategy.getAuthType().getValue(), strategy); | ||||
|     public AuthStrategy(List<AuthHandler<? extends AuthReq>> handlers) { | ||||
|         for (AuthHandler<? extends AuthReq> handler : handlers) { | ||||
|             handlerMap.put(handler.getAuthType(), handler); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     @SuppressWarnings("unchecked") | ||||
|     public <T extends AuthReq, R> AuthHandler<T, R> getHandler(String type) { | ||||
|         AuthHandler<?, ?> strategy = handlerMap.get(type); | ||||
|         if (strategy == null) { | ||||
|             throw new IllegalArgumentException("No handler found for type: " + type); | ||||
|         } | ||||
|         return (AuthHandler<T, R>)strategy; | ||||
|     /** | ||||
|      * 根据认证类型获取 | ||||
|      * | ||||
|      * @param authType 认证类型 | ||||
|      * @return 认证处理器 | ||||
|      */ | ||||
|     public AuthHandler<AuthReq> getHandler(AuthTypeEnum authType) { | ||||
|         return (AuthHandler<AuthReq>)handlerMap.get(authType); | ||||
|     } | ||||
| } | ||||
| @@ -22,10 +22,11 @@ import top.continew.admin.common.constant.UiConstants; | ||||
| import top.continew.starter.core.enums.BaseEnum; | ||||
|  | ||||
| /** | ||||
|  * 认证类型 | ||||
|  * 认证类型枚举 | ||||
|  * | ||||
|  * @author KAI | ||||
|  * @author Charles7c | ||||
|  * @since 2023/12/23 13:38 | ||||
|  * @since 2024/12/22 14:52 | ||||
|  */ | ||||
| @Getter | ||||
| @RequiredArgsConstructor | ||||
| @@ -34,22 +35,22 @@ public enum AuthTypeEnum implements BaseEnum<String> { | ||||
|     /** | ||||
|      * 账号 | ||||
|      */ | ||||
|     ACCOUNT("account", "账号", UiConstants.COLOR_ERROR), | ||||
|     ACCOUNT("ACCOUNT", "账号", UiConstants.COLOR_SUCCESS), | ||||
|  | ||||
|     /** | ||||
|      * 邮箱 | ||||
|      */ | ||||
|     EMAIL("email", "邮箱", UiConstants.COLOR_PRIMARY), | ||||
|     EMAIL("EMAIL", "邮箱", UiConstants.COLOR_PRIMARY), | ||||
|  | ||||
|     /** | ||||
|      * 手机号 | ||||
|      */ | ||||
|     PHONE("phone", "手机号", UiConstants.COLOR_SUCCESS), | ||||
|     PHONE("PHONE", "手机号", UiConstants.COLOR_PRIMARY), | ||||
|  | ||||
|     /** | ||||
|      * 第三方授权 | ||||
|      * 第三方账号 | ||||
|      */ | ||||
|     SOCIAL_AUTH("socialAuth", "第三方授权", UiConstants.COLOR_DEFAULT); | ||||
|     SOCIAL("SOCIAL", "第三方账号", UiConstants.COLOR_ERROR); | ||||
|  | ||||
|     private final String value; | ||||
|     private final String description; | ||||
|   | ||||
| @@ -16,105 +16,109 @@ | ||||
|  | ||||
| package top.continew.admin.auth.handler; | ||||
|  | ||||
| import cn.hutool.core.util.ObjectUtil; | ||||
| import cn.hutool.extra.servlet.JakartaServletUtil; | ||||
| import jakarta.servlet.http.HttpServletRequest; | ||||
| import lombok.RequiredArgsConstructor; | ||||
| import org.springframework.security.crypto.password.PasswordEncoder; | ||||
| import org.springframework.stereotype.Component; | ||||
| import top.continew.admin.auth.AbstractAuthHandler; | ||||
| import top.continew.admin.auth.enums.AuthTypeEnum; | ||||
| import top.continew.admin.auth.model.req.AccountAuthReq; | ||||
| import top.continew.admin.auth.model.resp.LoginResp; | ||||
| import top.continew.admin.auth.service.LoginService; | ||||
| import top.continew.admin.auth.AuthHandler; | ||||
| import top.continew.admin.common.constant.CacheConstants; | ||||
| import top.continew.admin.common.constant.SysConstants; | ||||
| import top.continew.admin.common.util.SecureUtils; | ||||
| import top.continew.admin.system.enums.PasswordPolicyEnum; | ||||
| import top.continew.admin.system.model.entity.UserDO; | ||||
| import top.continew.admin.system.model.resp.ClientResp; | ||||
| import top.continew.admin.system.service.OptionService; | ||||
| import top.continew.admin.system.service.UserService; | ||||
| import top.continew.starter.cache.redisson.util.RedisUtils; | ||||
| import top.continew.starter.core.util.ExceptionUtils; | ||||
| import top.continew.starter.core.validation.CheckUtils; | ||||
| import top.continew.starter.core.validation.ValidationUtils; | ||||
|  | ||||
| import java.time.Duration; | ||||
|  | ||||
| /** | ||||
|  * 账号认证处理器 | ||||
|  * | ||||
|  * @author KAI | ||||
|  * @since 2024/12/22 14:58:32 | ||||
|  * @author Charles7c | ||||
|  * @since 2024/12/22 14:58 | ||||
|  */ | ||||
| @Component | ||||
| @RequiredArgsConstructor | ||||
| public class AccountAuthHandler extends AbstractAuthHandler implements AuthHandler<AccountAuthReq, LoginResp> { | ||||
| public class AccountAuthHandler extends AbstractAuthHandler<AccountAuthReq> { | ||||
|  | ||||
|     private final UserService userService; | ||||
|     private final LoginService loginService; | ||||
|     private final PasswordEncoder passwordEncoder; | ||||
|     private final OptionService optionService; | ||||
|  | ||||
|     /** | ||||
|      * 获取认证类型 | ||||
|      * | ||||
|      * @return 账号认证类型 | ||||
|      */ | ||||
|     @Override | ||||
|     public LoginResp login(AccountAuthReq req, ClientResp client, HttpServletRequest request) { | ||||
|         // 解密密码 | ||||
|         String rawPassword = ExceptionUtils.exToNull(() -> SecureUtils.decryptByRsaPrivateKey(req.getPassword())); | ||||
|         ValidationUtils.throwIfBlank(rawPassword, "密码解密失败"); | ||||
|         // 验证用户名密码 | ||||
|         String username = req.getUsername(); | ||||
|         UserDO user = userService.getByUsername(username); | ||||
|         boolean isError = ObjectUtil.isNull(user) || !passwordEncoder.matches(rawPassword, user.getPassword()); | ||||
|         // 检查账号锁定状态 | ||||
|         this.checkUserLocked(req.getUsername(), request, isError); | ||||
|         ValidationUtils.throwIf(isError, "用户名或密码错误"); | ||||
|         // 检查用户状态 | ||||
|         super.checkUserStatus(user); | ||||
|         // 执行认证 | ||||
|         String token = this.authenticate(user, client); | ||||
|         return LoginResp.builder().token(token).build(); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public void preLogin(AccountAuthReq req, ClientResp client, HttpServletRequest request) { | ||||
|         super.preLogin(req, client, request); | ||||
|         // 校验验证码 | ||||
|         int loginCaptchaEnabled = optionService.getValueByCode2Int("LOGIN_CAPTCHA_ENABLED"); | ||||
|         if (SysConstants.YES.equals(loginCaptchaEnabled)) { | ||||
|             ValidationUtils.throwIfBlank(req.getCaptcha(), "验证码不能为空"); | ||||
|             ValidationUtils.throwIfBlank(req.getUuid(), "验证码标识不能为空"); | ||||
|             String captchaKey = CacheConstants.CAPTCHA_KEY_PREFIX + req.getUuid(); | ||||
|             String captcha = RedisUtils.get(captchaKey); | ||||
|             ValidationUtils.throwIfBlank(captcha, CAPTCHA_EXPIRED); | ||||
|             RedisUtils.delete(captchaKey); | ||||
|             ValidationUtils.throwIfNotEqualIgnoreCase(req.getCaptcha(), captcha, CAPTCHA_ERROR); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public AuthTypeEnum getAuthType() { | ||||
|         return AuthTypeEnum.ACCOUNT; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 校验账号登录请求对象 | ||||
|      * 检测用户是否已被锁定 | ||||
|      * | ||||
|      * @param authReq 登录请求参数 | ||||
|      * @param username 用户名 | ||||
|      * @param request  请求对象 | ||||
|      * @param isError  是否登录错误 | ||||
|      */ | ||||
|     @Override | ||||
|     public void validate(AccountAuthReq authReq) { | ||||
|         // 获取验证码开关 | ||||
|         int enableCaptcha = optionService.getValueByCode2Int("LOGIN_CAPTCHA_ENABLED"); | ||||
|  | ||||
|         ValidationUtils.validate(authReq); | ||||
|         if (SysConstants.YES.equals(enableCaptcha)) { | ||||
|             ValidationUtils.throwIfEmpty(authReq.getCaptcha(), "验证码不能为空"); | ||||
|             ValidationUtils.throwIfEmpty(authReq.getUuid(), "验证码标识不能为空"); | ||||
|     private void checkUserLocked(String username, HttpServletRequest request, boolean isError) { | ||||
|         // 不锁定 | ||||
|         int maxErrorCount = optionService.getValueByCode2Int(PasswordPolicyEnum.PASSWORD_ERROR_LOCK_COUNT.name()); | ||||
|         if (maxErrorCount <= SysConstants.NO) { | ||||
|             return; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 账号登录 | ||||
|      * | ||||
|      * @param authReq 账号登录请求对象 | ||||
|      * @param request HTTP请求对象 | ||||
|      * @return 登录响应 | ||||
|      */ | ||||
|     @Override | ||||
|     public LoginResp login(AccountAuthReq authReq, ClientResp clientResp, HttpServletRequest request) { | ||||
|         this.validate(authReq); | ||||
|         // 解密密码 | ||||
|         String rawPassword = ExceptionUtils.exToNull(() -> SecureUtils.decryptByRsaPrivateKey(authReq.getPassword())); | ||||
|         ValidationUtils.throwIfBlank(rawPassword, "密码解密失败"); | ||||
|  | ||||
|         // 验证用户名密码 | ||||
|         UserDO user = userService.getByUsername(authReq.getUsername()); | ||||
|         boolean isError = user == null || !passwordEncoder.matches(rawPassword, user.getPassword()); | ||||
|  | ||||
|         // 检查账号锁定状态 | ||||
|         loginService.checkUserLocked(authReq.getUsername(), request, isError); | ||||
|         ValidationUtils.throwIf(isError, "用户名或密码错误"); | ||||
|  | ||||
|         // 检查用户状态 | ||||
|         loginService.checkUserStatus(user); | ||||
|  | ||||
|         // 执行登录 | ||||
|         String token = this.authCertificate(user, clientResp); | ||||
|         return LoginResp.builder().token(token).build(); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 获取认证信息 | ||||
|      * | ||||
|      * @param user       用户信息 | ||||
|      * @param clientResp 客户端信息 | ||||
|      * @return 认证信息 | ||||
|      */ | ||||
|     @Override | ||||
|     public String authCertificate(UserDO user, ClientResp clientResp) { | ||||
|         return super.authCertificate(user, clientResp); | ||||
|         // 检测是否已被锁定 | ||||
|         String key = CacheConstants.USER_PASSWORD_ERROR_KEY_PREFIX + RedisUtils.formatKey(username, JakartaServletUtil | ||||
|             .getClientIP(request)); | ||||
|         int lockMinutes = optionService.getValueByCode2Int(PasswordPolicyEnum.PASSWORD_ERROR_LOCK_MINUTES.name()); | ||||
|         Integer currentErrorCount = ObjectUtil.defaultIfNull(RedisUtils.get(key), 0); | ||||
|         CheckUtils.throwIf(currentErrorCount >= maxErrorCount, "账号锁定 {} 分钟,请稍后再试", lockMinutes); | ||||
|         // 登录成功清除计数 | ||||
|         if (!isError) { | ||||
|             RedisUtils.delete(key); | ||||
|             return; | ||||
|         } | ||||
|         // 登录失败递增计数 | ||||
|         currentErrorCount++; | ||||
|         RedisUtils.set(key, currentErrorCount, Duration.ofMinutes(lockMinutes)); | ||||
|         CheckUtils.throwIf(currentErrorCount >= maxErrorCount, "密码错误已达 {} 次,账号锁定 {} 分钟", maxErrorCount, lockMinutes); | ||||
|     } | ||||
| } | ||||
| @@ -17,17 +17,14 @@ | ||||
| package top.continew.admin.auth.handler; | ||||
|  | ||||
| import jakarta.servlet.http.HttpServletRequest; | ||||
| import lombok.RequiredArgsConstructor; | ||||
| import org.springframework.stereotype.Component; | ||||
| import top.continew.admin.auth.AbstractAuthHandler; | ||||
| import top.continew.admin.auth.enums.AuthTypeEnum; | ||||
| import top.continew.admin.auth.model.req.EmailAuthReq; | ||||
| import top.continew.admin.auth.model.resp.LoginResp; | ||||
| import top.continew.admin.auth.service.LoginService; | ||||
| import top.continew.admin.auth.AuthHandler; | ||||
| import top.continew.admin.common.constant.CacheConstants; | ||||
| import top.continew.admin.system.model.entity.UserDO; | ||||
| import top.continew.admin.system.model.resp.ClientResp; | ||||
| import top.continew.admin.system.service.UserService; | ||||
| import top.continew.starter.cache.redisson.util.RedisUtils; | ||||
| import top.continew.starter.core.validation.ValidationUtils; | ||||
|  | ||||
| @@ -35,73 +32,36 @@ import top.continew.starter.core.validation.ValidationUtils; | ||||
|  * 邮箱认证处理器 | ||||
|  * | ||||
|  * @author KAI | ||||
|  * @author Charles7c | ||||
|  * @since 2024/12/22 14:58 | ||||
|  */ | ||||
| @Component | ||||
| @RequiredArgsConstructor | ||||
| public class EmailAuthHandler extends AbstractAuthHandler implements AuthHandler<EmailAuthReq, LoginResp> { | ||||
| public class EmailAuthHandler extends AbstractAuthHandler<EmailAuthReq> { | ||||
|  | ||||
|     private final UserService userService; | ||||
|     private final LoginService loginService; | ||||
|     @Override | ||||
|     public LoginResp login(EmailAuthReq req, ClientResp client, HttpServletRequest request) { | ||||
|         // 验证邮箱 | ||||
|         UserDO user = userService.getByEmail(req.getEmail()); | ||||
|         ValidationUtils.throwIfNull(user, "此邮箱未绑定本系统账号"); | ||||
|         // 检查用户状态 | ||||
|         super.checkUserStatus(user); | ||||
|         // 执行认证 | ||||
|         String token = super.authenticate(user, client); | ||||
|         return LoginResp.builder().token(token).build(); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public void preLogin(EmailAuthReq req, ClientResp client, HttpServletRequest request) { | ||||
|         String email = req.getEmail(); | ||||
|         String captchaKey = CacheConstants.CAPTCHA_KEY_PREFIX + email; | ||||
|         String captcha = RedisUtils.get(captchaKey); | ||||
|         ValidationUtils.throwIfBlank(captcha, CAPTCHA_EXPIRED); | ||||
|         ValidationUtils.throwIfNotEqualIgnoreCase(req.getCaptcha(), captcha, CAPTCHA_ERROR); | ||||
|         RedisUtils.delete(captchaKey); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 获取认证类型 | ||||
|      * | ||||
|      * @return 邮箱认证类型 | ||||
|      */ | ||||
|     @Override | ||||
|     public AuthTypeEnum getAuthType() { | ||||
|         return AuthTypeEnum.EMAIL; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 校验邮箱登录请求对象 | ||||
|      * | ||||
|      * @param authReq 邮箱登录请求参数 | ||||
|      */ | ||||
|     @Override | ||||
|     public void validate(EmailAuthReq authReq) { | ||||
|         ValidationUtils.validate(authReq); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 邮箱登录 | ||||
|      * | ||||
|      * @param authReq 邮箱登录请求对象 | ||||
|      * @param request HTTP请求对象 | ||||
|      * @return 登录响应 | ||||
|      */ | ||||
|     @Override | ||||
|     public LoginResp login(EmailAuthReq authReq, ClientResp clientResp, HttpServletRequest request) { | ||||
|         this.validate(authReq); | ||||
|  | ||||
|         String email = authReq.getEmail(); | ||||
|         String captchaKey = CacheConstants.CAPTCHA_KEY_PREFIX + email; | ||||
|         String captcha = RedisUtils.get(captchaKey); | ||||
|         ValidationUtils.throwIfBlank(captcha, AbstractAuthHandler.CAPTCHA_EXPIRED); | ||||
|         ValidationUtils.throwIfNotEqualIgnoreCase(authReq.getCaptcha(), captcha, CAPTCHA_ERROR); | ||||
|         RedisUtils.delete(captchaKey); | ||||
|         // 验证邮箱 | ||||
|         UserDO user = userService.getByEmail(authReq.getEmail()); | ||||
|         ValidationUtils.throwIfNull(user, "此邮箱未绑定本系统账号"); | ||||
|  | ||||
|         // 检查用户状态 | ||||
|         loginService.checkUserStatus(user); | ||||
|  | ||||
|         // 执行登录 | ||||
|         String token = this.authCertificate(user, clientResp); | ||||
|         return LoginResp.builder().token(token).build(); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 获取登录凭证 | ||||
|      * | ||||
|      * @param user       用户信息 | ||||
|      * @param clientResp 客户端信息 | ||||
|      * @return token 认证信息 | ||||
|      */ | ||||
|     @Override | ||||
|     public String authCertificate(UserDO user, ClientResp clientResp) { | ||||
|         return super.authCertificate(user, clientResp); | ||||
|     } | ||||
| } | ||||
| @@ -17,17 +17,14 @@ | ||||
| package top.continew.admin.auth.handler; | ||||
|  | ||||
| import jakarta.servlet.http.HttpServletRequest; | ||||
| import lombok.RequiredArgsConstructor; | ||||
| import org.springframework.stereotype.Component; | ||||
| import top.continew.admin.auth.AbstractAuthHandler; | ||||
| import top.continew.admin.auth.enums.AuthTypeEnum; | ||||
| import top.continew.admin.auth.model.req.PhoneAuthReq; | ||||
| import top.continew.admin.auth.model.resp.LoginResp; | ||||
| import top.continew.admin.auth.service.LoginService; | ||||
| import top.continew.admin.auth.AuthHandler; | ||||
| import top.continew.admin.common.constant.CacheConstants; | ||||
| import top.continew.admin.system.model.entity.UserDO; | ||||
| import top.continew.admin.system.model.resp.ClientResp; | ||||
| import top.continew.admin.system.service.UserService; | ||||
| import top.continew.starter.cache.redisson.util.RedisUtils; | ||||
| import top.continew.starter.core.validation.ValidationUtils; | ||||
|  | ||||
| @@ -35,75 +32,36 @@ import top.continew.starter.core.validation.ValidationUtils; | ||||
|  * 手机号认证处理器 | ||||
|  * | ||||
|  * @author KAI | ||||
|  * @author Charles7c | ||||
|  * @since 2024/12/22 14:59 | ||||
|  */ | ||||
| @Component | ||||
| @RequiredArgsConstructor | ||||
| public class PhoneAuthHandler extends AbstractAuthHandler implements AuthHandler<PhoneAuthReq, LoginResp> { | ||||
| public class PhoneAuthHandler extends AbstractAuthHandler<PhoneAuthReq> { | ||||
|  | ||||
|     private final UserService userService; | ||||
|     private final LoginService loginService; | ||||
|     @Override | ||||
|     public LoginResp login(PhoneAuthReq req, ClientResp client, HttpServletRequest request) { | ||||
|         // 验证手机号 | ||||
|         UserDO user = userService.getByPhone(req.getPhone()); | ||||
|         ValidationUtils.throwIfNull(user, "此手机号未绑定本系统账号"); | ||||
|         // 检查用户状态 | ||||
|         super.checkUserStatus(user); | ||||
|         // 执行认证 | ||||
|         String token = super.authenticate(user, client); | ||||
|         return LoginResp.builder().token(token).build(); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public void preLogin(PhoneAuthReq req, ClientResp client, HttpServletRequest request) { | ||||
|         String phone = req.getPhone(); | ||||
|         String captchaKey = CacheConstants.CAPTCHA_KEY_PREFIX + phone; | ||||
|         String captcha = RedisUtils.get(captchaKey); | ||||
|         ValidationUtils.throwIfBlank(captcha, CAPTCHA_EXPIRED); | ||||
|         ValidationUtils.throwIfNotEqualIgnoreCase(req.getCaptcha(), captcha, CAPTCHA_ERROR); | ||||
|         RedisUtils.delete(captchaKey); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 获取认证类型 | ||||
|      * | ||||
|      * @return 手机号认证类型 | ||||
|      */ | ||||
|     @Override | ||||
|     public AuthTypeEnum getAuthType() { | ||||
|         return AuthTypeEnum.PHONE; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 校验手机号登录请求对象 | ||||
|      * | ||||
|      * @param authReq 手机号登录请求参数 | ||||
|      */ | ||||
|     @Override | ||||
|     public void validate(PhoneAuthReq authReq) { | ||||
|         ValidationUtils.validate(authReq); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 手机号登录 | ||||
|      * | ||||
|      * @param authReq 手机号登录请求对象 | ||||
|      * @param request HTTP请求对象 | ||||
|      * @return 登录响应 | ||||
|      */ | ||||
|     @Override | ||||
|     public LoginResp login(PhoneAuthReq authReq, ClientResp clientResp, HttpServletRequest request) { | ||||
|         //校验参数 | ||||
|         this.validate(authReq); | ||||
|  | ||||
|         String phone = authReq.getPhone(); | ||||
|         String captchaKey = CacheConstants.CAPTCHA_KEY_PREFIX + phone; | ||||
|         String captcha = RedisUtils.get(captchaKey); | ||||
|         ValidationUtils.throwIfBlank(captcha, AbstractAuthHandler.CAPTCHA_EXPIRED); | ||||
|         ValidationUtils.throwIfNotEqualIgnoreCase(authReq.getCaptcha(), captcha, AbstractAuthHandler.CAPTCHA_ERROR); | ||||
|         RedisUtils.delete(captchaKey); | ||||
|  | ||||
|         // 验证手机号 | ||||
|         UserDO user = userService.getByPhone(authReq.getPhone()); | ||||
|         ValidationUtils.throwIfNull(user, "此手机号未绑定本系统账号"); | ||||
|  | ||||
|         // 检查用户状态 | ||||
|         loginService.checkUserStatus(user); | ||||
|  | ||||
|         // 执行登录 | ||||
|         String token = this.authCertificate(user, clientResp); | ||||
|         return LoginResp.builder().token(token).build(); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 获取登录凭证 | ||||
|      * | ||||
|      * @param user       用户信息 | ||||
|      * @param clientResp 客户端信息 | ||||
|      * @return token 认证信息 | ||||
|      */ | ||||
|     @Override | ||||
|     public String authCertificate(UserDO user, ClientResp clientResp) { | ||||
|         return super.authCertificate(user, clientResp); | ||||
|     } | ||||
| } | ||||
| @@ -18,6 +18,7 @@ package top.continew.admin.auth.handler; | ||||
|  | ||||
| import cn.dev33.satoken.stp.StpUtil; | ||||
| import cn.hutool.core.bean.BeanUtil; | ||||
| import cn.hutool.core.collection.CollUtil; | ||||
| import cn.hutool.core.util.IdUtil; | ||||
| import cn.hutool.core.util.RandomUtil; | ||||
| import cn.hutool.core.util.ReUtil; | ||||
| @@ -30,84 +31,59 @@ import me.zhyd.oauth.model.AuthResponse; | ||||
| import me.zhyd.oauth.model.AuthUser; | ||||
| import me.zhyd.oauth.request.AuthRequest; | ||||
| import org.springframework.stereotype.Component; | ||||
| import top.continew.admin.auth.AuthHandler; | ||||
| import top.continew.admin.auth.AbstractAuthHandler; | ||||
| import top.continew.admin.auth.enums.AuthTypeEnum; | ||||
| import top.continew.admin.auth.model.req.SocialAuthReq; | ||||
| import top.continew.admin.auth.model.resp.LoginResp; | ||||
| import top.continew.admin.auth.service.LoginService; | ||||
| import top.continew.admin.common.constant.RegexConstants; | ||||
| import top.continew.admin.common.constant.SysConstants; | ||||
| import top.continew.admin.common.enums.GenderEnum; | ||||
| import top.continew.admin.system.enums.MessageTemplateEnum; | ||||
| import top.continew.admin.system.enums.MessageTypeEnum; | ||||
| import top.continew.admin.system.model.entity.RoleDO; | ||||
| import top.continew.admin.system.model.entity.UserDO; | ||||
| import top.continew.admin.system.model.entity.UserSocialDO; | ||||
| import top.continew.admin.system.model.req.MessageReq; | ||||
| import top.continew.admin.system.model.resp.ClientResp; | ||||
| import top.continew.admin.system.service.RoleService; | ||||
| import top.continew.admin.system.service.MessageService; | ||||
| import top.continew.admin.system.service.UserRoleService; | ||||
| import top.continew.admin.system.service.UserService; | ||||
| import top.continew.admin.system.service.UserSocialService; | ||||
| import top.continew.starter.core.autoconfigure.project.ProjectProperties; | ||||
| import top.continew.starter.core.exception.BadRequestException; | ||||
| import top.continew.starter.core.validation.ValidationUtils; | ||||
| import top.continew.starter.messaging.websocket.util.WebSocketUtils; | ||||
|  | ||||
| import java.time.LocalDateTime; | ||||
| import java.util.Collections; | ||||
| import java.util.List; | ||||
|  | ||||
| /** | ||||
|  * 手机号认证处理器 | ||||
|  * 第三方账号认证处理器 | ||||
|  * | ||||
|  * @author KAI | ||||
|  * @since 2024/12/25 14:21 | ||||
|  */ | ||||
| @Component | ||||
| @RequiredArgsConstructor | ||||
| public class SocialAuthHandler extends AbstractAuthHandler implements AuthHandler<SocialAuthReq, LoginResp> { | ||||
| public class SocialAuthHandler extends AbstractAuthHandler<SocialAuthReq> { | ||||
|  | ||||
|     private final AuthRequestFactory authRequestFactory; | ||||
|     private final UserSocialService userSocialService; | ||||
|     private final UserService userService; | ||||
|     private final RoleService roleService; | ||||
|     private final UserRoleService userRoleService; | ||||
|     private final LoginService loginService; | ||||
|     private final MessageService messageService; | ||||
|     private final ProjectProperties projectProperties; | ||||
|  | ||||
|     /** | ||||
|      * 获取认证类型 | ||||
|      * | ||||
|      * @return 第三方认证类型 | ||||
|      */ | ||||
|     @Override | ||||
|     public AuthTypeEnum getAuthType() { | ||||
|         return AuthTypeEnum.SOCIAL_AUTH; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 校验第三方登录请求对象 | ||||
|      * | ||||
|      * @param authReq 登录请求参数 | ||||
|      */ | ||||
|     @Override | ||||
|     public void validate(SocialAuthReq authReq) { | ||||
|         ValidationUtils.validate(authReq); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 第三方登录 | ||||
|      * | ||||
|      * @param authReq 第三方登录请求对象 | ||||
|      * @param request HTTP请求对象 | ||||
|      * @return 登录响应 | ||||
|      */ | ||||
|     @Override | ||||
|     public LoginResp login(SocialAuthReq authReq, ClientResp clientResp, HttpServletRequest request) { | ||||
|         this.validate(authReq); | ||||
|         if (StpUtil.isLogin()) { | ||||
|             StpUtil.logout(); | ||||
|         } | ||||
|         AuthRequest authRequest = this.getAuthRequest(authReq.getSource()); | ||||
|     public LoginResp login(SocialAuthReq req, ClientResp client, HttpServletRequest request) { | ||||
|         // 获取第三方登录信息 | ||||
|         AuthRequest authRequest = this.getAuthRequest(req.getSource()); | ||||
|         AuthCallback callback = new AuthCallback(); | ||||
|         callback.setCode(authReq.getCode()); | ||||
|         callback.setState(authReq.getState()); | ||||
|         callback.setCode(req.getCode()); | ||||
|         callback.setState(req.getState()); | ||||
|         AuthResponse<AuthUser> response = authRequest.login(callback); | ||||
|         ValidationUtils.throwIf(!response.ok(), response.getMsg()); | ||||
|         AuthUser authUser = response.getData(); | ||||
|         // 如未绑定则自动注册新用户,保存或更新关联信息 | ||||
|         String source = authUser.getSource(); | ||||
|         String openId = authUser.getUuid(); | ||||
|         UserSocialDO userSocial = userSocialService.getBySourceAndOpenId(source, openId); | ||||
| @@ -136,19 +112,39 @@ public class SocialAuthHandler extends AbstractAuthHandler implements AuthHandle | ||||
|             userSocial.setUserId(userId); | ||||
|             userSocial.setSource(source); | ||||
|             userSocial.setOpenId(openId); | ||||
|             loginService.sendSecurityMsg(user); | ||||
|             this.sendSecurityMsg(user); | ||||
|         } else { | ||||
|             user = BeanUtil.copyProperties(userService.getById(userSocial.getUserId()), UserDO.class); | ||||
|         } | ||||
|         loginService.checkUserStatus(user); | ||||
|         // 检查用户状态 | ||||
|         super.checkUserStatus(user); | ||||
|         userSocial.setMetaJson(JSONUtil.toJsonStr(authUser)); | ||||
|         userSocial.setLastLoginTime(LocalDateTime.now()); | ||||
|         userSocialService.saveOrUpdate(userSocial); | ||||
|         // 执行登录 | ||||
|         String token = this.authCertificate(user, clientResp); | ||||
|         // 执行认证 | ||||
|         String token = super.authenticate(user, client); | ||||
|         return LoginResp.builder().token(token).build(); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public void preLogin(SocialAuthReq req, ClientResp client, HttpServletRequest request) { | ||||
|         super.preLogin(req, client, request); | ||||
|         if (StpUtil.isLogin()) { | ||||
|             StpUtil.logout(); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public AuthTypeEnum getAuthType() { | ||||
|         return AuthTypeEnum.SOCIAL; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 获取 AuthRequest | ||||
|      * | ||||
|      * @param source 平台名称 | ||||
|      * @return AuthRequest | ||||
|      */ | ||||
|     private AuthRequest getAuthRequest(String source) { | ||||
|         try { | ||||
|             return authRequestFactory.get(source); | ||||
| @@ -158,15 +154,20 @@ public class SocialAuthHandler extends AbstractAuthHandler implements AuthHandle | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 获取认证信息 | ||||
|      * 发送安全消息 | ||||
|      * | ||||
|      * @param user       用户信息 | ||||
|      * @param clientResp 客户端信息 | ||||
|      * @return 认证信息 | ||||
|      * @param user 用户信息 | ||||
|      */ | ||||
|     @Override | ||||
|     protected String authCertificate(UserDO user, ClientResp clientResp) { | ||||
|         return super.authCertificate(user, clientResp); | ||||
|     private void sendSecurityMsg(UserDO user) { | ||||
|         MessageReq req = new MessageReq(); | ||||
|         MessageTemplateEnum socialRegister = MessageTemplateEnum.SOCIAL_REGISTER; | ||||
|         req.setTitle(socialRegister.getTitle().formatted(projectProperties.getName())); | ||||
|         req.setContent(socialRegister.getContent().formatted(user.getNickname())); | ||||
|         req.setType(MessageTypeEnum.SECURITY); | ||||
|         messageService.add(req, CollUtil.toList(user.getId())); | ||||
|         List<String> tokenList = StpUtil.getTokenValueListByLoginId(user.getId()); | ||||
|         for (String token : tokenList) { | ||||
|             WebSocketUtils.sendMessage(token, "1"); | ||||
|         } | ||||
|     } | ||||
|  | ||||
| } | ||||
|   | ||||
| @@ -21,17 +21,16 @@ import jakarta.validation.constraints.NotBlank; | ||||
| import lombok.Data; | ||||
|  | ||||
| import java.io.Serial; | ||||
| import java.io.Serializable; | ||||
|  | ||||
| /** | ||||
|  * 账号登录参数 | ||||
|  * 账号认证参数 | ||||
|  * | ||||
|  * @author Charles7c | ||||
|  * @since 2022/12/21 20:43 | ||||
|  */ | ||||
| @Data | ||||
| @Schema(description = "账号登录参数") | ||||
| public class AccountAuthReq extends AuthReq implements Serializable { | ||||
| @Schema(description = "账号认证参数") | ||||
| public class AccountAuthReq extends AuthReq { | ||||
|  | ||||
|     @Serial | ||||
|     private static final long serialVersionUID = 1L; | ||||
|   | ||||
| @@ -20,27 +20,42 @@ import com.fasterxml.jackson.annotation.JsonSubTypes; | ||||
| import com.fasterxml.jackson.annotation.JsonTypeInfo; | ||||
| import io.swagger.v3.oas.annotations.media.Schema; | ||||
| import jakarta.validation.constraints.NotBlank; | ||||
| import jakarta.validation.constraints.NotNull; | ||||
| import lombok.Data; | ||||
| import top.continew.admin.auth.enums.AuthTypeEnum; | ||||
|  | ||||
| import java.io.Serial; | ||||
| import java.io.Serializable; | ||||
|  | ||||
| /** | ||||
|  * 登录参数基础类 | ||||
|  * 认证参数基类 | ||||
|  * | ||||
|  * @author KAI | ||||
|  * @author Charles7c | ||||
|  * @since 2024/12/22 15:16 | ||||
|  */ | ||||
| @Data | ||||
| @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.EXISTING_PROPERTY, property = "authType", visible = true) | ||||
| @JsonSubTypes({@JsonSubTypes.Type(value = AccountAuthReq.class, name = "account"), | ||||
|     @JsonSubTypes.Type(value = EmailAuthReq.class, name = "email"), | ||||
|     @JsonSubTypes.Type(value = PhoneAuthReq.class, name = "phone"), | ||||
|     @JsonSubTypes.Type(value = SocialAuthReq.class, name = "socialAuth")}) | ||||
| public abstract class AuthReq { | ||||
| @JsonSubTypes({@JsonSubTypes.Type(value = AccountAuthReq.class, name = "ACCOUNT"), | ||||
|     @JsonSubTypes.Type(value = EmailAuthReq.class, name = "EMAIL"), | ||||
|     @JsonSubTypes.Type(value = PhoneAuthReq.class, name = "PHONE"), | ||||
|     @JsonSubTypes.Type(value = SocialAuthReq.class, name = "SOCIAL")}) | ||||
| public class AuthReq implements Serializable { | ||||
|  | ||||
|     @Schema(description = "客户端id") | ||||
|     @NotBlank(message = "客户端id不能为空") | ||||
|     @Serial | ||||
|     private static final long serialVersionUID = 1L; | ||||
|  | ||||
|     /** | ||||
|      * 客户端 ID | ||||
|      */ | ||||
|     @Schema(description = "客户端 ID") | ||||
|     @NotBlank(message = "客户端ID不能为空") | ||||
|     private String clientId; | ||||
|  | ||||
|     /** | ||||
|      * 认证类型 | ||||
|      */ | ||||
|     @Schema(description = "认证类型") | ||||
|     @NotBlank(message = "认证类型不能为空") | ||||
|     private String authType; | ||||
|     @NotNull(message = "认证类型非法") | ||||
|     private AuthTypeEnum authType; | ||||
| } | ||||
|   | ||||
| @@ -24,17 +24,16 @@ import lombok.Data; | ||||
| import org.hibernate.validator.constraints.Length; | ||||
|  | ||||
| import java.io.Serial; | ||||
| import java.io.Serializable; | ||||
|  | ||||
| /** | ||||
|  * 邮箱登录参数 | ||||
|  * 邮箱认证参数 | ||||
|  * | ||||
|  * @author Charles7c | ||||
|  * @since 2023/10/23 20:15 | ||||
|  */ | ||||
| @Data | ||||
| @Schema(description = "邮箱登录参数") | ||||
| public class EmailAuthReq extends AuthReq implements Serializable { | ||||
| @Schema(description = "邮箱认证参数") | ||||
| public class EmailAuthReq extends AuthReq { | ||||
|  | ||||
|     @Serial | ||||
|     private static final long serialVersionUID = 1L; | ||||
|   | ||||
| @@ -24,17 +24,16 @@ import lombok.Data; | ||||
| import org.hibernate.validator.constraints.Length; | ||||
|  | ||||
| import java.io.Serial; | ||||
| import java.io.Serializable; | ||||
|  | ||||
| /** | ||||
|  * 手机号登录参数 | ||||
|  * 手机号认证参数 | ||||
|  * | ||||
|  * @author Charles7c | ||||
|  * @since 2023/10/26 22:37 | ||||
|  */ | ||||
| @Data | ||||
| @Schema(description = "手机号登录参数") | ||||
| public class PhoneAuthReq extends AuthReq implements Serializable { | ||||
| @Schema(description = "手机号认证参数") | ||||
| public class PhoneAuthReq extends AuthReq { | ||||
|  | ||||
|     @Serial | ||||
|     private static final long serialVersionUID = 1L; | ||||
|   | ||||
| @@ -20,30 +20,40 @@ import io.swagger.v3.oas.annotations.media.Schema; | ||||
| import jakarta.validation.constraints.NotBlank; | ||||
| import lombok.Data; | ||||
|  | ||||
| import java.io.Serial; | ||||
|  | ||||
| /** | ||||
|  * 第三方登录参数 | ||||
|  * 第三方账号认证参数 | ||||
|  * | ||||
|  * @author KAI | ||||
|  * @author Charles7c | ||||
|  * @since 2024/12/25 15:43 | ||||
|  */ | ||||
| @Data | ||||
| @Schema(description = "第三方登录参数") | ||||
| @Schema(description = "第三方账号认证参数") | ||||
| public class SocialAuthReq extends AuthReq { | ||||
|  | ||||
|     @Serial | ||||
|     private static final long serialVersionUID = 1L; | ||||
|  | ||||
|     /** | ||||
|      * 第三方登录平台 | ||||
|      */ | ||||
|     @Schema(description = "第三方登录平台", example = "gitee") | ||||
|     @NotBlank(message = "第三方登录平台不能为空") | ||||
|     private String source; | ||||
|  | ||||
|     /** | ||||
|      * 第三方登录code | ||||
|      * 授权码 | ||||
|      */ | ||||
|     @NotBlank(message = "第三方登录code不能为空") | ||||
|     @Schema(description = "授权码", example = "a08d33e9e577fb339de027499784ed4e871d6f62ae65b459153e906ab546bd56") | ||||
|     @NotBlank(message = "授权码不能为空") | ||||
|     private String code; | ||||
|  | ||||
|     /** | ||||
|      * 第三方登录state | ||||
|      * 状态码 | ||||
|      */ | ||||
|     @NotBlank(message = "第三方登录state不能为空") | ||||
|     @Schema(description = "状态码", example = "2ca8d8baf437eb374efaa1191a3d") | ||||
|     @NotBlank(message = "状态码不能为空") | ||||
|     private String state; | ||||
| } | ||||
|   | ||||
| @@ -24,14 +24,14 @@ import java.io.Serial; | ||||
| import java.io.Serializable; | ||||
|  | ||||
| /** | ||||
|  * 令牌信息 | ||||
|  * 登录响应参数 | ||||
|  * | ||||
|  * @author Charles7c | ||||
|  * @since 2022/12/21 20:42 | ||||
|  */ | ||||
| @Data | ||||
| @Builder | ||||
| @Schema(description = "令牌信息") | ||||
| @Schema(description = "登录响应参数") | ||||
| public class LoginResp implements Serializable { | ||||
|  | ||||
|     @Serial | ||||
|   | ||||
| @@ -17,51 +17,28 @@ | ||||
| package top.continew.admin.auth.service; | ||||
| 
 | ||||
| import jakarta.servlet.http.HttpServletRequest; | ||||
| import me.zhyd.oauth.model.AuthUser; | ||||
| import top.continew.admin.auth.model.req.AuthReq; | ||||
| import top.continew.admin.auth.model.resp.LoginResp; | ||||
| import top.continew.admin.auth.model.resp.RouteResp; | ||||
| import top.continew.admin.system.model.entity.UserDO; | ||||
| 
 | ||||
| import java.util.List; | ||||
| 
 | ||||
| /** | ||||
|  * 登录业务接口 | ||||
|  * 认证业务接口 | ||||
|  * | ||||
|  * @author Charles7c | ||||
|  * @since 2022/12/21 21:48 | ||||
|  */ | ||||
| public interface LoginService { | ||||
| public interface AuthService { | ||||
| 
 | ||||
|     /** | ||||
|      * 检查用户状态 | ||||
|      * 登录 | ||||
|      * | ||||
|      * @param user 用户信息 | ||||
|      * @param req     登录请求参数 | ||||
|      * @param request 请求对象 | ||||
|      * @return 登录响应参数 | ||||
|      */ | ||||
|     void checkUserStatus(UserDO user); | ||||
| 
 | ||||
|     /** | ||||
|      * 检查用户是否被锁定 | ||||
|      * | ||||
|      * @param username 用户名 | ||||
|      * @param request  请求对象 | ||||
|      * @param isError  是否登录错误 | ||||
|      */ | ||||
|     void checkUserLocked(String username, HttpServletRequest request, boolean isError); | ||||
| 
 | ||||
|     /** | ||||
|      * 执行登录操作 | ||||
|      * | ||||
|      * @param user 用户信息 | ||||
|      * @return token | ||||
|      */ | ||||
|     String login(UserDO user); | ||||
| 
 | ||||
|     /** | ||||
|      * 三方账号登录 | ||||
|      * | ||||
|      * @param authUser 三方账号信息 | ||||
|      * @return 令牌 | ||||
|      */ | ||||
|     String socialLogin(AuthUser authUser); | ||||
|     LoginResp login(AuthReq req, HttpServletRequest request); | ||||
| 
 | ||||
|     /** | ||||
|      * 构建路由树 | ||||
| @@ -70,11 +47,4 @@ public interface LoginService { | ||||
|      * @return 路由树 | ||||
|      */ | ||||
|     List<RouteResp> buildRouteTree(Long userId); | ||||
| 
 | ||||
|     /** | ||||
|      * 发送安全消息 | ||||
|      *  | ||||
|      * @param user 用户信息 | ||||
|      */ | ||||
|     void sendSecurityMsg(UserDO user); | ||||
| } | ||||
| @@ -0,0 +1,125 @@ | ||||
| /* | ||||
|  * Copyright (c) 2022-present Charles7c Authors. All Rights Reserved. | ||||
|  * | ||||
|  * Licensed under the Apache License, Version 2.0 (the "License"); | ||||
|  * you may not use this file except in compliance with the License. | ||||
|  * You may obtain a copy of the License at | ||||
|  * | ||||
|  *     http://www.apache.org/licenses/LICENSE-2.0 | ||||
|  * | ||||
|  * Unless required by applicable law or agreed to in writing, software | ||||
|  * distributed under the License is distributed on an "AS IS" BASIS, | ||||
|  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
|  * See the License for the specific language governing permissions and | ||||
|  * limitations under the License. | ||||
|  */ | ||||
|  | ||||
| package top.continew.admin.auth.service.impl; | ||||
|  | ||||
| import cn.hutool.core.bean.BeanUtil; | ||||
| import cn.hutool.core.collection.CollUtil; | ||||
| import cn.hutool.core.lang.tree.Tree; | ||||
| import cn.hutool.core.lang.tree.TreeNodeConfig; | ||||
| import cn.hutool.core.lang.tree.TreeUtil; | ||||
| import jakarta.servlet.http.HttpServletRequest; | ||||
| import lombok.RequiredArgsConstructor; | ||||
| import org.springframework.stereotype.Service; | ||||
| import top.continew.admin.auth.AuthHandler; | ||||
| import top.continew.admin.auth.AuthStrategy; | ||||
| import top.continew.admin.auth.enums.AuthTypeEnum; | ||||
| import top.continew.admin.auth.model.req.AuthReq; | ||||
| import top.continew.admin.auth.model.resp.LoginResp; | ||||
| import top.continew.admin.auth.model.resp.RouteResp; | ||||
| import top.continew.admin.auth.service.AuthService; | ||||
| import top.continew.admin.common.constant.SysConstants; | ||||
| import top.continew.admin.common.enums.DisEnableStatusEnum; | ||||
| import top.continew.admin.system.enums.MenuTypeEnum; | ||||
| import top.continew.admin.system.model.resp.ClientResp; | ||||
| import top.continew.admin.system.model.resp.MenuResp; | ||||
| import top.continew.admin.system.service.ClientService; | ||||
| import top.continew.admin.system.service.MenuService; | ||||
| import top.continew.admin.system.service.RoleService; | ||||
| import top.continew.starter.core.validation.ValidationUtils; | ||||
| import top.continew.starter.extension.crud.annotation.TreeField; | ||||
| import top.continew.starter.extension.crud.autoconfigure.CrudProperties; | ||||
|  | ||||
| import java.util.ArrayList; | ||||
| import java.util.LinkedHashSet; | ||||
| import java.util.List; | ||||
| import java.util.Set; | ||||
|  | ||||
| /** | ||||
|  * 认证业务实现 | ||||
|  * | ||||
|  * @author Charles7c | ||||
|  * @since 2022/12/21 21:49 | ||||
|  */ | ||||
| @Service | ||||
| @RequiredArgsConstructor | ||||
| public class AuthServiceImpl implements AuthService { | ||||
|  | ||||
|     private final AuthStrategy authStrategy; | ||||
|     private final ClientService clientService; | ||||
|     private final RoleService roleService; | ||||
|     private final MenuService menuService; | ||||
|     private final CrudProperties crudProperties; | ||||
|  | ||||
|     @Override | ||||
|     public LoginResp login(AuthReq req, HttpServletRequest request) { | ||||
|         AuthTypeEnum authType = req.getAuthType(); | ||||
|         // 校验客户端 | ||||
|         ClientResp client = clientService.getByClientId(req.getClientId()); | ||||
|         ValidationUtils.throwIfNull(client, "客户端不存在"); | ||||
|         ValidationUtils.throwIf(DisEnableStatusEnum.DISABLE.equals(client.getStatus()), "客户端已禁用"); | ||||
|         ValidationUtils.throwIf(!client.getAuthType().contains(authType.getValue()), "该客户端暂未授权 [{}] 认证", authType | ||||
|             .getDescription()); | ||||
|         // 获取处理器 | ||||
|         AuthHandler<AuthReq> authHandler = authStrategy.getHandler(authType); | ||||
|         // 登录前置处理 | ||||
|         authHandler.preLogin(req, client, request); | ||||
|         // 登录 | ||||
|         LoginResp loginResp = authHandler.login(req, client, request); | ||||
|         // 登录后置处理 | ||||
|         authHandler.postLogin(req, client, request); | ||||
|         return loginResp; | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public List<RouteResp> buildRouteTree(Long userId) { | ||||
|         Set<String> roleCodeSet = roleService.listCodeByUserId(userId); | ||||
|         if (CollUtil.isEmpty(roleCodeSet)) { | ||||
|             return new ArrayList<>(0); | ||||
|         } | ||||
|         // 查询菜单列表 | ||||
|         Set<MenuResp> menuSet = new LinkedHashSet<>(); | ||||
|         if (roleCodeSet.contains(SysConstants.SUPER_ROLE_CODE)) { | ||||
|             menuSet.addAll(menuService.listAll()); | ||||
|         } else { | ||||
|             roleCodeSet.forEach(roleCode -> menuSet.addAll(menuService.listByRoleCode(roleCode))); | ||||
|         } | ||||
|         List<MenuResp> menuList = menuSet.stream().filter(m -> !MenuTypeEnum.BUTTON.equals(m.getType())).toList(); | ||||
|         if (CollUtil.isEmpty(menuList)) { | ||||
|             return new ArrayList<>(0); | ||||
|         } | ||||
|         // 构建路由树 | ||||
|         TreeField treeField = MenuResp.class.getDeclaredAnnotation(TreeField.class); | ||||
|         TreeNodeConfig treeNodeConfig = crudProperties.getTree().genTreeNodeConfig(treeField); | ||||
|         List<Tree<Long>> treeList = TreeUtil.build(menuList, treeField.rootId(), treeNodeConfig, (m, tree) -> { | ||||
|             tree.setId(m.getId()); | ||||
|             tree.setParentId(m.getParentId()); | ||||
|             tree.setName(m.getTitle()); | ||||
|             tree.setWeight(m.getSort()); | ||||
|             tree.putExtra("type", m.getType().getValue()); | ||||
|             tree.putExtra("path", m.getPath()); | ||||
|             tree.putExtra("name", m.getName()); | ||||
|             tree.putExtra("component", m.getComponent()); | ||||
|             tree.putExtra("redirect", m.getRedirect()); | ||||
|             tree.putExtra("icon", m.getIcon()); | ||||
|             tree.putExtra("isExternal", m.getIsExternal()); | ||||
|             tree.putExtra("isCache", m.getIsCache()); | ||||
|             tree.putExtra("isHidden", m.getIsHidden()); | ||||
|             tree.putExtra("permission", m.getPermission()); | ||||
|         }); | ||||
|         return BeanUtil.copyToList(treeList, RouteResp.class); | ||||
|     } | ||||
| } | ||||
| @@ -1,267 +0,0 @@ | ||||
| /* | ||||
|  * Copyright (c) 2022-present Charles7c Authors. All Rights Reserved. | ||||
|  * | ||||
|  * Licensed under the Apache License, Version 2.0 (the "License"); | ||||
|  * you may not use this file except in compliance with the License. | ||||
|  * You may obtain a copy of the License at | ||||
|  * | ||||
|  *     http://www.apache.org/licenses/LICENSE-2.0 | ||||
|  * | ||||
|  * Unless required by applicable law or agreed to in writing, software | ||||
|  * distributed under the License is distributed on an "AS IS" BASIS, | ||||
|  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
|  * See the License for the specific language governing permissions and | ||||
|  * limitations under the License. | ||||
|  */ | ||||
|  | ||||
| package top.continew.admin.auth.service.impl; | ||||
|  | ||||
| import cn.dev33.satoken.stp.SaLoginConfig; | ||||
| import cn.dev33.satoken.stp.StpUtil; | ||||
| import cn.hutool.core.bean.BeanUtil; | ||||
| import cn.hutool.core.collection.CollUtil; | ||||
| import cn.hutool.core.lang.tree.Tree; | ||||
| import cn.hutool.core.lang.tree.TreeNodeConfig; | ||||
| import cn.hutool.core.lang.tree.TreeUtil; | ||||
| import cn.hutool.core.util.IdUtil; | ||||
| import cn.hutool.core.util.ObjectUtil; | ||||
| import cn.hutool.core.util.RandomUtil; | ||||
| import cn.hutool.core.util.ReUtil; | ||||
| import cn.hutool.extra.servlet.JakartaServletUtil; | ||||
| import cn.hutool.json.JSONUtil; | ||||
| import jakarta.servlet.http.HttpServletRequest; | ||||
| import lombok.RequiredArgsConstructor; | ||||
| import me.zhyd.oauth.model.AuthUser; | ||||
| import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; | ||||
| import org.springframework.security.crypto.password.PasswordEncoder; | ||||
| import org.springframework.stereotype.Service; | ||||
| import org.springframework.transaction.annotation.Transactional; | ||||
| import top.continew.admin.auth.model.resp.RouteResp; | ||||
| import top.continew.admin.auth.service.LoginService; | ||||
| import top.continew.admin.common.constant.CacheConstants; | ||||
| import top.continew.admin.common.constant.RegexConstants; | ||||
| import top.continew.admin.common.constant.SysConstants; | ||||
| import top.continew.admin.common.context.RoleContext; | ||||
| import top.continew.admin.common.context.UserContext; | ||||
| import top.continew.admin.common.context.UserContextHolder; | ||||
| import top.continew.admin.common.context.UserExtraContext; | ||||
| import top.continew.admin.common.enums.DisEnableStatusEnum; | ||||
| import top.continew.admin.common.enums.GenderEnum; | ||||
| import top.continew.admin.system.enums.MenuTypeEnum; | ||||
| import top.continew.admin.system.enums.MessageTemplateEnum; | ||||
| import top.continew.admin.system.enums.MessageTypeEnum; | ||||
| import top.continew.admin.system.enums.PasswordPolicyEnum; | ||||
| import top.continew.admin.system.model.entity.DeptDO; | ||||
| import top.continew.admin.system.model.entity.RoleDO; | ||||
| import top.continew.admin.system.model.entity.UserDO; | ||||
| import top.continew.admin.system.model.entity.UserSocialDO; | ||||
| import top.continew.admin.system.model.req.MessageReq; | ||||
| import top.continew.admin.system.model.resp.MenuResp; | ||||
| import top.continew.admin.system.service.*; | ||||
| import top.continew.starter.cache.redisson.util.RedisUtils; | ||||
| import top.continew.starter.core.autoconfigure.project.ProjectProperties; | ||||
| import top.continew.starter.core.validation.CheckUtils; | ||||
| import top.continew.starter.extension.crud.annotation.TreeField; | ||||
| import top.continew.starter.extension.crud.autoconfigure.CrudProperties; | ||||
| import top.continew.starter.messaging.websocket.util.WebSocketUtils; | ||||
| import top.continew.starter.web.util.SpringWebUtils; | ||||
|  | ||||
| import java.time.Duration; | ||||
| import java.time.LocalDateTime; | ||||
| import java.util.*; | ||||
| import java.util.concurrent.CompletableFuture; | ||||
|  | ||||
| import static top.continew.admin.system.enums.PasswordPolicyEnum.PASSWORD_EXPIRATION_DAYS; | ||||
|  | ||||
| /** | ||||
|  * 登录业务实现 | ||||
|  * | ||||
|  * @author Charles7c | ||||
|  * @since 2022/12/21 21:49 | ||||
|  */ | ||||
| @Service | ||||
| @RequiredArgsConstructor | ||||
| public class LoginServiceImpl implements LoginService { | ||||
|  | ||||
|     private final ProjectProperties projectProperties; | ||||
|     private final CrudProperties crudProperties; | ||||
|     private final PasswordEncoder passwordEncoder; | ||||
|     private final ThreadPoolTaskExecutor threadPoolTaskExecutor; | ||||
|     private final UserService userService; | ||||
|     private final DeptService deptService; | ||||
|     private final RoleService roleService; | ||||
|     private final MenuService menuService; | ||||
|     private final UserRoleService userRoleService; | ||||
|     private final UserSocialService userSocialService; | ||||
|     private final OptionService optionService; | ||||
|     private final MessageService messageService; | ||||
|  | ||||
|     @Override | ||||
|     @Transactional(rollbackFor = Exception.class) | ||||
|     public String socialLogin(AuthUser authUser) { | ||||
|         String source = authUser.getSource(); | ||||
|         String openId = authUser.getUuid(); | ||||
|         UserSocialDO userSocial = userSocialService.getBySourceAndOpenId(source, openId); | ||||
|         UserDO user; | ||||
|         if (null == userSocial) { | ||||
|             String username = authUser.getUsername(); | ||||
|             String nickname = authUser.getNickname(); | ||||
|             UserDO existsUser = userService.getByUsername(username); | ||||
|             String randomStr = RandomUtil.randomString(RandomUtil.BASE_CHAR, 5); | ||||
|             if (null != existsUser || !ReUtil.isMatch(RegexConstants.USERNAME, username)) { | ||||
|                 username = randomStr + IdUtil.fastSimpleUUID(); | ||||
|             } | ||||
|             if (!ReUtil.isMatch(RegexConstants.GENERAL_NAME, nickname)) { | ||||
|                 nickname = source.toLowerCase() + randomStr; | ||||
|             } | ||||
|             user = new UserDO(); | ||||
|             user.setUsername(username); | ||||
|             user.setNickname(nickname); | ||||
|             user.setGender(GenderEnum.valueOf(authUser.getGender().name())); | ||||
|             user.setAvatar(authUser.getAvatar()); | ||||
|             user.setDeptId(SysConstants.SUPER_DEPT_ID); | ||||
|             Long userId = userService.add(user); | ||||
|             RoleDO role = roleService.getByCode(SysConstants.SUPER_ROLE_CODE); | ||||
|             userRoleService.assignRolesToUser(Collections.singletonList(role.getId()), userId); | ||||
|             userSocial = new UserSocialDO(); | ||||
|             userSocial.setUserId(userId); | ||||
|             userSocial.setSource(source); | ||||
|             userSocial.setOpenId(openId); | ||||
|             this.sendSecurityMsg(user); | ||||
|         } else { | ||||
|             user = BeanUtil.copyProperties(userService.getById(userSocial.getUserId()), UserDO.class); | ||||
|         } | ||||
|         this.checkUserStatus(user); | ||||
|         userSocial.setMetaJson(JSONUtil.toJsonStr(authUser)); | ||||
|         userSocial.setLastLoginTime(LocalDateTime.now()); | ||||
|         userSocialService.saveOrUpdate(userSocial); | ||||
|         return this.login(user); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public List<RouteResp> buildRouteTree(Long userId) { | ||||
|         Set<String> roleCodeSet = roleService.listCodeByUserId(userId); | ||||
|         if (CollUtil.isEmpty(roleCodeSet)) { | ||||
|             return new ArrayList<>(0); | ||||
|         } | ||||
|         // 查询菜单列表 | ||||
|         Set<MenuResp> menuSet = new LinkedHashSet<>(); | ||||
|         if (roleCodeSet.contains(SysConstants.SUPER_ROLE_CODE)) { | ||||
|             menuSet.addAll(menuService.listAll()); | ||||
|         } else { | ||||
|             roleCodeSet.forEach(roleCode -> menuSet.addAll(menuService.listByRoleCode(roleCode))); | ||||
|         } | ||||
|         List<MenuResp> menuList = menuSet.stream().filter(m -> !MenuTypeEnum.BUTTON.equals(m.getType())).toList(); | ||||
|         if (CollUtil.isEmpty(menuList)) { | ||||
|             return new ArrayList<>(0); | ||||
|         } | ||||
|         // 构建路由树 | ||||
|         TreeField treeField = MenuResp.class.getDeclaredAnnotation(TreeField.class); | ||||
|         TreeNodeConfig treeNodeConfig = crudProperties.getTree().genTreeNodeConfig(treeField); | ||||
|         List<Tree<Long>> treeList = TreeUtil.build(menuList, treeField.rootId(), treeNodeConfig, (m, tree) -> { | ||||
|             tree.setId(m.getId()); | ||||
|             tree.setParentId(m.getParentId()); | ||||
|             tree.setName(m.getTitle()); | ||||
|             tree.setWeight(m.getSort()); | ||||
|             tree.putExtra("type", m.getType().getValue()); | ||||
|             tree.putExtra("path", m.getPath()); | ||||
|             tree.putExtra("name", m.getName()); | ||||
|             tree.putExtra("component", m.getComponent()); | ||||
|             tree.putExtra("redirect", m.getRedirect()); | ||||
|             tree.putExtra("icon", m.getIcon()); | ||||
|             tree.putExtra("isExternal", m.getIsExternal()); | ||||
|             tree.putExtra("isCache", m.getIsCache()); | ||||
|             tree.putExtra("isHidden", m.getIsHidden()); | ||||
|             tree.putExtra("permission", m.getPermission()); | ||||
|         }); | ||||
|         return BeanUtil.copyToList(treeList, RouteResp.class); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 登录并缓存用户信息 | ||||
|      * | ||||
|      * @param user 用户信息 | ||||
|      * @return 令牌 | ||||
|      */ | ||||
|     @Override | ||||
|     public String login(UserDO user) { | ||||
|         Long userId = user.getId(); | ||||
|         CompletableFuture<Set<String>> permissionFuture = CompletableFuture.supplyAsync(() -> roleService | ||||
|             .listPermissionByUserId(userId), threadPoolTaskExecutor); | ||||
|         CompletableFuture<Set<RoleContext>> roleFuture = CompletableFuture.supplyAsync(() -> roleService | ||||
|             .listByUserId(userId), threadPoolTaskExecutor); | ||||
|         CompletableFuture<Integer> passwordExpirationDaysFuture = CompletableFuture.supplyAsync(() -> optionService | ||||
|             .getValueByCode2Int(PASSWORD_EXPIRATION_DAYS.name())); | ||||
|         CompletableFuture.allOf(permissionFuture, roleFuture, passwordExpirationDaysFuture); | ||||
|         UserContext userContext = new UserContext(permissionFuture.join(), roleFuture | ||||
|             .join(), passwordExpirationDaysFuture.join()); | ||||
|         BeanUtil.copyProperties(user, userContext); | ||||
|         // 登录并缓存用户信息 | ||||
|         StpUtil.login(userContext.getId(), SaLoginConfig.setExtraData(BeanUtil | ||||
|             .beanToMap(new UserExtraContext(SpringWebUtils.getRequest())))); | ||||
|         UserContextHolder.setContext(userContext); | ||||
|         return StpUtil.getTokenValue(); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 检查用户状态 | ||||
|      * | ||||
|      * @param user 用户信息 | ||||
|      */ | ||||
|     @Override | ||||
|     public void checkUserStatus(UserDO user) { | ||||
|         CheckUtils.throwIfEqual(DisEnableStatusEnum.DISABLE, user.getStatus(), "此账号已被禁用,如有疑问,请联系管理员"); | ||||
|         DeptDO dept = deptService.getById(user.getDeptId()); | ||||
|         CheckUtils.throwIfEqual(DisEnableStatusEnum.DISABLE, dept.getStatus(), "此账号所属部门已被禁用,如有疑问,请联系管理员"); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 检测用户是否已被锁定 | ||||
|      * | ||||
|      * @param username 用户名 | ||||
|      * @param request  请求对象 | ||||
|      * @param isError  是否登录错误 | ||||
|      */ | ||||
|     @Override | ||||
|     public void checkUserLocked(String username, HttpServletRequest request, boolean isError) { | ||||
|         // 不锁定 | ||||
|         int maxErrorCount = optionService.getValueByCode2Int(PasswordPolicyEnum.PASSWORD_ERROR_LOCK_COUNT.name()); | ||||
|         if (maxErrorCount <= SysConstants.NO) { | ||||
|             return; | ||||
|         } | ||||
|         // 检测是否已被锁定 | ||||
|         String key = CacheConstants.USER_PASSWORD_ERROR_KEY_PREFIX + RedisUtils.formatKey(username, JakartaServletUtil | ||||
|             .getClientIP(request)); | ||||
|         int lockMinutes = optionService.getValueByCode2Int(PasswordPolicyEnum.PASSWORD_ERROR_LOCK_MINUTES.name()); | ||||
|         Integer currentErrorCount = ObjectUtil.defaultIfNull(RedisUtils.get(key), 0); | ||||
|         CheckUtils.throwIf(currentErrorCount >= maxErrorCount, "账号锁定 {} 分钟,请稍后再试", lockMinutes); | ||||
|         // 登录成功清除计数 | ||||
|         if (!isError) { | ||||
|             RedisUtils.delete(key); | ||||
|             return; | ||||
|         } | ||||
|         // 登录失败递增计数 | ||||
|         currentErrorCount++; | ||||
|         RedisUtils.set(key, currentErrorCount, Duration.ofMinutes(lockMinutes)); | ||||
|         CheckUtils.throwIf(currentErrorCount >= maxErrorCount, "密码错误已达 {} 次,账号锁定 {} 分钟", maxErrorCount, lockMinutes); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 发送安全消息 | ||||
|      * | ||||
|      * @param user 用户信息 | ||||
|      */ | ||||
|     @Override | ||||
|     public void sendSecurityMsg(UserDO user) { | ||||
|         MessageReq req = new MessageReq(); | ||||
|         MessageTemplateEnum socialRegister = MessageTemplateEnum.SOCIAL_REGISTER; | ||||
|         req.setTitle(socialRegister.getTitle().formatted(projectProperties.getName())); | ||||
|         req.setContent(socialRegister.getContent().formatted(user.getNickname())); | ||||
|         req.setType(MessageTypeEnum.SECURITY); | ||||
|         messageService.add(req, CollUtil.toList(user.getId())); | ||||
|         List<String> tokenList = StpUtil.getTokenValueListByLoginId(user.getId()); | ||||
|         for (String token : tokenList) { | ||||
|             WebSocketUtils.sendMessage(token, "1"); | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -20,9 +20,9 @@ import top.continew.starter.data.mp.base.BaseMapper; | ||||
| import top.continew.admin.system.model.entity.ClientDO; | ||||
|  | ||||
| /** | ||||
|  * 客户端管理 Mapper | ||||
|  * 客户端 Mapper | ||||
|  * | ||||
|  * @author MoChou | ||||
|  * @author KAI | ||||
|  * @since 2024/12/03 16:04 | ||||
|  */ | ||||
| public interface ClientMapper extends BaseMapper<ClientDO> {} | ||||
| @@ -27,9 +27,9 @@ import java.io.Serial; | ||||
| import java.util.List; | ||||
|  | ||||
| /** | ||||
|  * 客户端管理实体 | ||||
|  * 客户端实体 | ||||
|  * | ||||
|  * @author MoChou | ||||
|  * @author KAI | ||||
|  * @since 2024/12/03 16:04 | ||||
|  */ | ||||
| @Data | ||||
| @@ -40,12 +40,12 @@ public class ClientDO extends BaseDO { | ||||
|     private static final long serialVersionUID = 1L; | ||||
|  | ||||
|     /** | ||||
|      * 客户端ID | ||||
|      * 客户端 ID | ||||
|      */ | ||||
|     private String clientId; | ||||
|  | ||||
|     /** | ||||
|      * 客户端Key | ||||
|      * 客户端 Key | ||||
|      */ | ||||
|     private String clientKey; | ||||
|  | ||||
|   | ||||
| @@ -27,13 +27,13 @@ import java.io.Serializable; | ||||
| import java.util.List; | ||||
|  | ||||
| /** | ||||
|  * 客户端管理查询条件 | ||||
|  * 客户端查询条件 | ||||
|  * | ||||
|  * @author MoChou | ||||
|  * @author KAI | ||||
|  * @since 2024/12/03 16:04 | ||||
|  */ | ||||
| @Data | ||||
| @Schema(description = "客户端管理查询条件") | ||||
| @Schema(description = "客户端查询条件") | ||||
| public class ClientQuery implements Serializable { | ||||
|  | ||||
|     @Serial | ||||
| @@ -43,33 +43,30 @@ public class ClientQuery implements Serializable { | ||||
|      * 客户端Key | ||||
|      */ | ||||
|     @Schema(description = "客户端Key") | ||||
|     @Query(type = QueryType.EQ) | ||||
|     private String clientKey; | ||||
|  | ||||
|     /** | ||||
|      * 客户端秘钥 | ||||
|      */ | ||||
|     @Schema(description = "客户端秘钥") | ||||
|     @Query(type = QueryType.EQ) | ||||
|     private String clientSecret; | ||||
|  | ||||
|     /** | ||||
|      * 登录类型 | ||||
|      * 认证类型 | ||||
|      */ | ||||
|     @Schema(description = "登录类型") | ||||
|     @Schema(description = "认证类型") | ||||
|     @Query(type = QueryType.IN) | ||||
|     private List<String> authType; | ||||
|  | ||||
|     /** | ||||
|      * 客户端类型 | ||||
|      */ | ||||
|     @Schema(description = "客户端类型") | ||||
|     @Query(type = QueryType.EQ) | ||||
|     private String clientType; | ||||
|  | ||||
|     /** | ||||
|      * 状态(1:启用;2:禁用) | ||||
|      * 状态 | ||||
|      */ | ||||
|     @Schema(description = "状态") | ||||
|     @Query(type = QueryType.EQ) | ||||
|     @Schema(description = "状态", example = "1") | ||||
|     private DisEnableStatusEnum status; | ||||
| } | ||||
| @@ -28,13 +28,13 @@ import java.io.Serial; | ||||
| import java.util.List; | ||||
|  | ||||
| /** | ||||
|  * 创建或修改客户端管理参数 | ||||
|  * 创建或修改客户端参数 | ||||
|  * | ||||
|  * @author MoChou | ||||
|  * @author KAI | ||||
|  * @since 2024/12/03 16:04 | ||||
|  */ | ||||
| @Data | ||||
| @Schema(description = "创建或修改客户端管理参数") | ||||
| @Schema(description = "创建或修改客户端参数") | ||||
| public class ClientReq extends BaseReq { | ||||
|  | ||||
|     @Serial | ||||
| @@ -63,11 +63,12 @@ public class ClientReq extends BaseReq { | ||||
|     private String clientSecret; | ||||
|  | ||||
|     /** | ||||
|      * 登录类型 | ||||
|      * 认证类型 | ||||
|      */ | ||||
|     @Schema(description = "登录类型") | ||||
|     @NotNull(message = "登录类型不能为空") | ||||
|     @Schema(description = "认证类型") | ||||
|     @NotNull(message = "认证类型不能为空") | ||||
|     private List<String> authType; | ||||
|  | ||||
|     /** | ||||
|      * 客户端类型 | ||||
|      */ | ||||
| @@ -89,9 +90,8 @@ public class ClientReq extends BaseReq { | ||||
|     private Integer timeout; | ||||
|  | ||||
|     /** | ||||
|      * 状态(1:启用;2:禁用) | ||||
|      * 状态 | ||||
|      */ | ||||
|     @Schema(description = "状态(1:启用;2:禁用)") | ||||
|     @NotNull(message = "状态(1:启用;2:禁用)不能为空") | ||||
|     @Schema(description = "状态", example = "1") | ||||
|     private DisEnableStatusEnum status; | ||||
| } | ||||
| @@ -22,19 +22,20 @@ import io.swagger.v3.oas.annotations.media.Schema; | ||||
| import lombok.Data; | ||||
| import top.continew.admin.common.enums.DisEnableStatusEnum; | ||||
| import top.continew.starter.extension.crud.model.resp.BaseDetailResp; | ||||
| import top.continew.starter.file.excel.converter.ExcelBaseEnumConverter; | ||||
|  | ||||
| import java.io.Serial; | ||||
| import java.util.List; | ||||
|  | ||||
| /** | ||||
|  * 客户端管理详情信息 | ||||
|  * 客户端详情信息 | ||||
|  * | ||||
|  * @author MoChou | ||||
|  * @author KAI | ||||
|  * @since 2024/12/03 16:04 | ||||
|  */ | ||||
| @Data | ||||
| @ExcelIgnoreUnannotated | ||||
| @Schema(description = "客户端管理详情信息") | ||||
| @Schema(description = "客户端详情信息") | ||||
| public class ClientDetailResp extends BaseDetailResp { | ||||
|  | ||||
|     @Serial | ||||
| @@ -44,55 +45,55 @@ public class ClientDetailResp extends BaseDetailResp { | ||||
|      * 客户端ID | ||||
|      */ | ||||
|     @Schema(description = "客户端ID") | ||||
|     @ExcelProperty(value = "客户端ID") | ||||
|     @ExcelProperty(value = "客户端ID", order = 2) | ||||
|     private String clientId; | ||||
|  | ||||
|     /** | ||||
|      * 客户端Key | ||||
|      */ | ||||
|     @Schema(description = "客户端Key") | ||||
|     @ExcelProperty(value = "客户端Key") | ||||
|     @ExcelProperty(value = "客户端Key", order = 3) | ||||
|     private String clientKey; | ||||
|  | ||||
|     /** | ||||
|      * 客户端秘钥 | ||||
|      */ | ||||
|     @Schema(description = "客户端秘钥") | ||||
|     @ExcelProperty(value = "客户端秘钥") | ||||
|     @ExcelProperty(value = "客户端秘钥", order = 4) | ||||
|     private String clientSecret; | ||||
|  | ||||
|     /** | ||||
|      * 登录类型 | ||||
|      */ | ||||
|     @Schema(description = "登录类型") | ||||
|     @ExcelProperty(value = "登录类型") | ||||
|     @ExcelProperty(value = "登录类型", order = 5) | ||||
|     private List<String> authType; | ||||
|  | ||||
|     /** | ||||
|      * 客户端类型 | ||||
|      */ | ||||
|     @Schema(description = "客户端类型") | ||||
|     @ExcelProperty(value = "客户端类型") | ||||
|     @ExcelProperty(value = "客户端类型", order = 6) | ||||
|     private String clientType; | ||||
|  | ||||
|     /** | ||||
|      * Token最低活跃频率(-1为不限制) | ||||
|      */ | ||||
|     @Schema(description = "Token最低活跃频率(-1为不限制)") | ||||
|     @ExcelProperty(value = "Token最低活跃频率(-1为不限制)") | ||||
|     @ExcelProperty(value = "Token最低活跃频率(-1为不限制)", order = 7) | ||||
|     private Integer activeTimeout; | ||||
|  | ||||
|     /** | ||||
|      * Token有效期(默认30天,单位:秒) | ||||
|      */ | ||||
|     @Schema(description = "Token有效期(默认30天,单位:秒)") | ||||
|     @ExcelProperty(value = "Token有效期(默认30天,单位:秒)") | ||||
|     @ExcelProperty(value = "Token有效期(默认30天,单位:秒)", order = 8) | ||||
|     private Integer timeout; | ||||
|  | ||||
|     /** | ||||
|      * 状态 | ||||
|      */ | ||||
|     @Schema(description = "状态") | ||||
|     @ExcelProperty(value = "状态") | ||||
|     @Schema(description = "状态", example = "1") | ||||
|     @ExcelProperty(value = "状态", converter = ExcelBaseEnumConverter.class, order = 9) | ||||
|     private DisEnableStatusEnum status; | ||||
| } | ||||
| @@ -25,13 +25,13 @@ import java.io.Serial; | ||||
| import java.util.List; | ||||
|  | ||||
| /** | ||||
|  * 客户端管理信息 | ||||
|  * 客户端信息 | ||||
|  * | ||||
|  * @author MoChou | ||||
|  * @author KAI | ||||
|  * @since 2024/12/03 16:04 | ||||
|  */ | ||||
| @Data | ||||
| @Schema(description = "客户端管理信息") | ||||
| @Schema(description = "客户端信息") | ||||
| public class ClientResp extends BaseResp { | ||||
|  | ||||
|     @Serial | ||||
|   | ||||
| @@ -23,13 +23,19 @@ import top.continew.admin.system.model.resp.ClientResp; | ||||
| import top.continew.starter.extension.crud.service.BaseService; | ||||
|  | ||||
| /** | ||||
|  * 客户端管理业务接口 | ||||
|  * 客户端业务接口 | ||||
|  * | ||||
|  * @author MoChou | ||||
|  * @author KAI | ||||
|  * @author Charles7c | ||||
|  * @since 2024/12/03 16:04 | ||||
|  */ | ||||
| public interface ClientService extends BaseService<ClientResp, ClientDetailResp, ClientQuery, ClientReq> { | ||||
|  | ||||
|     ClientResp getClientByClientId(String clientId); | ||||
|  | ||||
|     /** | ||||
|      * 根据客户端 ID 查詢 | ||||
|      * | ||||
|      * @param clientId 客戶端 ID | ||||
|      * @return 客户端信息 | ||||
|      */ | ||||
|     ClientResp getByClientId(String clientId); | ||||
| } | ||||
| @@ -17,10 +17,7 @@ | ||||
| package top.continew.admin.system.service.impl; | ||||
|  | ||||
| import cn.hutool.core.bean.BeanUtil; | ||||
| import cn.hutool.core.util.StrUtil; | ||||
| import cn.hutool.crypto.digest.DigestUtil; | ||||
| import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; | ||||
| import lombok.RequiredArgsConstructor; | ||||
| import org.springframework.stereotype.Service; | ||||
| import top.continew.admin.auth.enums.AuthTypeEnum; | ||||
| import top.continew.admin.system.mapper.ClientMapper; | ||||
| @@ -37,40 +34,38 @@ import top.continew.starter.extension.crud.service.BaseServiceImpl; | ||||
| import java.util.List; | ||||
|  | ||||
| /** | ||||
|  * 客户端管理业务实现 | ||||
|  * 客户端业务实现 | ||||
|  * | ||||
|  * @author MoChou | ||||
|  * @author KAI | ||||
|  * @author Charles7c | ||||
|  * @since 2024/12/03 16:04 | ||||
|  */ | ||||
| @Service | ||||
| @RequiredArgsConstructor | ||||
| public class ClientServiceImpl extends BaseServiceImpl<ClientMapper, ClientDO, ClientResp, ClientDetailResp, ClientQuery, ClientReq> implements ClientService { | ||||
|  | ||||
|     @Override | ||||
|     protected void beforeAdd(ClientReq req) { | ||||
|     public void beforeAdd(ClientReq req) { | ||||
|         String clientId = DigestUtil.md5Hex(req.getClientKey() + StringConstants.COLON + req.getClientSecret()); | ||||
|         req.setClientId(clientId); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 通过ClientId获取客户端实例 | ||||
|      *  | ||||
|      * @param clientId 客户端id | ||||
|      * @return 客户端响应对象 | ||||
|      */ | ||||
|     @Override | ||||
|     public ClientResp getClientByClientId(String clientId) { | ||||
|         ClientDO clientDO = baseMapper.selectOne(new LambdaQueryWrapper<ClientDO>() | ||||
|             .eq(ClientDO::getClientId, clientId)); | ||||
|         return BeanUtil.copyProperties(clientDO, ClientResp.class); | ||||
|     public void beforeDelete(List<Long> ids) { | ||||
|         // 查询如果删除客户端记录以后是否还存在账号认证的方式,不存在则不允许删除 | ||||
|         List<ClientDO> clientList = baseMapper.lambdaQuery() | ||||
|             .in(ClientDO::getId, ids) | ||||
|             .like(ClientDO::getAuthType, AuthTypeEnum.ACCOUNT.getValue()) | ||||
|             .list(); | ||||
|         ValidationUtils.throwIfEmpty(clientList, "请至少保留 [{}] 认证类型", AuthTypeEnum.ACCOUNT.getDescription()); | ||||
|         super.beforeDelete(ids); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     protected void beforeDelete(List<Long> ids) { | ||||
|         // 查询如果删除客户端记录以后是否还存在账号认证的方式,不存在则不允许删除 | ||||
|         List<ClientDO> clientDOS = baseMapper.selectList(new LambdaQueryWrapper<ClientDO>().notIn(ClientDO::getId, ids) | ||||
|             .like(ClientDO::getAuthType, AuthTypeEnum.ACCOUNT.getValue())); | ||||
|         ValidationUtils.throwIfEmpty(clientDOS, StrUtil.format("请至少保留一条{}认证的方式", AuthTypeEnum.ACCOUNT | ||||
|             .getDescription())); | ||||
|         super.beforeDelete(ids); | ||||
|     public ClientResp getByClientId(String clientId) { | ||||
|         return baseMapper.lambdaQuery() | ||||
|             .eq(ClientDO::getClientId, clientId) | ||||
|             .oneOpt() | ||||
|             .map(client -> BeanUtil.copyProperties(client, ClientResp.class)) | ||||
|             .orElse(null); | ||||
|     } | ||||
| } | ||||
| @@ -146,13 +146,13 @@ public class LogDaoLocalImpl implements LogDao { | ||||
|         // 解析登录接口信息 | ||||
|         if (requestUri.startsWith(SysConstants.LOGIN_URI) && LogStatusEnum.SUCCESS.equals(logDO.getStatus())) { | ||||
|             String requestBody = logRequest.getBody(); | ||||
|             //账号登录设置操作人 | ||||
|             // 解析账号登录用户为操作人 | ||||
|             if (requestBody.contains(AuthTypeEnum.ACCOUNT.getValue())) { | ||||
|                 AccountAuthReq loginReq = JSONUtil.toBean(requestBody, AccountAuthReq.class); | ||||
|                 logDO.setCreateUser(ExceptionUtils.exToNull(() -> userService.getByUsername(loginReq.getUsername()) | ||||
|                 AccountAuthReq authReq = JSONUtil.toBean(requestBody, AccountAuthReq.class); | ||||
|                 logDO.setCreateUser(ExceptionUtils.exToNull(() -> userService.getByUsername(authReq.getUsername()) | ||||
|                     .getId())); | ||||
|                 return; | ||||
|             } | ||||
|             return; | ||||
|         } | ||||
|         // 解析 Token 信息 | ||||
|         Map<String, String> requestHeaders = logRequest.getHeaders(); | ||||
|   | ||||
| @@ -19,30 +19,23 @@ package top.continew.admin.controller.auth; | ||||
| import cn.dev33.satoken.annotation.SaIgnore; | ||||
| import cn.dev33.satoken.stp.StpUtil; | ||||
| import cn.hutool.core.bean.BeanUtil; | ||||
| import cn.hutool.core.util.StrUtil; | ||||
| import io.swagger.v3.oas.annotations.Operation; | ||||
| import io.swagger.v3.oas.annotations.Parameter; | ||||
| import io.swagger.v3.oas.annotations.enums.ParameterIn; | ||||
| import io.swagger.v3.oas.annotations.tags.Tag; | ||||
| import jakarta.servlet.http.HttpServletRequest; | ||||
| import lombok.RequiredArgsConstructor; | ||||
| import lombok.extern.slf4j.Slf4j; | ||||
| import org.springframework.validation.annotation.Validated; | ||||
| import org.springframework.web.bind.annotation.*; | ||||
| import top.continew.admin.auth.config.AuthHandlerContext; | ||||
| import top.continew.admin.auth.model.req.AuthReq; | ||||
| import top.continew.admin.auth.model.resp.LoginResp; | ||||
| import top.continew.admin.auth.model.resp.RouteResp; | ||||
| import top.continew.admin.auth.model.resp.UserInfoResp; | ||||
| import top.continew.admin.auth.service.LoginService; | ||||
| import top.continew.admin.auth.service.AuthService; | ||||
| import top.continew.admin.common.context.UserContext; | ||||
| import top.continew.admin.common.context.UserContextHolder; | ||||
| import top.continew.admin.system.model.resp.ClientResp; | ||||
| import top.continew.admin.system.model.resp.user.UserDetailResp; | ||||
| import top.continew.admin.system.service.ClientService; | ||||
| import top.continew.admin.system.service.UserService; | ||||
| import top.continew.starter.core.exception.BusinessException; | ||||
| import top.continew.starter.core.validation.ValidationUtils; | ||||
| import top.continew.starter.log.annotation.Log; | ||||
|  | ||||
| import java.util.List; | ||||
| @@ -53,44 +46,25 @@ import java.util.List; | ||||
|  * @author Charles7c | ||||
|  * @since 2022/12/21 20:37 | ||||
|  */ | ||||
| @Slf4j | ||||
| @Log(module = "登录") | ||||
| @Tag(name = "认证 API") | ||||
| @Log(module = "登录") | ||||
| @Validated | ||||
| @RestController | ||||
| @RequiredArgsConstructor | ||||
| @RequestMapping("/auth") | ||||
| public class AuthController { | ||||
|     private final ClientService clientService; | ||||
|  | ||||
|     private final AuthService authService; | ||||
|     private final UserService userService; | ||||
|  | ||||
|     private final LoginService loginService; | ||||
|  | ||||
|     private final AuthHandlerContext authHandlerContext; | ||||
|  | ||||
|     @SaIgnore | ||||
|     @Operation(summary = "登录", description = "统一登录入口") | ||||
|     @Operation(summary = "登录", description = "用户登录") | ||||
|     @PostMapping("/login") | ||||
|     public LoginResp login(@Validated @RequestBody AuthReq loginReq, HttpServletRequest request) { | ||||
|         // 认证类型 | ||||
|         String authType = loginReq.getAuthType(); | ||||
|  | ||||
|         // 获取并验证客户端信息 | ||||
|         ClientResp clientResp = clientService.getClientByClientId(loginReq.getClientId()); | ||||
|         ValidationUtils.throwIfNull(clientResp, "客户端信息不存在,请检查客户端id是否正确!"); | ||||
|  | ||||
|         // 验证认证类型 | ||||
|         ValidationUtils.throwIf(!clientResp.getAuthType().contains(authType), StrUtil.format("暂未授权此类型:{}", authType)); | ||||
|         try { | ||||
|             // 执行登录策略 | ||||
|             return (LoginResp)authHandlerContext.getHandler(authType).login(loginReq, clientResp, request); | ||||
|         } catch (Exception e) { | ||||
|             log.error("登录失败: {}", e.getMessage(), e); | ||||
|             throw new BusinessException("登录失败: " + e.getMessage()); | ||||
|         } | ||||
|     public LoginResp login(@Validated @RequestBody AuthReq req, HttpServletRequest request) { | ||||
|         return authService.login(req, request); | ||||
|     } | ||||
|  | ||||
|     @Operation(summary = "用户退出", description = "注销用户的当前登录") | ||||
|     @Operation(summary = "登出", description = "注销用户的当前登录") | ||||
|     @Parameter(name = "Authorization", description = "令牌", required = true, example = "Bearer xxxx-xxxx-xxxx-xxxx", in = ParameterIn.HEADER) | ||||
|     @PostMapping("/logout") | ||||
|     public Object logout() { | ||||
| @@ -114,8 +88,8 @@ public class AuthController { | ||||
|  | ||||
|     @Log(ignore = true) | ||||
|     @Operation(summary = "获取路由信息", description = "获取登录用户的路由信息") | ||||
|     @GetMapping("/route") | ||||
|     @GetMapping("/user/route") | ||||
|     public List<RouteResp> listRoute() { | ||||
|         return loginService.buildRouteTree(UserContextHolder.getUserId()); | ||||
|         return authService.buildRouteTree(UserContextHolder.getUserId()); | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -17,24 +17,20 @@ | ||||
| package top.continew.admin.controller.auth; | ||||
|  | ||||
| import cn.dev33.satoken.annotation.SaIgnore; | ||||
| import cn.dev33.satoken.stp.StpUtil; | ||||
| import com.xkcoding.justauth.AuthRequestFactory; | ||||
| import io.swagger.v3.oas.annotations.Operation; | ||||
| import io.swagger.v3.oas.annotations.Parameter; | ||||
| import io.swagger.v3.oas.annotations.enums.ParameterIn; | ||||
| import io.swagger.v3.oas.annotations.tags.Tag; | ||||
| import lombok.RequiredArgsConstructor; | ||||
| import me.zhyd.oauth.model.AuthCallback; | ||||
| import me.zhyd.oauth.model.AuthResponse; | ||||
| import me.zhyd.oauth.model.AuthUser; | ||||
| import me.zhyd.oauth.request.AuthRequest; | ||||
| import me.zhyd.oauth.utils.AuthStateUtils; | ||||
| import org.springframework.web.bind.annotation.*; | ||||
| import top.continew.admin.auth.model.resp.LoginResp; | ||||
| import org.springframework.web.bind.annotation.GetMapping; | ||||
| import org.springframework.web.bind.annotation.PathVariable; | ||||
| import org.springframework.web.bind.annotation.RequestMapping; | ||||
| import org.springframework.web.bind.annotation.RestController; | ||||
| import top.continew.admin.auth.model.resp.SocialAuthAuthorizeResp; | ||||
| import top.continew.admin.auth.service.LoginService; | ||||
| import top.continew.starter.core.exception.BadRequestException; | ||||
| import top.continew.starter.core.validation.ValidationUtils; | ||||
| import top.continew.starter.log.annotation.Log; | ||||
|  | ||||
| /** | ||||
| @@ -51,7 +47,6 @@ import top.continew.starter.log.annotation.Log; | ||||
| @RequestMapping("/oauth") | ||||
| public class SocialAuthController { | ||||
|  | ||||
|     private final LoginService loginService; | ||||
|     private final AuthRequestFactory authRequestFactory; | ||||
|  | ||||
|     @Operation(summary = "三方账号登录授权", description = "三方账号登录授权") | ||||
| @@ -64,21 +59,6 @@ public class SocialAuthController { | ||||
|             .build(); | ||||
|     } | ||||
|  | ||||
|     @Operation(summary = "三方账号登录", description = "三方账号登录") | ||||
|     @Parameter(name = "source", description = "来源", example = "gitee", in = ParameterIn.PATH) | ||||
|     @PostMapping("/{source}") | ||||
|     public LoginResp login(@PathVariable String source, @RequestBody AuthCallback callback) { | ||||
|         if (StpUtil.isLogin()) { | ||||
|             StpUtil.logout(); | ||||
|         } | ||||
|         AuthRequest authRequest = this.getAuthRequest(source); | ||||
|         AuthResponse<AuthUser> response = authRequest.login(callback); | ||||
|         ValidationUtils.throwIf(!response.ok(), response.getMsg()); | ||||
|         AuthUser authUser = response.getData(); | ||||
|         String token = loginService.socialLogin(authUser); | ||||
|         return LoginResp.builder().token(token).build(); | ||||
|     } | ||||
|  | ||||
|     private AuthRequest getAuthRequest(String source) { | ||||
|         try { | ||||
|             return authRequestFactory.get(source); | ||||
|   | ||||
| @@ -28,12 +28,12 @@ import top.continew.starter.extension.crud.annotation.CrudRequestMapping; | ||||
| import top.continew.starter.extension.crud.enums.Api; | ||||
|  | ||||
| /** | ||||
|  * 客户端管理管理 API | ||||
|  * 客户端管理 API | ||||
|  * | ||||
|  * @author MoChou | ||||
|  * @author KAI | ||||
|  * @since 2024/12/03 16:04 | ||||
|  */ | ||||
| @Tag(name = "客户端管理管理 API") | ||||
| @Tag(name = "客户端管理 API") | ||||
| @RestController | ||||
| @CrudRequestMapping(value = "/system/client", api = {Api.PAGE, Api.DETAIL, Api.ADD, Api.UPDATE, Api.DELETE, Api.EXPORT}) | ||||
| public class ClientController extends BaseController<ClientService, ClientResp, ClientDetailResp, ClientQuery, ClientReq> { | ||||
|   | ||||
| @@ -1,6 +1,6 @@ | ||||
| -- liquibase formatted sql | ||||
|  | ||||
| -- changeset Charles7c:1 | ||||
| -- changeset charles7c:1 | ||||
| -- comment 初始化表数据 | ||||
| -- 初始化默认菜单 | ||||
| INSERT INTO `sys_menu` | ||||
| @@ -76,6 +76,13 @@ VALUES | ||||
| (1114, '修改', 1110, 3, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, 'system:storage:update', 4, 1, 1, NOW()), | ||||
| (1115, '删除', 1110, 3, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, 'system:storage:delete', 5, 1, 1, NOW()), | ||||
|  | ||||
| ( 1180, '客户端管理', 1000, 2, '/system/client', 'SystemClient', 'system/client/index', NULL, 'mobile', b'0', b'0', b'0', NULL, 9, 1, 1, NOW()), | ||||
| (1181, '列表', 1180, 3, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, 'system:client:list', 1, 1, 1, NOW()), | ||||
| (1182, '详情', 1180, 3, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, 'system:client:detail', 2, 1, 1, NOW()), | ||||
| (1183, '新增', 1180, 3, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, 'system:client:add', 3, 1, 1, NOW()), | ||||
| (1184, '修改', 1180, 3, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, 'system:client:update', 4, 1, 1, NOW()), | ||||
| (1185, '删除', 1180, 3, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, 'system:client:delete', 5, 1, 1, NOW()), | ||||
|  | ||||
| (1190, '系统配置', 1000, 2, '/system/config', 'SystemConfig', 'system/config/index', NULL, 'config', b'0', b'0', b'0', NULL, 999, 1, 1, NOW()), | ||||
| (1191, '查看', 1190, 3, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, 'system:config:list', 1, 1, 1, NOW()), | ||||
| (1192, '修改', 1190, 3, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, 'system:config:update', 2, 1, 1, NOW()), | ||||
| @@ -150,7 +157,8 @@ INSERT INTO `sys_dict` | ||||
| (`id`, `name`, `code`, `description`, `is_system`, `create_user`, `create_time`) | ||||
| VALUES | ||||
| (1, '公告类型', 'notice_type', NULL, b'1', 1, NOW()), | ||||
| (2, '消息类型', 'message_type', NULL, b'1', 1, NOW()); | ||||
| (2, '消息类型', 'message_type', NULL, b'1', 1, NOW()), | ||||
| (3, '客户端类型', 'client_type', NULL, b'1', 1, NOW()); | ||||
|  | ||||
| INSERT INTO `sys_dict_item` | ||||
| (`id`, `label`, `value`, `color`, `sort`, `description`, `status`, `dict_id`, `create_user`, `create_time`) | ||||
| @@ -158,7 +166,10 @@ VALUES | ||||
| (1, '通知', '1', 'blue', 1, NULL, 1, 1, 1, NOW()), | ||||
| (2, '活动', '2', 'orangered', 2, NULL, 1, 1, 1, NOW()), | ||||
| (3, '安全消息', '1', 'blue', 1, NULL, 1, 2, 1, NOW()), | ||||
| (4, '活动消息', '2', 'orangered', 2, NULL, 1, 2, 1, NOW()); | ||||
| (4, '活动消息', '2', 'orangered', 2, NULL, 1, 2, 1, NOW()), | ||||
| (5, '桌面端', 'PC', 'blue', 1, NULL, 1, 3, 1, NOW()), | ||||
| (6, '安卓', 'ANDROID', '#148628', 2, NULL, 1, 3, 1, NOW()), | ||||
| (7, '小程序', 'XCX', '#7930AD', 3, NULL, 1, 3, 1, NOW()); | ||||
|  | ||||
| -- 初始化默认用户和角色关联数据 | ||||
| INSERT INTO `sys_user_role` | ||||
| @@ -188,30 +199,8 @@ VALUES | ||||
| (1, '开发环境', 'local_dev', 2, NULL, NULL, NULL, 'C:/continew-admin/data/file/', 'http://localhost:8000/file', '本地存储', b'1', 1, 1, 1, NOW()), | ||||
| (2, '生产环境', 'local_prod', 2, NULL, NULL, NULL, '../data/file/', 'http://api.continew.top/file', '本地存储', b'0', 2, 2, 1, NOW()); | ||||
|  | ||||
| -- 客户端管理管理菜单 | ||||
| INSERT INTO `sys_menu` | ||||
| (`title`, `parent_id`, `type`, `path`, `name`, `component`, `redirect`, `icon`, `is_external`, `is_cache`, `is_hidden`, `permission`, `sort`, `status`, `create_user`, `create_time`, `update_user`, `update_time`) | ||||
| -- 初始化客户端数据 | ||||
| INSERT INTO `sys_client` | ||||
| (`id`, `client_id`, `client_key`, `client_secret`, `auth_type`, `client_type`, `active_timeout`, `timeout`, `status`, `create_user`, `create_time`) | ||||
| VALUES | ||||
| ( '客户端管理', 1000, 2, '/system/client', 'SystemClient', 'system/client/index', NULL, 'mobile', b'0', b'0', b'0', NULL, 9, 1, 1, NOW(), NULL, NULL); | ||||
|  | ||||
| SET @parentId = LAST_INSERT_ID(); | ||||
|  | ||||
| -- 客户端管理管理按钮 | ||||
| INSERT INTO `sys_menu` | ||||
| (`title`, `parent_id`, `type`, `path`, `name`, `component`, `redirect`, `icon`, `is_external`, `is_cache`, `is_hidden`, `permission`, `sort`, `status`, `create_user`, `create_time`, `update_user`, `update_time`) | ||||
| VALUES | ||||
|     ('列表', @parentId, 3, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, 'system:client:list', 1, 1, 1, NOW(), NULL, NULL), | ||||
|     ('详情', @parentId, 3, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, 'system:client:detail', 2, 1, 1, NOW(), NULL, NULL), | ||||
|     ('新增', @parentId, 3, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, 'system:client:add', 3, 1, 1, NOW(), NULL, NULL), | ||||
|     ('修改', @parentId, 3, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, 'system:client:update', 4, 1, 1, NOW(), NULL, NULL), | ||||
|     ('删除', @parentId, 3, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, 'system:client:delete', 5, 1, 1, NOW(), NULL, NULL), | ||||
|     ('导出', @parentId, 3, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, 'system:client:export', 6, 1, 1, NOW(), NULL, NULL); | ||||
| -- 插入客户端字典数据 | ||||
| INSERT INTO `sys_dict` (`id`, `name`, `code`, `description`, `is_system`, `create_user`, `create_time`, `update_user`, `update_time`) VALUES (4, '客户端类型', 'client_type', NULL, b'0', 1, '2024-12-17 18:09:37', NULL, NULL); | ||||
| INSERT INTO `sys_dict_item` (`id`, `label`, `value`, `color`, `sort`, `description`, `status`, `dict_id`, `create_user`, `create_time`, `update_user`, `update_time`) VALUES (659457796923719711, '桌面端', 'pc', 'blue', 999, NULL, 1, 4, 1, '2024-12-17 18:09:51', NULL, NULL); | ||||
| INSERT INTO `sys_dict_item` (`id`, `label`, `value`, `color`, `sort`, `description`, `status`, `dict_id`, `create_user`, `create_time`, `update_user`, `update_time`) VALUES (659457877756346402, '安卓', 'android', '#148628', 999, NULL, 1, 4, 1, '2024-12-17 18:10:10', NULL, NULL); | ||||
| INSERT INTO `sys_dict_item` (`id`, `label`, `value`, `color`, `sort`, `description`, `status`, `dict_id`, `create_user`, `create_time`, `update_user`, `update_time`) VALUES (659457929665052709, '小程序', 'xcx', '#7930AD', 999, NULL, 1, 4, 1, '2024-12-17 18:10:23', NULL, NULL); | ||||
| -- 插入客户端数据 | ||||
| INSERT INTO `sys_client` (`id`, `client_id`, `client_key`, `client_secret`, `auth_type`, `client_type`, `active_timeout`, `timeout`, `status`, `create_user`, `create_time`, `update_user`, `update_time`) VALUES (1, 'ef51c9a3e9046c4f2ea45142c8a8344a', 'pc', 'dd77ab1e353a027e0d60ce3b151e8642', '[\"account\", \"socialAuth\", \"email\", \"phone\"]', 'pc', 1800, 86400, 1, 1, now(), 1, '2024-12-25 17:41:36'); | ||||
|  | ||||
|  | ||||
| (1, 'ef51c9a3e9046c4f2ea45142c8a8344a', 'pc', 'dd77ab1e353a027e0d60ce3b151e8642', '[\"ACCOUNT\", \"EMAIL\", \"PHONE\", \"SOCIAL\"]', 'PC', 1800, 86400, 1, 1, NOW()); | ||||
|   | ||||
| @@ -300,14 +300,14 @@ CREATE TABLE IF NOT EXISTS `sys_client` ( | ||||
|     `client_id`      varchar(50)  NOT NULL                    COMMENT '客户端ID', | ||||
|     `client_key`     varchar(255) NOT NULL                    COMMENT '客户端Key', | ||||
|     `client_secret`  varchar(255) NOT NULL                    COMMENT '客户端秘钥', | ||||
|     `auth_type`      json         DEFAULT NULL                COMMENT '授权类型', | ||||
|     `client_type`    varchar(50)  DEFAULT NULL                COMMENT '客户端类型', | ||||
|     `auth_type`      json         DEFAULT NULL                COMMENT '认证类型', | ||||
|     `client_type`    varchar(50)  NOT NULL                    COMMENT '客户端类型', | ||||
|     `active_timeout` int          DEFAULT '-1'                COMMENT 'Token最低活跃频率(-1为不限制)', | ||||
|     `timeout`        int          DEFAULT '2592000'           COMMENT 'Token有效期(默认30天,单位:秒)', | ||||
|     `status`         tinyint(1)   UNSIGNED NOT NULL           COMMENT '状态(1:启用;2:禁用)', | ||||
|     `status`         tinyint(1)   UNSIGNED NOT NULL DEFAULT 1 COMMENT '状态(1:启用;2:禁用)', | ||||
|     `create_user`    bigint(20)   NOT NULL                    COMMENT '创建人', | ||||
|     `create_time`    datetime     NOT NULL                    COMMENT '创建时间', | ||||
|     `update_user`    bigint(20)   DEFAULT NULL                COMMENT '修改人', | ||||
|     `update_time`    datetime     DEFAULT NULL                COMMENT '修改时间', | ||||
|     PRIMARY KEY (`id`) | ||||
| ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='客户端管理'; | ||||
| ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='客户端表'; | ||||
|   | ||||
| @@ -1,6 +1,6 @@ | ||||
| -- liquibase formatted sql | ||||
|  | ||||
| -- changeset Charles7c:1 | ||||
| -- changeset charles7c:1 | ||||
| -- comment 初始化表数据 | ||||
| -- 初始化默认菜单 | ||||
| INSERT INTO "sys_menu" | ||||
|   | ||||
| @@ -1,6 +1,6 @@ | ||||
| -- liquibase formatted sql | ||||
|  | ||||
| -- changeset Charles7c:1 | ||||
| -- changeset charles7c:1 | ||||
| -- comment 初始化表结构 | ||||
| CREATE TABLE IF NOT EXISTS "sys_menu" ( | ||||
|     "id"          int8         NOT NULL, | ||||
|   | ||||
		Reference in New Issue
	
	Block a user