refactor: 优化认证及客户端相关代码

This commit is contained in:
2024-12-27 00:20:36 +08:00
parent 95f2617a4c
commit c90e80e9d7
35 changed files with 595 additions and 867 deletions

View File

@@ -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";

View File

@@ -81,12 +81,12 @@ public class UserContext implements Serializable {
private Set<RoleContext> roles;
/**
* 设备类型
* 客户端类型
*/
private String clientType;
/**
* 客户端ID
* 客户端 ID
*/
private String clientId;

View File

@@ -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(), "此账号所属部门已被禁用,如有疑问,请联系管理员");
}
}

View File

@@ -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);
}

View File

@@ -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);
}
}

View File

@@ -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;

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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");
}
}
}

View File

@@ -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;

View File

@@ -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;
}

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;
}

View File

@@ -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

View File

@@ -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);
}

View File

@@ -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);
}
}

View File

@@ -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");
}
}
}

View File

@@ -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> {}

View File

@@ -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;

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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

View File

@@ -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);
}

View File

@@ -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);
}
}

View File

@@ -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();

View File

@@ -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());
}
}

View File

@@ -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);

View File

@@ -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> {

View File

@@ -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());

View File

@@ -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='客户端';

View File

@@ -1,6 +1,6 @@
-- liquibase formatted sql
-- changeset Charles7c:1
-- changeset charles7c:1
-- comment 初始化表数据
-- 初始化默认菜单
INSERT INTO "sys_menu"

View File

@@ -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,