feat(system): 管理后台微信小程序一键登录接口
新增 /system/auth/weixin-mini-app-login 端点,通过微信手机号授权 匹配管理员账号并自动绑定,含绑定冲突检测: - 同一微信已绑定其他管理员 → 拒绝 - 同一管理员已绑定其他微信 → 拒绝 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,31 @@
|
||||
package com.viewsh.module.system.controller.admin.auth.vo;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.validation.constraints.NotEmpty;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
@Schema(description = "管理后台 - 微信小程序手机登录 Request VO")
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Builder
|
||||
public class AuthWeixinMiniAppLoginReqVO {
|
||||
|
||||
@Schema(description = "手机 code,小程序通过 wx.getPhoneNumber 方法获得",
|
||||
requiredMode = Schema.RequiredMode.REQUIRED, example = "xxxx")
|
||||
@NotEmpty(message = "手机 code 不能为空")
|
||||
private String phoneCode;
|
||||
|
||||
@Schema(description = "登录 code,小程序通过 wx.login 方法获得",
|
||||
requiredMode = Schema.RequiredMode.REQUIRED, example = "yyyy")
|
||||
@NotEmpty(message = "登录 code 不能为空")
|
||||
private String loginCode;
|
||||
|
||||
@Schema(description = "state", requiredMode = Schema.RequiredMode.REQUIRED,
|
||||
example = "9b2ffbc1-7425-4155-9894-9d5c08541d62")
|
||||
@NotEmpty(message = "state 不能为空")
|
||||
private String state;
|
||||
}
|
||||
@@ -1,87 +1,97 @@
|
||||
package com.viewsh.module.system.service.auth;
|
||||
|
||||
import com.viewsh.module.system.controller.admin.auth.vo.*;
|
||||
import com.viewsh.module.system.dal.dataobject.user.AdminUserDO;
|
||||
import jakarta.validation.Valid;
|
||||
|
||||
/**
|
||||
* 管理后台的认证 Service 接口
|
||||
*
|
||||
* 提供用户的登录、登出的能力
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
public interface AdminAuthService {
|
||||
|
||||
/**
|
||||
* 验证账号 + 密码。如果通过,则返回用户
|
||||
*
|
||||
* @param username 账号
|
||||
* @param password 密码
|
||||
* @return 用户
|
||||
*/
|
||||
AdminUserDO authenticate(String username, String password);
|
||||
|
||||
/**
|
||||
* 账号登录
|
||||
*
|
||||
* @param reqVO 登录信息
|
||||
* @return 登录结果
|
||||
*/
|
||||
AuthLoginRespVO login(@Valid AuthLoginReqVO reqVO);
|
||||
|
||||
/**
|
||||
* 基于 token 退出登录
|
||||
*
|
||||
* @param token token
|
||||
* @param logType 登出类型
|
||||
*/
|
||||
void logout(String token, Integer logType);
|
||||
|
||||
/**
|
||||
* 短信验证码发送
|
||||
*
|
||||
* @param reqVO 发送请求
|
||||
*/
|
||||
void sendSmsCode(AuthSmsSendReqVO reqVO);
|
||||
|
||||
/**
|
||||
* 短信登录
|
||||
*
|
||||
* @param reqVO 登录信息
|
||||
* @return 登录结果
|
||||
*/
|
||||
AuthLoginRespVO smsLogin(AuthSmsLoginReqVO reqVO);
|
||||
|
||||
/**
|
||||
* 社交快捷登录,使用 code 授权码
|
||||
*
|
||||
* @param reqVO 登录信息
|
||||
* @return 登录结果
|
||||
*/
|
||||
AuthLoginRespVO socialLogin(@Valid AuthSocialLoginReqVO reqVO);
|
||||
|
||||
/**
|
||||
* 刷新访问令牌
|
||||
*
|
||||
* @param refreshToken 刷新令牌
|
||||
* @return 登录结果
|
||||
*/
|
||||
AuthLoginRespVO refreshToken(String refreshToken);
|
||||
|
||||
/**
|
||||
* 用户注册
|
||||
*
|
||||
* @param createReqVO 注册用户
|
||||
* @return 注册结果
|
||||
*/
|
||||
AuthLoginRespVO register(AuthRegisterReqVO createReqVO);
|
||||
|
||||
/**
|
||||
* 重置密码
|
||||
*
|
||||
* @param reqVO 验证码信息
|
||||
*/
|
||||
void resetPassword(AuthResetPasswordReqVO reqVO);
|
||||
|
||||
}
|
||||
package com.viewsh.module.system.service.auth;
|
||||
|
||||
import com.viewsh.module.system.controller.admin.auth.vo.*;
|
||||
import com.viewsh.module.system.dal.dataobject.user.AdminUserDO;
|
||||
import jakarta.validation.Valid;
|
||||
|
||||
/**
|
||||
* 管理后台的认证 Service 接口
|
||||
*
|
||||
* 提供用户的登录、登出的能力
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
public interface AdminAuthService {
|
||||
|
||||
/**
|
||||
* 验证账号 + 密码。如果通过,则返回用户
|
||||
*
|
||||
* @param username 账号
|
||||
* @param password 密码
|
||||
* @return 用户
|
||||
*/
|
||||
AdminUserDO authenticate(String username, String password);
|
||||
|
||||
/**
|
||||
* 账号登录
|
||||
*
|
||||
* @param reqVO 登录信息
|
||||
* @return 登录结果
|
||||
*/
|
||||
AuthLoginRespVO login(@Valid AuthLoginReqVO reqVO);
|
||||
|
||||
/**
|
||||
* 基于 token 退出登录
|
||||
*
|
||||
* @param token token
|
||||
* @param logType 登出类型
|
||||
*/
|
||||
void logout(String token, Integer logType);
|
||||
|
||||
/**
|
||||
* 短信验证码发送
|
||||
*
|
||||
* @param reqVO 发送请求
|
||||
*/
|
||||
void sendSmsCode(AuthSmsSendReqVO reqVO);
|
||||
|
||||
/**
|
||||
* 短信登录
|
||||
*
|
||||
* @param reqVO 登录信息
|
||||
* @return 登录结果
|
||||
*/
|
||||
AuthLoginRespVO smsLogin(AuthSmsLoginReqVO reqVO);
|
||||
|
||||
/**
|
||||
* 社交快捷登录,使用 code 授权码
|
||||
*
|
||||
* @param reqVO 登录信息
|
||||
* @return 登录结果
|
||||
*/
|
||||
AuthLoginRespVO socialLogin(@Valid AuthSocialLoginReqVO reqVO);
|
||||
|
||||
/**
|
||||
* 刷新访问令牌
|
||||
*
|
||||
* @param refreshToken 刷新令牌
|
||||
* @return 登录结果
|
||||
*/
|
||||
AuthLoginRespVO refreshToken(String refreshToken);
|
||||
|
||||
/**
|
||||
* 用户注册
|
||||
*
|
||||
* @param createReqVO 注册用户
|
||||
* @return 注册结果
|
||||
*/
|
||||
AuthLoginRespVO register(AuthRegisterReqVO createReqVO);
|
||||
|
||||
/**
|
||||
* 重置密码
|
||||
*
|
||||
* @param reqVO 验证码信息
|
||||
*/
|
||||
void resetPassword(AuthResetPasswordReqVO reqVO);
|
||||
|
||||
/**
|
||||
* 微信小程序一键登录(管理后台)
|
||||
*
|
||||
* 通过手机号匹配管理员账号,并绑定微信社交账号
|
||||
*
|
||||
* @param reqVO 登录信息
|
||||
* @return 登录结果
|
||||
*/
|
||||
AuthLoginRespVO weixinMiniAppLogin(@Valid AuthWeixinMiniAppLoginReqVO reqVO);
|
||||
|
||||
}
|
||||
|
||||
@@ -1,306 +1,361 @@
|
||||
package com.viewsh.module.system.service.auth;
|
||||
|
||||
import cn.hutool.core.util.ObjectUtil;
|
||||
import com.viewsh.framework.common.enums.CommonStatusEnum;
|
||||
import com.viewsh.framework.common.enums.UserTypeEnum;
|
||||
import com.viewsh.framework.common.util.monitor.TracerUtils;
|
||||
import com.viewsh.framework.common.util.object.BeanUtils;
|
||||
import com.viewsh.framework.common.util.servlet.ServletUtils;
|
||||
import com.viewsh.framework.common.util.validation.ValidationUtils;
|
||||
import com.viewsh.framework.datapermission.core.annotation.DataPermission;
|
||||
import com.viewsh.module.system.api.logger.dto.LoginLogCreateReqDTO;
|
||||
import com.viewsh.module.system.api.sms.SmsCodeApi;
|
||||
import com.viewsh.module.system.api.sms.dto.code.SmsCodeUseReqDTO;
|
||||
import com.viewsh.module.system.api.social.dto.SocialUserBindReqDTO;
|
||||
import com.viewsh.module.system.api.social.dto.SocialUserRespDTO;
|
||||
import com.viewsh.module.system.controller.admin.auth.vo.*;
|
||||
import com.viewsh.module.system.convert.auth.AuthConvert;
|
||||
import com.viewsh.module.system.dal.dataobject.oauth2.OAuth2AccessTokenDO;
|
||||
import com.viewsh.module.system.dal.dataobject.user.AdminUserDO;
|
||||
import com.viewsh.module.system.enums.logger.LoginLogTypeEnum;
|
||||
import com.viewsh.module.system.enums.logger.LoginResultEnum;
|
||||
import com.viewsh.module.system.enums.oauth2.OAuth2ClientConstants;
|
||||
import com.viewsh.module.system.enums.sms.SmsSceneEnum;
|
||||
import com.viewsh.module.system.service.logger.LoginLogService;
|
||||
import com.viewsh.module.system.service.member.MemberService;
|
||||
import com.viewsh.module.system.service.oauth2.OAuth2TokenService;
|
||||
import com.viewsh.module.system.service.social.SocialUserService;
|
||||
import com.viewsh.module.system.service.user.AdminUserService;
|
||||
import com.anji.captcha.model.common.ResponseModel;
|
||||
import com.anji.captcha.model.vo.CaptchaVO;
|
||||
import com.anji.captcha.service.CaptchaService;
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
import jakarta.annotation.Resource;
|
||||
import jakarta.validation.Validator;
|
||||
import lombok.Setter;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
import static com.viewsh.framework.common.exception.util.ServiceExceptionUtil.exception;
|
||||
import static com.viewsh.framework.common.util.servlet.ServletUtils.getClientIP;
|
||||
import static com.viewsh.module.system.enums.ErrorCodeConstants.*;
|
||||
|
||||
/**
|
||||
* Auth Service 实现类
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@Service
|
||||
@Slf4j
|
||||
public class AdminAuthServiceImpl implements AdminAuthService {
|
||||
|
||||
@Resource
|
||||
private AdminUserService userService;
|
||||
@Resource
|
||||
private LoginLogService loginLogService;
|
||||
@Resource
|
||||
private OAuth2TokenService oauth2TokenService;
|
||||
@Resource
|
||||
private SocialUserService socialUserService;
|
||||
@Resource
|
||||
private MemberService memberService;
|
||||
@Resource
|
||||
private Validator validator;
|
||||
@Resource
|
||||
private CaptchaService captchaService;
|
||||
@Resource
|
||||
private SmsCodeApi smsCodeApi;
|
||||
|
||||
/**
|
||||
* 验证码的开关,默认为 true
|
||||
*/
|
||||
@Value("${viewsh.captcha.enable:true}")
|
||||
@Setter // 为了单测:开启或者关闭验证码
|
||||
private Boolean captchaEnable;
|
||||
|
||||
@Override
|
||||
public AdminUserDO authenticate(String username, String password) {
|
||||
final LoginLogTypeEnum logTypeEnum = LoginLogTypeEnum.LOGIN_USERNAME;
|
||||
// 校验账号是否存在
|
||||
AdminUserDO user = userService.getUserByUsername(username);
|
||||
if (user == null) {
|
||||
createLoginLog(null, username, logTypeEnum, LoginResultEnum.BAD_CREDENTIALS);
|
||||
throw exception(AUTH_LOGIN_BAD_CREDENTIALS);
|
||||
}
|
||||
if (!userService.isPasswordMatch(password, user.getPassword())) {
|
||||
createLoginLog(user.getId(), username, logTypeEnum, LoginResultEnum.BAD_CREDENTIALS);
|
||||
throw exception(AUTH_LOGIN_BAD_CREDENTIALS);
|
||||
}
|
||||
// 校验是否禁用
|
||||
if (CommonStatusEnum.isDisable(user.getStatus())) {
|
||||
createLoginLog(user.getId(), username, logTypeEnum, LoginResultEnum.USER_DISABLED);
|
||||
throw exception(AUTH_LOGIN_USER_DISABLED);
|
||||
}
|
||||
return user;
|
||||
}
|
||||
|
||||
@Override
|
||||
@DataPermission(enable = false)
|
||||
public AuthLoginRespVO login(AuthLoginReqVO reqVO) {
|
||||
// 校验验证码
|
||||
validateCaptcha(reqVO);
|
||||
|
||||
// 使用账号密码,进行登录
|
||||
AdminUserDO user = authenticate(reqVO.getUsername(), reqVO.getPassword());
|
||||
|
||||
// 如果 socialType 非空,说明需要绑定社交用户
|
||||
if (reqVO.getSocialType() != null) {
|
||||
socialUserService.bindSocialUser(new SocialUserBindReqDTO(user.getId(), getUserType().getValue(),
|
||||
reqVO.getSocialType(), reqVO.getSocialCode(), reqVO.getSocialState()));
|
||||
}
|
||||
// 创建 Token 令牌,记录登录日志
|
||||
return createTokenAfterLoginSuccess(user.getId(), reqVO.getUsername(), LoginLogTypeEnum.LOGIN_USERNAME);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void sendSmsCode(AuthSmsSendReqVO reqVO) {
|
||||
// 如果是重置密码场景,需要校验图形验证码是否正确
|
||||
if (Objects.equals(SmsSceneEnum.ADMIN_MEMBER_RESET_PASSWORD.getScene(), reqVO.getScene())) {
|
||||
ResponseModel response = doValidateCaptcha(reqVO);
|
||||
if (!response.isSuccess()) {
|
||||
throw exception(AUTH_REGISTER_CAPTCHA_CODE_ERROR, response.getRepMsg());
|
||||
}
|
||||
}
|
||||
|
||||
// 登录场景,验证是否存在
|
||||
if (userService.getUserByMobile(reqVO.getMobile()) == null) {
|
||||
throw exception(AUTH_MOBILE_NOT_EXISTS);
|
||||
}
|
||||
// 发送验证码
|
||||
smsCodeApi.sendSmsCode(AuthConvert.INSTANCE.convert(reqVO).setCreateIp(getClientIP()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public AuthLoginRespVO smsLogin(AuthSmsLoginReqVO reqVO) {
|
||||
// 校验验证码
|
||||
smsCodeApi.useSmsCode(AuthConvert.INSTANCE.convert(reqVO, SmsSceneEnum.ADMIN_MEMBER_LOGIN.getScene(), getClientIP())).checkError();
|
||||
|
||||
// 获得用户信息
|
||||
AdminUserDO user = userService.getUserByMobile(reqVO.getMobile());
|
||||
if (user == null) {
|
||||
throw exception(USER_NOT_EXISTS);
|
||||
}
|
||||
|
||||
// 创建 Token 令牌,记录登录日志
|
||||
return createTokenAfterLoginSuccess(user.getId(), reqVO.getMobile(), LoginLogTypeEnum.LOGIN_MOBILE);
|
||||
}
|
||||
|
||||
private void createLoginLog(Long userId, String username,
|
||||
LoginLogTypeEnum logTypeEnum, LoginResultEnum loginResult) {
|
||||
// 插入登录日志
|
||||
LoginLogCreateReqDTO reqDTO = new LoginLogCreateReqDTO();
|
||||
reqDTO.setLogType(logTypeEnum.getType());
|
||||
reqDTO.setTraceId(TracerUtils.getTraceId());
|
||||
reqDTO.setUserId(userId);
|
||||
reqDTO.setUserType(getUserType().getValue());
|
||||
reqDTO.setUsername(username);
|
||||
reqDTO.setUserAgent(ServletUtils.getUserAgent());
|
||||
reqDTO.setUserIp(ServletUtils.getClientIP());
|
||||
reqDTO.setResult(loginResult.getResult());
|
||||
loginLogService.createLoginLog(reqDTO);
|
||||
// 更新最后登录时间
|
||||
if (userId != null && Objects.equals(LoginResultEnum.SUCCESS.getResult(), loginResult.getResult())) {
|
||||
userService.updateUserLogin(userId, ServletUtils.getClientIP());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public AuthLoginRespVO socialLogin(AuthSocialLoginReqVO reqVO) {
|
||||
// 使用 code 授权码,进行登录。然后,获得到绑定的用户编号
|
||||
SocialUserRespDTO socialUser = socialUserService.getSocialUserByCode(UserTypeEnum.ADMIN.getValue(), reqVO.getType(),
|
||||
reqVO.getCode(), reqVO.getState());
|
||||
if (socialUser == null || socialUser.getUserId() == null) {
|
||||
throw exception(AUTH_THIRD_LOGIN_NOT_BIND);
|
||||
}
|
||||
|
||||
// 获得用户
|
||||
AdminUserDO user = userService.getUser(socialUser.getUserId());
|
||||
if (user == null) {
|
||||
throw exception(USER_NOT_EXISTS);
|
||||
}
|
||||
|
||||
// 创建 Token 令牌,记录登录日志
|
||||
return createTokenAfterLoginSuccess(user.getId(), user.getUsername(), LoginLogTypeEnum.LOGIN_SOCIAL);
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
void validateCaptcha(AuthLoginReqVO reqVO) {
|
||||
ResponseModel response = doValidateCaptcha(reqVO);
|
||||
// 校验验证码
|
||||
if (!response.isSuccess()) {
|
||||
// 创建登录失败日志(验证码不正确)
|
||||
createLoginLog(null, reqVO.getUsername(), LoginLogTypeEnum.LOGIN_USERNAME, LoginResultEnum.CAPTCHA_CODE_ERROR);
|
||||
throw exception(AUTH_LOGIN_CAPTCHA_CODE_ERROR, response.getRepMsg());
|
||||
}
|
||||
}
|
||||
|
||||
private ResponseModel doValidateCaptcha(CaptchaVerificationReqVO reqVO) {
|
||||
// 如果验证码关闭,则不进行校验
|
||||
if (!captchaEnable) {
|
||||
return ResponseModel.success();
|
||||
}
|
||||
ValidationUtils.validate(validator, reqVO, CaptchaVerificationReqVO.CodeEnableGroup.class);
|
||||
CaptchaVO captchaVO = new CaptchaVO();
|
||||
captchaVO.setCaptchaVerification(reqVO.getCaptchaVerification());
|
||||
return captchaService.verification(captchaVO);
|
||||
}
|
||||
|
||||
private AuthLoginRespVO createTokenAfterLoginSuccess(Long userId, String username, LoginLogTypeEnum logType) {
|
||||
// 插入登陆日志
|
||||
createLoginLog(userId, username, logType, LoginResultEnum.SUCCESS);
|
||||
// 创建访问令牌
|
||||
OAuth2AccessTokenDO accessTokenDO = oauth2TokenService.createAccessToken(userId, getUserType().getValue(),
|
||||
OAuth2ClientConstants.CLIENT_ID_DEFAULT, null);
|
||||
// 构建返回结果
|
||||
return BeanUtils.toBean(accessTokenDO, AuthLoginRespVO.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
public AuthLoginRespVO refreshToken(String refreshToken) {
|
||||
OAuth2AccessTokenDO accessTokenDO = oauth2TokenService.refreshAccessToken(refreshToken, OAuth2ClientConstants.CLIENT_ID_DEFAULT);
|
||||
return BeanUtils.toBean(accessTokenDO, AuthLoginRespVO.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void logout(String token, Integer logType) {
|
||||
// 删除访问令牌
|
||||
OAuth2AccessTokenDO accessTokenDO = oauth2TokenService.removeAccessToken(token);
|
||||
if (accessTokenDO == null) {
|
||||
return;
|
||||
}
|
||||
// 删除成功,则记录登出日志
|
||||
createLogoutLog(accessTokenDO.getUserId(), accessTokenDO.getUserType(), logType);
|
||||
}
|
||||
|
||||
private void createLogoutLog(Long userId, Integer userType, Integer logType) {
|
||||
LoginLogCreateReqDTO reqDTO = new LoginLogCreateReqDTO();
|
||||
reqDTO.setLogType(logType);
|
||||
reqDTO.setTraceId(TracerUtils.getTraceId());
|
||||
reqDTO.setUserId(userId);
|
||||
reqDTO.setUserType(userType);
|
||||
if (ObjectUtil.equal(getUserType().getValue(), userType)) {
|
||||
reqDTO.setUsername(getUsername(userId));
|
||||
} else {
|
||||
reqDTO.setUsername(memberService.getMemberUserMobile(userId));
|
||||
}
|
||||
reqDTO.setUserAgent(ServletUtils.getUserAgent());
|
||||
reqDTO.setUserIp(ServletUtils.getClientIP());
|
||||
reqDTO.setResult(LoginResultEnum.SUCCESS.getResult());
|
||||
loginLogService.createLoginLog(reqDTO);
|
||||
}
|
||||
|
||||
private String getUsername(Long userId) {
|
||||
if (userId == null) {
|
||||
return null;
|
||||
}
|
||||
AdminUserDO user = userService.getUser(userId);
|
||||
return user != null ? user.getUsername() : null;
|
||||
}
|
||||
|
||||
private UserTypeEnum getUserType() {
|
||||
return UserTypeEnum.ADMIN;
|
||||
}
|
||||
|
||||
@Override
|
||||
public AuthLoginRespVO register(AuthRegisterReqVO registerReqVO) {
|
||||
// 1. 校验验证码
|
||||
validateCaptcha(registerReqVO);
|
||||
|
||||
// 2. 校验用户名是否已存在
|
||||
Long userId = userService.registerUser(registerReqVO);
|
||||
|
||||
// 3. 创建 Token 令牌,记录登录日志
|
||||
return createTokenAfterLoginSuccess(userId, registerReqVO.getUsername(), LoginLogTypeEnum.LOGIN_USERNAME);
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
void validateCaptcha(AuthRegisterReqVO reqVO) {
|
||||
ResponseModel response = doValidateCaptcha(reqVO);
|
||||
// 验证不通过
|
||||
if (!response.isSuccess()) {
|
||||
throw exception(AUTH_REGISTER_CAPTCHA_CODE_ERROR, response.getRepMsg());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void resetPassword(AuthResetPasswordReqVO reqVO) {
|
||||
AdminUserDO userByMobile = userService.getUserByMobile(reqVO.getMobile());
|
||||
if (userByMobile == null) {
|
||||
throw exception(USER_MOBILE_NOT_EXISTS);
|
||||
}
|
||||
|
||||
smsCodeApi.useSmsCode(new SmsCodeUseReqDTO()
|
||||
.setCode(reqVO.getCode())
|
||||
.setMobile(reqVO.getMobile())
|
||||
.setScene(SmsSceneEnum.ADMIN_MEMBER_RESET_PASSWORD.getScene())
|
||||
.setUsedIp(getClientIP())
|
||||
).checkError();
|
||||
|
||||
userService.updateUserPassword(userByMobile.getId(), reqVO.getPassword());
|
||||
}
|
||||
}
|
||||
package com.viewsh.module.system.service.auth;
|
||||
|
||||
import cn.hutool.core.util.ObjectUtil;
|
||||
import com.viewsh.framework.common.enums.CommonStatusEnum;
|
||||
import com.viewsh.framework.common.enums.UserTypeEnum;
|
||||
import com.viewsh.framework.common.util.monitor.TracerUtils;
|
||||
import com.viewsh.framework.common.util.object.BeanUtils;
|
||||
import com.viewsh.framework.common.util.servlet.ServletUtils;
|
||||
import com.viewsh.framework.common.util.validation.ValidationUtils;
|
||||
import com.viewsh.framework.datapermission.core.annotation.DataPermission;
|
||||
import com.viewsh.module.system.api.logger.dto.LoginLogCreateReqDTO;
|
||||
import com.viewsh.module.system.api.sms.SmsCodeApi;
|
||||
import com.viewsh.module.system.api.sms.dto.code.SmsCodeUseReqDTO;
|
||||
import com.viewsh.module.system.api.social.dto.SocialUserBindReqDTO;
|
||||
import com.viewsh.module.system.api.social.dto.SocialUserRespDTO;
|
||||
import com.viewsh.module.system.controller.admin.auth.vo.*;
|
||||
import com.viewsh.module.system.convert.auth.AuthConvert;
|
||||
import com.viewsh.module.system.dal.dataobject.oauth2.OAuth2AccessTokenDO;
|
||||
import com.viewsh.module.system.dal.dataobject.user.AdminUserDO;
|
||||
import com.viewsh.module.system.enums.logger.LoginLogTypeEnum;
|
||||
import com.viewsh.module.system.enums.logger.LoginResultEnum;
|
||||
import com.viewsh.module.system.enums.oauth2.OAuth2ClientConstants;
|
||||
import com.viewsh.module.system.enums.sms.SmsSceneEnum;
|
||||
import com.viewsh.module.system.service.logger.LoginLogService;
|
||||
import com.viewsh.module.system.service.member.MemberService;
|
||||
import com.viewsh.module.system.service.social.SocialClientService;
|
||||
import com.viewsh.module.system.service.oauth2.OAuth2TokenService;
|
||||
import com.viewsh.module.system.service.social.SocialUserService;
|
||||
import com.viewsh.module.system.service.user.AdminUserService;
|
||||
import com.anji.captcha.model.common.ResponseModel;
|
||||
import com.anji.captcha.model.vo.CaptchaVO;
|
||||
import com.anji.captcha.service.CaptchaService;
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
import jakarta.annotation.Resource;
|
||||
import jakarta.validation.Validator;
|
||||
import lombok.Setter;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import cn.binarywang.wx.miniapp.bean.WxMaPhoneNumberInfo;
|
||||
import cn.hutool.core.lang.Assert;
|
||||
import com.viewsh.module.system.enums.social.SocialTypeEnum;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
import static com.viewsh.framework.common.exception.util.ServiceExceptionUtil.exception;
|
||||
import static com.viewsh.framework.common.util.servlet.ServletUtils.getClientIP;
|
||||
import static com.viewsh.module.system.enums.ErrorCodeConstants.*;
|
||||
|
||||
/**
|
||||
* Auth Service 实现类
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@Service
|
||||
@Slf4j
|
||||
public class AdminAuthServiceImpl implements AdminAuthService {
|
||||
|
||||
@Resource
|
||||
private AdminUserService userService;
|
||||
@Resource
|
||||
private LoginLogService loginLogService;
|
||||
@Resource
|
||||
private OAuth2TokenService oauth2TokenService;
|
||||
@Resource
|
||||
private SocialUserService socialUserService;
|
||||
@Resource
|
||||
private MemberService memberService;
|
||||
@Resource
|
||||
private SocialClientService socialClientService;
|
||||
@Resource
|
||||
private Validator validator;
|
||||
@Resource
|
||||
private CaptchaService captchaService;
|
||||
@Resource
|
||||
private SmsCodeApi smsCodeApi;
|
||||
|
||||
/**
|
||||
* 验证码的开关,默认为 true
|
||||
*/
|
||||
@Value("${viewsh.captcha.enable:true}")
|
||||
@Setter // 为了单测:开启或者关闭验证码
|
||||
private Boolean captchaEnable;
|
||||
|
||||
@Override
|
||||
public AdminUserDO authenticate(String username, String password) {
|
||||
final LoginLogTypeEnum logTypeEnum = LoginLogTypeEnum.LOGIN_USERNAME;
|
||||
// 校验账号是否存在
|
||||
AdminUserDO user = userService.getUserByUsername(username);
|
||||
if (user == null) {
|
||||
createLoginLog(null, username, logTypeEnum, LoginResultEnum.BAD_CREDENTIALS);
|
||||
throw exception(AUTH_LOGIN_BAD_CREDENTIALS);
|
||||
}
|
||||
if (!userService.isPasswordMatch(password, user.getPassword())) {
|
||||
createLoginLog(user.getId(), username, logTypeEnum, LoginResultEnum.BAD_CREDENTIALS);
|
||||
throw exception(AUTH_LOGIN_BAD_CREDENTIALS);
|
||||
}
|
||||
// 校验是否禁用
|
||||
if (CommonStatusEnum.isDisable(user.getStatus())) {
|
||||
createLoginLog(user.getId(), username, logTypeEnum, LoginResultEnum.USER_DISABLED);
|
||||
throw exception(AUTH_LOGIN_USER_DISABLED);
|
||||
}
|
||||
return user;
|
||||
}
|
||||
|
||||
@Override
|
||||
@DataPermission(enable = false)
|
||||
public AuthLoginRespVO login(AuthLoginReqVO reqVO) {
|
||||
// 校验验证码
|
||||
validateCaptcha(reqVO);
|
||||
|
||||
// 使用账号密码,进行登录
|
||||
AdminUserDO user = authenticate(reqVO.getUsername(), reqVO.getPassword());
|
||||
|
||||
// 如果 socialType 非空,说明需要绑定社交用户
|
||||
if (reqVO.getSocialType() != null) {
|
||||
socialUserService.bindSocialUser(new SocialUserBindReqDTO(user.getId(), getUserType().getValue(),
|
||||
reqVO.getSocialType(), reqVO.getSocialCode(), reqVO.getSocialState()));
|
||||
}
|
||||
// 创建 Token 令牌,记录登录日志
|
||||
return createTokenAfterLoginSuccess(user.getId(), reqVO.getUsername(), LoginLogTypeEnum.LOGIN_USERNAME);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void sendSmsCode(AuthSmsSendReqVO reqVO) {
|
||||
// 如果是重置密码场景,需要校验图形验证码是否正确
|
||||
if (Objects.equals(SmsSceneEnum.ADMIN_MEMBER_RESET_PASSWORD.getScene(), reqVO.getScene())) {
|
||||
ResponseModel response = doValidateCaptcha(reqVO);
|
||||
if (!response.isSuccess()) {
|
||||
throw exception(AUTH_REGISTER_CAPTCHA_CODE_ERROR, response.getRepMsg());
|
||||
}
|
||||
}
|
||||
|
||||
// 登录场景,验证是否存在
|
||||
if (userService.getUserByMobile(reqVO.getMobile()) == null) {
|
||||
throw exception(AUTH_MOBILE_NOT_EXISTS);
|
||||
}
|
||||
// 发送验证码
|
||||
smsCodeApi.sendSmsCode(AuthConvert.INSTANCE.convert(reqVO).setCreateIp(getClientIP()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public AuthLoginRespVO smsLogin(AuthSmsLoginReqVO reqVO) {
|
||||
// 校验验证码
|
||||
smsCodeApi.useSmsCode(AuthConvert.INSTANCE.convert(reqVO, SmsSceneEnum.ADMIN_MEMBER_LOGIN.getScene(), getClientIP())).checkError();
|
||||
|
||||
// 获得用户信息
|
||||
AdminUserDO user = userService.getUserByMobile(reqVO.getMobile());
|
||||
if (user == null) {
|
||||
throw exception(USER_NOT_EXISTS);
|
||||
}
|
||||
|
||||
// 创建 Token 令牌,记录登录日志
|
||||
return createTokenAfterLoginSuccess(user.getId(), reqVO.getMobile(), LoginLogTypeEnum.LOGIN_MOBILE);
|
||||
}
|
||||
|
||||
private void createLoginLog(Long userId, String username,
|
||||
LoginLogTypeEnum logTypeEnum, LoginResultEnum loginResult) {
|
||||
// 插入登录日志
|
||||
LoginLogCreateReqDTO reqDTO = new LoginLogCreateReqDTO();
|
||||
reqDTO.setLogType(logTypeEnum.getType());
|
||||
reqDTO.setTraceId(TracerUtils.getTraceId());
|
||||
reqDTO.setUserId(userId);
|
||||
reqDTO.setUserType(getUserType().getValue());
|
||||
reqDTO.setUsername(username);
|
||||
reqDTO.setUserAgent(ServletUtils.getUserAgent());
|
||||
reqDTO.setUserIp(ServletUtils.getClientIP());
|
||||
reqDTO.setResult(loginResult.getResult());
|
||||
loginLogService.createLoginLog(reqDTO);
|
||||
// 更新最后登录时间
|
||||
if (userId != null && Objects.equals(LoginResultEnum.SUCCESS.getResult(), loginResult.getResult())) {
|
||||
userService.updateUserLogin(userId, ServletUtils.getClientIP());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public AuthLoginRespVO socialLogin(AuthSocialLoginReqVO reqVO) {
|
||||
// 使用 code 授权码,进行登录。然后,获得到绑定的用户编号
|
||||
SocialUserRespDTO socialUser = socialUserService.getSocialUserByCode(UserTypeEnum.ADMIN.getValue(), reqVO.getType(),
|
||||
reqVO.getCode(), reqVO.getState());
|
||||
if (socialUser == null || socialUser.getUserId() == null) {
|
||||
throw exception(AUTH_THIRD_LOGIN_NOT_BIND);
|
||||
}
|
||||
|
||||
// 获得用户
|
||||
AdminUserDO user = userService.getUser(socialUser.getUserId());
|
||||
if (user == null) {
|
||||
throw exception(USER_NOT_EXISTS);
|
||||
}
|
||||
|
||||
// 创建 Token 令牌,记录登录日志
|
||||
return createTokenAfterLoginSuccess(user.getId(), user.getUsername(), LoginLogTypeEnum.LOGIN_SOCIAL);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public AuthLoginRespVO weixinMiniAppLogin(AuthWeixinMiniAppLoginReqVO reqVO) {
|
||||
// 1. 通过 phoneCode 获取手机号
|
||||
WxMaPhoneNumberInfo phoneNumberInfo = socialClientService.getWxMaPhoneNumberInfo(
|
||||
UserTypeEnum.ADMIN.getValue(), reqVO.getPhoneCode());
|
||||
Assert.notNull(phoneNumberInfo, "获得手机信息失败,结果为空");
|
||||
String mobile = phoneNumberInfo.getPurePhoneNumber();
|
||||
|
||||
// 2. 通过手机号查找管理员
|
||||
AdminUserDO user = userService.getUserByMobile(mobile);
|
||||
if (user == null) {
|
||||
throw exception(AUTH_WEIXIN_MINI_APP_PHONE_NOT_FOUND);
|
||||
}
|
||||
if (CommonStatusEnum.isDisable(user.getStatus())) {
|
||||
throw exception(AUTH_LOGIN_USER_DISABLED);
|
||||
}
|
||||
|
||||
// 3. 通过 loginCode 获取社交用户(含 openid)
|
||||
SocialUserRespDTO socialUser = socialUserService.getSocialUserByCode(
|
||||
UserTypeEnum.ADMIN.getValue(), SocialTypeEnum.WECHAT_MINI_PROGRAM.getType(),
|
||||
reqVO.getLoginCode(), reqVO.getState());
|
||||
Assert.notNull(socialUser, "社交用户不能为空");
|
||||
|
||||
// 4. 绑定冲突检测
|
||||
// 4a. 当前 openid 是否已绑定其他管理员
|
||||
if (socialUser.getUserId() != null && !socialUser.getUserId().equals(user.getId())) {
|
||||
throw exception(AUTH_WEIXIN_MINI_APP_WECHAT_BINDTO_OTHER);
|
||||
}
|
||||
// 4b. 目标管理员是否已绑定其他微信
|
||||
SocialUserRespDTO existByUser = socialUserService.getSocialUserByUserId(
|
||||
UserTypeEnum.ADMIN.getValue(), user.getId(),
|
||||
SocialTypeEnum.WECHAT_MINI_PROGRAM.getType());
|
||||
if (existByUser != null && !existByUser.getOpenid().equals(socialUser.getOpenid())) {
|
||||
throw exception(AUTH_WEIXIN_MINI_APP_BINDTO_OTHER_WECHAT);
|
||||
}
|
||||
|
||||
// 5. 绑定社交用户(无冲突且未绑定时)
|
||||
if (existByUser == null) {
|
||||
socialUserService.bindSocialUser(new SocialUserBindReqDTO(user.getId(),
|
||||
getUserType().getValue(), SocialTypeEnum.WECHAT_MINI_PROGRAM.getType(),
|
||||
reqVO.getLoginCode(), reqVO.getState()));
|
||||
}
|
||||
|
||||
// 6. 创建 Token 令牌,记录登录日志
|
||||
return createTokenAfterLoginSuccess(user.getId(), user.getUsername(), LoginLogTypeEnum.LOGIN_SOCIAL);
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
void validateCaptcha(AuthLoginReqVO reqVO) {
|
||||
ResponseModel response = doValidateCaptcha(reqVO);
|
||||
// 校验验证码
|
||||
if (!response.isSuccess()) {
|
||||
// 创建登录失败日志(验证码不正确)
|
||||
createLoginLog(null, reqVO.getUsername(), LoginLogTypeEnum.LOGIN_USERNAME, LoginResultEnum.CAPTCHA_CODE_ERROR);
|
||||
throw exception(AUTH_LOGIN_CAPTCHA_CODE_ERROR, response.getRepMsg());
|
||||
}
|
||||
}
|
||||
|
||||
private ResponseModel doValidateCaptcha(CaptchaVerificationReqVO reqVO) {
|
||||
// 如果验证码关闭,则不进行校验
|
||||
if (!captchaEnable) {
|
||||
return ResponseModel.success();
|
||||
}
|
||||
ValidationUtils.validate(validator, reqVO, CaptchaVerificationReqVO.CodeEnableGroup.class);
|
||||
CaptchaVO captchaVO = new CaptchaVO();
|
||||
captchaVO.setCaptchaVerification(reqVO.getCaptchaVerification());
|
||||
return captchaService.verification(captchaVO);
|
||||
}
|
||||
|
||||
private AuthLoginRespVO createTokenAfterLoginSuccess(Long userId, String username, LoginLogTypeEnum logType) {
|
||||
// 插入登陆日志
|
||||
createLoginLog(userId, username, logType, LoginResultEnum.SUCCESS);
|
||||
// 创建访问令牌
|
||||
OAuth2AccessTokenDO accessTokenDO = oauth2TokenService.createAccessToken(userId, getUserType().getValue(),
|
||||
OAuth2ClientConstants.CLIENT_ID_DEFAULT, null);
|
||||
// 构建返回结果
|
||||
return BeanUtils.toBean(accessTokenDO, AuthLoginRespVO.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
public AuthLoginRespVO refreshToken(String refreshToken) {
|
||||
OAuth2AccessTokenDO accessTokenDO = oauth2TokenService.refreshAccessToken(refreshToken, OAuth2ClientConstants.CLIENT_ID_DEFAULT);
|
||||
return BeanUtils.toBean(accessTokenDO, AuthLoginRespVO.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void logout(String token, Integer logType) {
|
||||
// 删除访问令牌
|
||||
OAuth2AccessTokenDO accessTokenDO = oauth2TokenService.removeAccessToken(token);
|
||||
if (accessTokenDO == null) {
|
||||
return;
|
||||
}
|
||||
// 删除成功,则记录登出日志
|
||||
createLogoutLog(accessTokenDO.getUserId(), accessTokenDO.getUserType(), logType);
|
||||
}
|
||||
|
||||
private void createLogoutLog(Long userId, Integer userType, Integer logType) {
|
||||
LoginLogCreateReqDTO reqDTO = new LoginLogCreateReqDTO();
|
||||
reqDTO.setLogType(logType);
|
||||
reqDTO.setTraceId(TracerUtils.getTraceId());
|
||||
reqDTO.setUserId(userId);
|
||||
reqDTO.setUserType(userType);
|
||||
if (ObjectUtil.equal(getUserType().getValue(), userType)) {
|
||||
reqDTO.setUsername(getUsername(userId));
|
||||
} else {
|
||||
reqDTO.setUsername(memberService.getMemberUserMobile(userId));
|
||||
}
|
||||
reqDTO.setUserAgent(ServletUtils.getUserAgent());
|
||||
reqDTO.setUserIp(ServletUtils.getClientIP());
|
||||
reqDTO.setResult(LoginResultEnum.SUCCESS.getResult());
|
||||
loginLogService.createLoginLog(reqDTO);
|
||||
}
|
||||
|
||||
private String getUsername(Long userId) {
|
||||
if (userId == null) {
|
||||
return null;
|
||||
}
|
||||
AdminUserDO user = userService.getUser(userId);
|
||||
return user != null ? user.getUsername() : null;
|
||||
}
|
||||
|
||||
private UserTypeEnum getUserType() {
|
||||
return UserTypeEnum.ADMIN;
|
||||
}
|
||||
|
||||
@Override
|
||||
public AuthLoginRespVO register(AuthRegisterReqVO registerReqVO) {
|
||||
// 1. 校验验证码
|
||||
validateCaptcha(registerReqVO);
|
||||
|
||||
// 2. 校验用户名是否已存在
|
||||
Long userId = userService.registerUser(registerReqVO);
|
||||
|
||||
// 3. 创建 Token 令牌,记录登录日志
|
||||
return createTokenAfterLoginSuccess(userId, registerReqVO.getUsername(), LoginLogTypeEnum.LOGIN_USERNAME);
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
void validateCaptcha(AuthRegisterReqVO reqVO) {
|
||||
ResponseModel response = doValidateCaptcha(reqVO);
|
||||
// 验证不通过
|
||||
if (!response.isSuccess()) {
|
||||
throw exception(AUTH_REGISTER_CAPTCHA_CODE_ERROR, response.getRepMsg());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void resetPassword(AuthResetPasswordReqVO reqVO) {
|
||||
AdminUserDO userByMobile = userService.getUserByMobile(reqVO.getMobile());
|
||||
if (userByMobile == null) {
|
||||
throw exception(USER_MOBILE_NOT_EXISTS);
|
||||
}
|
||||
|
||||
smsCodeApi.useSmsCode(new SmsCodeUseReqDTO()
|
||||
.setCode(reqVO.getCode())
|
||||
.setMobile(reqVO.getMobile())
|
||||
.setScene(SmsSceneEnum.ADMIN_MEMBER_RESET_PASSWORD.getScene())
|
||||
.setUsedIp(getClientIP())
|
||||
).checkError();
|
||||
|
||||
userService.updateUserPassword(userByMobile.getId(), reqVO.getPassword());
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user