feat(system/client): 新增客户端管理

This commit is contained in:
KAI
2024-12-26 02:18:34 +00:00
committed by Charles7c
parent 6bcff7244f
commit 95f2617a4c
30 changed files with 1651 additions and 114 deletions

View File

@@ -67,7 +67,7 @@ public class SysConstants {
/**
* 账号登录 URI
*/
public static final String LOGIN_URI = "/auth/account";
public static final String LOGIN_URI = "/auth/login";
/**
* 退出 URI

View File

@@ -80,6 +80,16 @@ public class UserContext implements Serializable {
*/
private Set<RoleContext> roles;
/**
* 设备类型
*/
private String clientType;
/**
* 客户端ID
*/
private String clientId;
public UserContext(Set<String> permissions, Set<RoleContext> roles, Integer passwordExpirationDays) {
this.permissions = permissions;
this.setRoles(roles);

View File

@@ -0,0 +1,55 @@
/*
* 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;
import jakarta.servlet.http.HttpServletRequest;
import top.continew.admin.auth.enums.AuthTypeEnum;
import top.continew.admin.auth.model.req.AuthReq;
import top.continew.admin.system.model.resp.ClientResp;
/**
* 认证接口
*
* @author KAI
* @since 2024/12/22 14:52:23
*/
public interface AuthHandler<T extends AuthReq, R> {
/**
* 执行登录
*
* @param authReq 登录请求参数
* @param request HTTP请求对象
* @return 登录响应
*/
R login(T authReq, ClientResp clientResp, HttpServletRequest request);
/**
* 获取登录类型
*
* @return 登录类型Enum
*/
AuthTypeEnum getAuthType();
/**
* 校验参数
*
* @param authReq 登录请求参数
*/
void validate(T authReq);
}

View File

@@ -0,0 +1,53 @@
/*
* 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.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
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
*/
@Component
public class AuthHandlerContext {
private final Map<String, AuthHandler<?, ?>> handlerMap = new HashMap<>();
@Autowired
public AuthHandlerContext(List<AuthHandler<?, ?>> strategies) {
for (AuthHandler<?, ?> strategy : strategies) {
handlerMap.put(strategy.getAuthType().getValue(), strategy);
}
}
@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;
}
}

View File

@@ -0,0 +1,57 @@
/*
* 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.enums;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import top.continew.admin.common.constant.UiConstants;
import top.continew.starter.core.enums.BaseEnum;
/**
* 认证类型
*
* @author Charles7c
* @since 2023/12/23 13:38
*/
@Getter
@RequiredArgsConstructor
public enum AuthTypeEnum implements BaseEnum<String> {
/**
* 账号
*/
ACCOUNT("account", "账号", UiConstants.COLOR_ERROR),
/**
* 邮箱
*/
EMAIL("email", "邮箱", UiConstants.COLOR_PRIMARY),
/**
* 手机号
*/
PHONE("phone", "手机号", UiConstants.COLOR_SUCCESS),
/**
* 第三方授权
*/
SOCIAL_AUTH("socialAuth", "第三方授权", UiConstants.COLOR_DEFAULT);
private final String value;
private final String description;
private final String color;
}

View File

@@ -0,0 +1,137 @@
/*
* 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.handler;
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 org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import org.springframework.stereotype.Component;
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.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.RoleService;
import top.continew.starter.web.util.SpringWebUtils;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import static top.continew.admin.system.enums.PasswordPolicyEnum.PASSWORD_EXPIRATION_DAYS;
/**
* 认证处理器抽象类
*
* @author KAI
* @since 2024/12/22 14:52
*/
@Component
@RequiredArgsConstructor
public abstract class AbstractAuthHandler {
@Resource
private RoleService roleService;
@Resource
private OptionService optionService;
@Resource
private ThreadPoolTaskExecutor threadPoolTaskExecutor;
public static final String CAPTCHA_EXPIRED = "验证码已失效";
public static final String CAPTCHA_ERROR = "验证码错误";
public static final String CLIENT_ID = "clientId";
/**
* 获取登录凭证
*
* @param user 用户信息
* @param clientResp 客户端信息
* @return token 认证信息
*/
protected String authCertificate(UserDO user, ClientResp clientResp) {
preLogin(user, clientResp);
// 核心登录逻辑
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);
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);
// 登录并缓存用户信息
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;
}
/**
* 登录前置处理
*
* @param user 用户信息
* @param clientResp 客户端信息
*/
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) {
}
}

