mirror of
https://github.com/continew-org/continew-admin.git
synced 2025-09-10 07:02:47 +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