View File

@@ -0,0 +1,120 @@
/*
* 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.handler;
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.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.SysConstants;
import top.continew.admin.common.util.SecureUtils;
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.core.util.ExceptionUtils;
import top.continew.starter.core.validation.ValidationUtils;
/**
* 账号认证处理器
*
* @author KAI
* @since 2024/12/22 14:58:32
*/
@Component
@RequiredArgsConstructor
public class AccountAuthHandler extends AbstractAuthHandler implements AuthHandler<AccountAuthReq, LoginResp> {
private final UserService userService;
private final LoginService loginService;
private final PasswordEncoder passwordEncoder;
private final OptionService optionService;
/**
* 获取认证类型
*
* @return 账号认证类型
*/
@Override
public AuthTypeEnum getAuthType() {
return AuthTypeEnum.ACCOUNT;
}
/**
* 校验账号登录请求对象
*
* @param authReq 登录请求参数
*/
@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(), "验证码标识不能为空");
}
}
/**
* 账号登录
*
* @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);
}
}

View File

@@ -0,0 +1,107 @@
/*
* 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.handler;
import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;
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;
/**
* 邮箱认证处理器
*
* @author KAI
* @since 2024/12/22 14:58
*/
@Component
@RequiredArgsConstructor
public class EmailAuthHandler extends AbstractAuthHandler implements AuthHandler<EmailAuthReq, LoginResp> {
private final UserService userService;
private final LoginService loginService;
/**
* 获取认证类型
*
* @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

@@ -0,0 +1,109 @@
/*
* 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.handler;
import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;
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;
/**
* 手机号认证处理器
*
* @author KAI
* @since 2024/12/22 14:59
*/
@Component
@RequiredArgsConstructor
public class PhoneAuthHandler extends AbstractAuthHandler implements AuthHandler<PhoneAuthReq, LoginResp> {
private final UserService userService;
private final LoginService loginService;
/**
* 获取认证类型
*
* @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

@@ -0,0 +1,172 @@
/*
* 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.handler;
import cn.dev33.satoken.stp.StpUtil;
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.util.IdUtil;
import cn.hutool.core.util.RandomUtil;
import cn.hutool.core.util.ReUtil;
import cn.hutool.json.JSONUtil;
import com.xkcoding.justauth.AuthRequestFactory;
import jakarta.servlet.http.HttpServletRequest;
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 org.springframework.stereotype.Component;
import top.continew.admin.auth.AuthHandler;
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.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.resp.ClientResp;
import top.continew.admin.system.service.RoleService;
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.exception.BadRequestException;
import top.continew.starter.core.validation.ValidationUtils;
import java.time.LocalDateTime;
import java.util.Collections;
/**
* 手机号认证处理器
*
* @author KAI
* @since 2024/12/25 14:21
*/
@Component
@RequiredArgsConstructor
public class SocialAuthHandler extends AbstractAuthHandler implements AuthHandler<SocialAuthReq, LoginResp> {
private final AuthRequestFactory authRequestFactory;
private final UserSocialService userSocialService;
private final UserService userService;
private final RoleService roleService;
private final UserRoleService userRoleService;
private final LoginService loginService;
/**
* 获取认证类型
*
* @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());
AuthCallback callback = new AuthCallback();
callback.setCode(authReq.getCode());
callback.setState(authReq.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);
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);
loginService.sendSecurityMsg(user);
} else {
user = BeanUtil.copyProperties(userService.getById(userSocial.getUserId()), UserDO.class);
}
loginService.checkUserStatus(user);
userSocial.setMetaJson(JSONUtil.toJsonStr(authUser));
userSocial.setLastLoginTime(LocalDateTime.now());
userSocialService.saveOrUpdate(userSocial);
// 执行登录
String token = this.authCertificate(user, clientResp);
return LoginResp.builder().token(token).build();
}
private AuthRequest getAuthRequest(String source) {
try {
return authRequestFactory.get(source);
} catch (Exception e) {
throw new BadRequestException("暂不支持 [%s] 平台账号登录".formatted(source));
}
}
/**
* 获取认证信息
*
* @param user 用户信息
* @param clientResp 客户端信息
* @return 认证信息
*/
@Override
protected String authCertificate(UserDO user, ClientResp clientResp) {
return super.authCertificate(user, clientResp);
}
}

View File

@@ -31,7 +31,7 @@ import java.io.Serializable;
*/
@Data
@Schema(description = "账号登录参数")
public class AccountLoginReq implements Serializable {
public class AccountAuthReq extends AuthReq implements Serializable {
@Serial
private static final long serialVersionUID = 1L;

View File

@@ -0,0 +1,46 @@
/*
* 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.model.req;
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 lombok.Data;
/**
* 登录参数基础类
*
* @author KAI
* @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 {
@Schema(description = "客户端id")
@NotBlank(message = "客户端id不能为空")
private String clientId;
@Schema(description = "认证类型")
@NotBlank(message = "认证类型不能为空")
private String authType;
}

View File

@@ -34,7 +34,7 @@ import java.io.Serializable;
*/
@Data
@Schema(description = "邮箱登录参数")
public class EmailLoginReq implements Serializable {
public class EmailAuthReq extends AuthReq implements Serializable {
@Serial
private static final long serialVersionUID = 1L;

View File

@@ -34,7 +34,7 @@ import java.io.Serializable;
*/
@Data
@Schema(description = "手机号登录参数")
public class PhoneLoginReq implements Serializable {
public class PhoneAuthReq extends AuthReq implements Serializable {
@Serial
private static final long serialVersionUID = 1L;

View File

@@ -0,0 +1,49 @@
/*
* 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.model.req;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import lombok.Data;
/**
* 第三方登录参数
*
* @author KAI
* @since 2024/12/25 15:43
*/
@Data
@Schema(description = "第三方登录参数")
public class SocialAuthReq extends AuthReq {
/**
* 第三方登录平台
*/
@NotBlank(message = "第三方登录平台不能为空")
private String source;
/**
* 第三方登录code
*/
@NotBlank(message = "第三方登录code不能为空")
private String code;
/**
* 第三方登录state
*/
@NotBlank(message = "第三方登录state不能为空")
private String state;
}

View File

@@ -19,6 +19,7 @@ package top.continew.admin.auth.service;
import jakarta.servlet.http.HttpServletRequest;
import me.zhyd.oauth.model.AuthUser;
import top.continew.admin.auth.model.resp.RouteResp;
import top.continew.admin.system.model.entity.UserDO;
import java.util.List;
@@ -31,30 +32,28 @@ import java.util.List;
public interface LoginService {
/**
* 账号登录
* 检查用户状态
*
* @param user 用户信息
*/
void checkUserStatus(UserDO user);
/**
* 检查用户是否被锁定
*
* @param username 用户名
* @param password 密码
* @param request 请求对象
* @return 令牌
* @param isError 是否登录错误
*/
String accountLogin(String username, String password, HttpServletRequest request);
void checkUserLocked(String username, HttpServletRequest request, boolean isError);
/**
* 手机号登录
* 执行登录操作
*
* @param phone 手机号
* @return 令牌
* @param user 用户信息
* @return token
*/
String phoneLogin(String phone);
/**
* 邮箱登录
*
* @param email 邮箱
* @return 令牌
*/
String emailLogin(String email);
String login(UserDO user);
/**
* 三方账号登录
@@ -71,4 +70,11 @@ public interface LoginService {
* @return 路由树
*/
List<RouteResp> buildRouteTree(Long userId);
/**
* 发送安全消息
*
* @param user 用户信息
*/
void sendSecurityMsg(UserDO user);
}

View File

@@ -96,32 +96,6 @@ public class LoginServiceImpl implements LoginService {
private final OptionService optionService;
private final MessageService messageService;
@Override
public String accountLogin(String username, String password, HttpServletRequest request) {
UserDO user = userService.getByUsername(username);
boolean isError = ObjectUtil.isNull(user) || !passwordEncoder.matches(password, user.getPassword());
this.checkUserLocked(username, request, isError);
CheckUtils.throwIf(isError, "用户名或密码错误");
this.checkUserStatus(user);
return this.login(user);
}
@Override
public String phoneLogin(String phone) {
UserDO user = userService.getByPhone(phone);
CheckUtils.throwIfNull(user, "此手机号未绑定本系统账号");
this.checkUserStatus(user);
return this.login(user);
}
@Override
public String emailLogin(String email) {
UserDO user = userService.getByEmail(email);
CheckUtils.throwIfNull(user, "此邮箱未绑定本系统账号");
this.checkUserStatus(user);
return this.login(user);
}
@Override
@Transactional(rollbackFor = Exception.class)
public String socialLogin(AuthUser authUser) {
@@ -209,7 +183,8 @@ public class LoginServiceImpl implements LoginService {
* @param user 用户信息
* @return 令牌
*/
private String login(UserDO user) {
@Override
public String login(UserDO user) {
Long userId = user.getId();
CompletableFuture<Set<String>> permissionFuture = CompletableFuture.supplyAsync(() -> roleService
.listPermissionByUserId(userId), threadPoolTaskExecutor);
@@ -233,7 +208,8 @@ public class LoginServiceImpl implements LoginService {
*
* @param user 用户信息
*/
private void checkUserStatus(UserDO 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(), "此账号所属部门已被禁用,如有疑问,请联系管理员");
@@ -246,7 +222,8 @@ public class LoginServiceImpl implements LoginService {
* @param request 请求对象
* @param isError 是否登录错误
*/
private void checkUserLocked(String username, HttpServletRequest request, boolean 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) {
@@ -274,7 +251,8 @@ public class LoginServiceImpl implements LoginService {
*
* @param user 用户信息
*/
private void sendSecurityMsg(UserDO user) {
@Override
public void sendSecurityMsg(UserDO user) {
MessageReq req = new MessageReq();
MessageTemplateEnum socialRegister = MessageTemplateEnum.SOCIAL_REGISTER;
req.setTitle(socialRegister.getTitle().formatted(projectProperties.getName()));

View File

@@ -0,0 +1,28 @@
/*
* 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.system.mapper;
import top.continew.starter.data.mp.base.BaseMapper;
import top.continew.admin.system.model.entity.ClientDO;
/**
* 客户端管理 Mapper
*
* @author MoChou
* @since 2024/12/03 16:04
*/
public interface ClientMapper extends BaseMapper<ClientDO> {}

View File

@@ -0,0 +1,82 @@
/*
* 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.system.model.entity;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableName;
import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler;
import lombok.Data;
import top.continew.admin.common.enums.DisEnableStatusEnum;
import top.continew.starter.extension.crud.model.entity.BaseDO;
import java.io.Serial;
import java.util.List;
/**
* 客户端管理实体
*
* @author MoChou
* @since 2024/12/03 16:04
*/
@Data
@TableName(value = "sys_client", autoResultMap = true)
public class ClientDO extends BaseDO {
@Serial
private static final long serialVersionUID = 1L;
/**
* 客户端ID
*/
private String clientId;
/**
* 客户端Key
*/
private String clientKey;
/**
* 客户端秘钥
*/
private String clientSecret;
/**
* 登录类型
*/
@TableField(typeHandler = JacksonTypeHandler.class)
private List<String> authType;
/**
* 客户端类型
*/
private String clientType;
/**
* Token最低活跃频率-1为不限制
*/
private Integer activeTimeout;
/**
* Token有效期默认30天单位
*/
private Integer timeout;
/**
* 状态1启用2禁用
*/
private DisEnableStatusEnum status;
}

View File

@@ -0,0 +1,75 @@
/*
* 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.system.model.query;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import top.continew.admin.common.enums.DisEnableStatusEnum;
import top.continew.starter.data.core.annotation.Query;
import top.continew.starter.data.core.enums.QueryType;
import java.io.Serial;
import java.io.Serializable;
import java.util.List;
/**
* 客户端管理查询条件
*
* @author MoChou
* @since 2024/12/03 16:04
*/
@Data
@Schema(description = "客户端管理查询条件")
public class ClientQuery implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/**
* 客户端Key
*/
@Schema(description = "客户端Key")
@Query(type = QueryType.EQ)
private String clientKey;
/**
* 客户端秘钥
*/
@Schema(description = "客户端秘钥")
@Query(type = QueryType.EQ)
private String clientSecret;
/**
* 登录类型
*/
@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)
private DisEnableStatusEnum status;
}

View File

@@ -0,0 +1,97 @@
/*
* 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.system.model.req;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import org.hibernate.validator.constraints.Length;
import top.continew.admin.common.enums.DisEnableStatusEnum;
import top.continew.starter.extension.crud.model.req.BaseReq;
import java.io.Serial;
import java.util.List;
/**
* 创建或修改客户端管理参数
*
* @author MoChou
* @since 2024/12/03 16:04
*/
@Data
@Schema(description = "创建或修改客户端管理参数")
public class ClientReq extends BaseReq {
@Serial
private static final long serialVersionUID = 1L;
/**
* 客户端ID
*/
@Schema(description = "客户端ID")
private String clientId;
/**
* 客户端Key
*/
@Schema(description = "客户端Key")
@NotBlank(message = "客户端Key不能为空")
@Length(max = 32, message = "客户端Key长度不能超过 {max} 个字符")
private String clientKey;
/**
* 客户端秘钥
*/
@Schema(description = "客户端秘钥")
@NotBlank(message = "客户端秘钥不能为空")
@Length(max = 255, message = "客户端秘钥长度不能超过 {max} 个字符")
private String clientSecret;
/**
* 登录类型
*/
@Schema(description = "登录类型")
@NotNull(message = "登录类型不能为空")
private List<String> authType;
/**
* 客户端类型
*/
@Schema(description = "客户端类型")
@NotBlank(message = "客户端类型不能为空")
@Length(max = 32, message = "客户端类型长度不能超过 {max} 个字符")
private String clientType;
/**
* Token最低活跃频率-1为不限制
*/
@Schema(description = "Token最低活跃频率-1为不限制")
private Integer activeTimeout;
/**
* Token有效期默认30天单位
*/
@Schema(description = "Token有效期默认30天单位")
private Integer timeout;
/**
* 状态1启用2禁用
*/
@Schema(description = "状态1启用2禁用")
@NotNull(message = "状态1启用2禁用不能为空")
private DisEnableStatusEnum status;
}

View File

@@ -0,0 +1,98 @@
/*
* 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.system.model.resp;
import com.alibaba.excel.annotation.ExcelIgnoreUnannotated;
import com.alibaba.excel.annotation.ExcelProperty;
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 java.io.Serial;
import java.util.List;
/**
* 客户端管理详情信息
*
* @author MoChou
* @since 2024/12/03 16:04
*/
@Data
@ExcelIgnoreUnannotated
@Schema(description = "客户端管理详情信息")
public class ClientDetailResp extends BaseDetailResp {
@Serial
private static final long serialVersionUID = 1L;
/**
* 客户端ID
*/
@Schema(description = "客户端ID")
@ExcelProperty(value = "客户端ID")
private String clientId;
/**
* 客户端Key
*/
@Schema(description = "客户端Key")
@ExcelProperty(value = "客户端Key")
private String clientKey;
/**
* 客户端秘钥
*/
@Schema(description = "客户端秘钥")
@ExcelProperty(value = "客户端秘钥")
private String clientSecret;
/**
* 登录类型
*/
@Schema(description = "登录类型")
@ExcelProperty(value = "登录类型")
private List<String> authType;
/**
* 客户端类型
*/
@Schema(description = "客户端类型")
@ExcelProperty(value = "客户端类型")
private String clientType;
/**
* Token最低活跃频率-1为不限制
*/
@Schema(description = "Token最低活跃频率-1为不限制")
@ExcelProperty(value = "Token最低活跃频率-1为不限制")
private Integer activeTimeout;
/**
* Token有效期默认30天单位
*/
@Schema(description = "Token有效期默认30天单位")
@ExcelProperty(value = "Token有效期默认30天单位")
private Integer timeout;
/**
* 状态
*/
@Schema(description = "状态")
@ExcelProperty(value = "状态")
private DisEnableStatusEnum status;
}

View File

@@ -0,0 +1,87 @@
/*
* 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.system.model.resp;
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.BaseResp;
import java.io.Serial;
import java.util.List;
/**
* 客户端管理信息
*
* @author MoChou
* @since 2024/12/03 16:04
*/
@Data
@Schema(description = "客户端管理信息")
public class ClientResp extends BaseResp {
@Serial
private static final long serialVersionUID = 1L;
/**
* 客户端ID
*/
@Schema(description = "客户端ID")
private String clientId;
/**
* 客户端Key
*/
@Schema(description = "客户端Key")
private String clientKey;
/**
* 客户端秘钥
*/
@Schema(description = "客户端秘钥")
private String clientSecret;
/**
* 认证类型
*/
@Schema(description = "认证类型")
private List<String> authType;
/**
* 客户端类型
*/
@Schema(description = "客户端类型")
private String clientType;
/**
* Token最低活跃频率-1为不限制
*/
@Schema(description = "Token最低活跃频率-1为不限制")
private Integer activeTimeout;
/**
* Token有效期默认30天单位
*/
@Schema(description = "Token有效期默认30天单位")
private Integer timeout;
/**
* 状态1启用2禁用
*/
@Schema(description = "状态")
private DisEnableStatusEnum status;
}

View File

@@ -0,0 +1,35 @@
/*
* 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.system.service;
import top.continew.admin.system.model.query.ClientQuery;
import top.continew.admin.system.model.req.ClientReq;
import top.continew.admin.system.model.resp.ClientDetailResp;
import top.continew.admin.system.model.resp.ClientResp;
import top.continew.starter.extension.crud.service.BaseService;
/**
* 客户端管理业务接口
*
* @author MoChou
* @since 2024/12/03 16:04
*/
public interface ClientService extends BaseService<ClientResp, ClientDetailResp, ClientQuery, ClientReq> {
ClientResp getClientByClientId(String clientId);
}

View File

@@ -0,0 +1,76 @@
/*
* 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.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;
import top.continew.admin.system.model.entity.ClientDO;
import top.continew.admin.system.model.query.ClientQuery;
import top.continew.admin.system.model.req.ClientReq;
import top.continew.admin.system.model.resp.ClientDetailResp;
import top.continew.admin.system.model.resp.ClientResp;
import top.continew.admin.system.service.ClientService;
import top.continew.starter.core.constant.StringConstants;
import top.continew.starter.core.validation.ValidationUtils;
import top.continew.starter.extension.crud.service.BaseServiceImpl;
import java.util.List;
/**
* 客户端管理业务实现
*
* @author MoChou
* @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) {
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);
}
@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);
}
}

View File

@@ -28,7 +28,8 @@ import cn.hutool.json.JSONUtil;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpHeaders;
import org.springframework.scheduling.annotation.Async;
import top.continew.admin.auth.model.req.AccountLoginReq;
import top.continew.admin.auth.enums.AuthTypeEnum;
import top.continew.admin.auth.model.req.AccountAuthReq;
import top.continew.admin.common.constant.SysConstants;
import top.continew.admin.system.enums.LogStatusEnum;
import top.continew.admin.system.mapper.LogMapper;
@@ -145,9 +146,12 @@ public class LogDaoLocalImpl implements LogDao {
// 解析登录接口信息
if (requestUri.startsWith(SysConstants.LOGIN_URI) && LogStatusEnum.SUCCESS.equals(logDO.getStatus())) {
String requestBody = logRequest.getBody();
AccountLoginReq loginReq = JSONUtil.toBean(requestBody, AccountLoginReq.class);
logDO.setCreateUser(ExceptionUtils.exToNull(() -> userService.getByUsername(loginReq.getUsername())
.getId()));
//账号登录设置操作人
if (requestBody.contains(AuthTypeEnum.ACCOUNT.getValue())) {
AccountAuthReq loginReq = JSONUtil.toBean(requestBody, AccountAuthReq.class);
logDO.setCreateUser(ExceptionUtils.exToNull(() -> userService.getByUsername(loginReq.getUsername())
.getId()));
}
return;
}
// 解析 Token 信息

View File

@@ -19,31 +19,29 @@ 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.model.req.AccountLoginReq;
import top.continew.admin.auth.model.req.EmailLoginReq;
import top.continew.admin.auth.model.req.PhoneLoginReq;
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.common.constant.CacheConstants;
import top.continew.admin.common.constant.SysConstants;
import top.continew.admin.common.context.UserContext;
import top.continew.admin.common.context.UserContextHolder;
import top.continew.admin.common.util.SecureUtils;
import top.continew.admin.system.model.resp.ClientResp;
import top.continew.admin.system.model.resp.user.UserDetailResp;
import top.continew.admin.system.service.OptionService;
import top.continew.admin.system.service.ClientService;
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.exception.BusinessException;
import top.continew.starter.core.validation.ValidationUtils;
import top.continew.starter.log.annotation.Log;
@@ -55,67 +53,41 @@ import java.util.List;
* @author Charles7c
* @since 2022/12/21 20:37
*/
@Slf4j
@Log(module = "登录")
@Tag(name = "认证 API")
@RestController
@RequiredArgsConstructor
@RequestMapping("/auth")
public class AuthController {
private final ClientService clientService;
private static final String CAPTCHA_EXPIRED = "验证码已失效";
private static final String CAPTCHA_ERROR = "验证码错误";
private final OptionService optionService;
private final LoginService loginService;
private final UserService userService;
private final LoginService loginService;
private final AuthHandlerContext authHandlerContext;
@SaIgnore
@Operation(summary = "账号登录", description = "根据账号和密码进行登录认证")
@PostMapping("/account")
public LoginResp accountLogin(@Validated @RequestBody AccountLoginReq loginReq, HttpServletRequest request) {
// 校验验证码
int loginCaptchaEnabled = optionService.getValueByCode2Int("LOGIN_CAPTCHA_ENABLED");
if (SysConstants.YES.equals(loginCaptchaEnabled)) {
ValidationUtils.throwIfBlank(loginReq.getCaptcha(), "验证码不能为空");
ValidationUtils.throwIfBlank(loginReq.getUuid(), "验证码标识不能为空");
String captchaKey = CacheConstants.CAPTCHA_KEY_PREFIX + loginReq.getUuid();
String captcha = RedisUtils.get(captchaKey);
ValidationUtils.throwIfBlank(captcha, CAPTCHA_EXPIRED);
RedisUtils.delete(captchaKey);
ValidationUtils.throwIfNotEqualIgnoreCase(loginReq.getCaptcha(), captcha, CAPTCHA_ERROR);
@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());
}
// 用户登录
String rawPassword = ExceptionUtils.exToNull(() -> SecureUtils.decryptByRsaPrivateKey(loginReq.getPassword()));
ValidationUtils.throwIfBlank(rawPassword, "密码解密失败");
String token = loginService.accountLogin(loginReq.getUsername(), rawPassword, request);
return LoginResp.builder().token(token).build();
}
@SaIgnore
@Operation(summary = "手机号登录", description = "根据手机号和验证码进行登录认证")
@PostMapping("/phone")
public LoginResp phoneLogin(@Validated @RequestBody PhoneLoginReq loginReq) {
String phone = loginReq.getPhone();
String captchaKey = CacheConstants.CAPTCHA_KEY_PREFIX + phone;
String captcha = RedisUtils.get(captchaKey);
ValidationUtils.throwIfBlank(captcha, CAPTCHA_EXPIRED);
ValidationUtils.throwIfNotEqualIgnoreCase(loginReq.getCaptcha(), captcha, CAPTCHA_ERROR);
RedisUtils.delete(captchaKey);
String token = loginService.phoneLogin(phone);
return LoginResp.builder().token(token).build();
}
@SaIgnore
@Operation(summary = "邮箱登录", description = "根据邮箱和验证码进行登录认证")
@PostMapping("/email")
public LoginResp emailLogin(@Validated @RequestBody EmailLoginReq loginReq) {
String email = loginReq.getEmail();
String captchaKey = CacheConstants.CAPTCHA_KEY_PREFIX + email;
String captcha = RedisUtils.get(captchaKey);
ValidationUtils.throwIfBlank(captcha, CAPTCHA_EXPIRED);
ValidationUtils.throwIfNotEqualIgnoreCase(loginReq.getCaptcha(), captcha, CAPTCHA_ERROR);
RedisUtils.delete(captchaKey);
String token = loginService.emailLogin(email);
return LoginResp.builder().token(token).build();
}
@Operation(summary = "用户退出", description = "注销用户的当前登录")
@@ -146,4 +118,4 @@ public class AuthController {
public List<RouteResp> listRoute() {
return loginService.buildRouteTree(UserContextHolder.getUserId());
}
}
}

View File

@@ -0,0 +1,40 @@
/*
* 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.controller.system;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.web.bind.annotation.RestController;
import top.continew.admin.common.base.BaseController;
import top.continew.admin.system.model.query.ClientQuery;
import top.continew.admin.system.model.req.ClientReq;
import top.continew.admin.system.model.resp.ClientDetailResp;
import top.continew.admin.system.model.resp.ClientResp;
import top.continew.admin.system.service.ClientService;
import top.continew.starter.extension.crud.annotation.CrudRequestMapping;
import top.continew.starter.extension.crud.enums.Api;
/**
* 客户端管理管理 API
*
* @author MoChou
* @since 2024/12/03 16:04
*/
@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

@@ -188,3 +188,30 @@ 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`)
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');

View File

@@ -294,3 +294,20 @@ CREATE TABLE IF NOT EXISTS `sys_file` (
INDEX `idx_create_user`(`create_user`),
INDEX `idx_update_user`(`update_user`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='文件表';
CREATE TABLE IF NOT EXISTS `sys_client` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'ID',
`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 '客户端类型',
`active_timeout` int DEFAULT '-1' COMMENT 'Token最低活跃频率-1为不限制',
`timeout` int DEFAULT '2592000' COMMENT 'Token有效期默认30天单位',
`status` tinyint(1) UNSIGNED NOT NULL 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='客户端管理';