package cn.iocoder.yudao.module.member.service.auth; import cn.hutool.core.lang.Assert; import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; import cn.iocoder.yudao.framework.common.enums.TerminalEnum; import cn.iocoder.yudao.framework.common.enums.UserTypeEnum; import cn.iocoder.yudao.framework.common.util.monitor.TracerUtils; import cn.iocoder.yudao.framework.common.util.servlet.ServletUtils; import cn.iocoder.yudao.module.member.controller.app.auth.vo.*; import cn.iocoder.yudao.module.member.convert.auth.AuthConvert; import cn.iocoder.yudao.module.member.dal.dataobject.user.MemberUserDO; import cn.iocoder.yudao.module.member.service.user.MemberUserService; import cn.iocoder.yudao.module.system.api.logger.LoginLogApi; import cn.iocoder.yudao.module.system.api.logger.dto.LoginLogCreateReqDTO; import cn.iocoder.yudao.framework.common.biz.system.oauth2.OAuth2TokenCommonApi; import cn.iocoder.yudao.framework.common.biz.system.oauth2.dto.OAuth2AccessTokenCreateReqDTO; import cn.iocoder.yudao.framework.common.biz.system.oauth2.dto.OAuth2AccessTokenRespDTO; import cn.iocoder.yudao.module.system.api.sms.SmsCodeApi; import cn.iocoder.yudao.module.system.api.social.SocialClientApi; import cn.iocoder.yudao.module.system.api.social.SocialUserApi; import cn.iocoder.yudao.module.system.api.social.dto.SocialUserBindReqDTO; import cn.iocoder.yudao.module.system.api.social.dto.SocialUserRespDTO; import cn.iocoder.yudao.module.system.api.social.dto.SocialWxPhoneNumberInfoRespDTO; import cn.iocoder.yudao.module.system.enums.logger.LoginLogTypeEnum; import cn.iocoder.yudao.module.system.enums.logger.LoginResultEnum; import cn.iocoder.yudao.module.system.enums.oauth2.OAuth2ClientConstants; import cn.iocoder.yudao.module.system.enums.sms.SmsSceneEnum; import cn.iocoder.yudao.module.system.enums.social.SocialTypeEnum; import jakarta.annotation.Resource; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.util.Objects; import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; import static cn.iocoder.yudao.framework.common.util.servlet.ServletUtils.getClientIP; import static cn.iocoder.yudao.framework.web.core.util.WebFrameworkUtils.getTerminal; import static cn.iocoder.yudao.module.member.enums.ErrorCodeConstants.*; /** * 会员的认证 Service 接口 * * @author 芋道源码 */ @Service @Slf4j public class MemberAuthServiceImpl implements MemberAuthService { @Resource private MemberUserService userService; @Resource private SmsCodeApi smsCodeApi; @Resource private LoginLogApi loginLogApi; @Resource private SocialUserApi socialUserApi; @Resource private SocialClientApi socialClientApi; @Resource private OAuth2TokenCommonApi oauth2TokenApi; @Override public AppAuthLoginRespVO login(AppAuthLoginReqVO reqVO) { // 使用手机 + 密码,进行登录。 MemberUserDO user = login0(reqVO.getMobile(), reqVO.getPassword()); // 如果 socialType 非空,说明需要绑定社交用户 String openid = null; if (reqVO.getSocialType() != null) { openid = socialUserApi.bindSocialUser(new SocialUserBindReqDTO(user.getId(), getUserType().getValue(), reqVO.getSocialType(), reqVO.getSocialCode(), reqVO.getSocialState())); } // 创建 Token 令牌,记录登录日志 return createTokenAfterLoginSuccess(user, reqVO.getMobile(), LoginLogTypeEnum.LOGIN_MOBILE, openid); } @Override @Transactional public AppAuthLoginRespVO smsLogin(AppAuthSmsLoginReqVO reqVO) { // 校验验证码 String userIp = getClientIP(); smsCodeApi.useSmsCode(AuthConvert.INSTANCE.convert(reqVO, SmsSceneEnum.MEMBER_LOGIN.getScene(), userIp)); // 获得获得注册用户 MemberUserDO user = userService.createUserIfAbsent(reqVO.getMobile(), userIp, getTerminal()); Assert.notNull(user, "获取用户失败,结果为空"); // 校验是否禁用 if (CommonStatusEnum.isDisable(user.getStatus())) { createLoginLog(user.getId(), reqVO.getMobile(), LoginLogTypeEnum.LOGIN_SMS, LoginResultEnum.USER_DISABLED); throw exception(AUTH_LOGIN_USER_DISABLED); } // 如果 socialType 非空,说明需要绑定社交用户 String openid = null; if (reqVO.getSocialType() != null) { openid = socialUserApi.bindSocialUser(new SocialUserBindReqDTO(user.getId(), getUserType().getValue(), reqVO.getSocialType(), reqVO.getSocialCode(), reqVO.getSocialState())); } // 创建 Token 令牌,记录登录日志 return createTokenAfterLoginSuccess(user, reqVO.getMobile(), LoginLogTypeEnum.LOGIN_SMS, openid); } @Override @Transactional public AppAuthLoginRespVO socialLogin(AppAuthSocialLoginReqVO reqVO) { // 使用 code 授权码,进行登录。然后,获得到绑定的用户编号 SocialUserRespDTO socialUser = socialUserApi.getSocialUserByCode(UserTypeEnum.MEMBER.getValue(), reqVO.getType(), reqVO.getCode(), reqVO.getState()); if (socialUser == null) { throw exception(AUTH_SOCIAL_USER_NOT_FOUND); } // 情况一:已绑定,直接读取用户信息 MemberUserDO user; if (socialUser.getUserId() != null) { user = userService.getUser(socialUser.getUserId()); // 情况二:未绑定,注册用户 + 绑定用户 } else { user = userService.createUser(socialUser.getNickname(), socialUser.getAvatar(), getClientIP(), getTerminal()); socialUserApi.bindSocialUser(new SocialUserBindReqDTO(user.getId(), getUserType().getValue(), reqVO.getType(), reqVO.getCode(), reqVO.getState())); } if (user == null) { throw exception(USER_NOT_EXISTS); } // 创建 Token 令牌,记录登录日志 return createTokenAfterLoginSuccess(user, user.getMobile(), LoginLogTypeEnum.LOGIN_SOCIAL, socialUser.getOpenid()); } @Override public AppAuthLoginRespVO weixinMiniAppLogin(AppAuthWeixinMiniAppLoginReqVO reqVO) { // 获得对应的手机号信息 SocialWxPhoneNumberInfoRespDTO phoneNumberInfo = socialClientApi.getWxMaPhoneNumberInfo( UserTypeEnum.MEMBER.getValue(), reqVO.getPhoneCode()); Assert.notNull(phoneNumberInfo, "获得手机信息失败,结果为空"); // 获得获得注册用户 MemberUserDO user = userService.createUserIfAbsent(phoneNumberInfo.getPurePhoneNumber(), getClientIP(), TerminalEnum.WECHAT_MINI_PROGRAM.getTerminal()); Assert.notNull(user, "获取用户失败,结果为空"); // 绑定社交用户 String openid = socialUserApi.bindSocialUser(new SocialUserBindReqDTO(user.getId(), getUserType().getValue(), SocialTypeEnum.WECHAT_MINI_PROGRAM.getType(), reqVO.getLoginCode(), reqVO.getState())); // 创建 Token 令牌,记录登录日志 return createTokenAfterLoginSuccess(user, user.getMobile(), LoginLogTypeEnum.LOGIN_SOCIAL, openid); } private AppAuthLoginRespVO createTokenAfterLoginSuccess(MemberUserDO user, String mobile, LoginLogTypeEnum logType, String openid) { // 插入登陆日志 createLoginLog(user.getId(), mobile, logType, LoginResultEnum.SUCCESS); // 创建 Token 令牌 OAuth2AccessTokenRespDTO accessTokenRespDTO = oauth2TokenApi.createAccessToken(new OAuth2AccessTokenCreateReqDTO() .setUserId(user.getId()).setUserType(getUserType().getValue()) .setClientId(OAuth2ClientConstants.CLIENT_ID_DEFAULT)); // 构建返回结果 return AuthConvert.INSTANCE.convert(accessTokenRespDTO, openid); } @Override public String getSocialAuthorizeUrl(Integer type, String redirectUri) { return socialClientApi.getAuthorizeUrl(type, UserTypeEnum.MEMBER.getValue(), redirectUri); } private MemberUserDO login0(String mobile, String password) { final LoginLogTypeEnum logTypeEnum = LoginLogTypeEnum.LOGIN_MOBILE; // 校验账号是否存在 MemberUserDO user = userService.getUserByMobile(mobile); if (user == null) { createLoginLog(null, mobile, logTypeEnum, LoginResultEnum.BAD_CREDENTIALS); throw exception(AUTH_LOGIN_BAD_CREDENTIALS); } if (!userService.isPasswordMatch(password, user.getPassword())) { createLoginLog(user.getId(), mobile, logTypeEnum, LoginResultEnum.BAD_CREDENTIALS); throw exception(AUTH_LOGIN_BAD_CREDENTIALS); } // 校验是否禁用 if (CommonStatusEnum.isDisable(user.getStatus())) { createLoginLog(user.getId(), mobile, logTypeEnum, LoginResultEnum.USER_DISABLED); throw exception(AUTH_LOGIN_USER_DISABLED); } return user; } private void createLoginLog(Long userId, String mobile, LoginLogTypeEnum logType, LoginResultEnum loginResult) { // 插入登录日志 LoginLogCreateReqDTO reqDTO = new LoginLogCreateReqDTO(); reqDTO.setLogType(logType.getType()); reqDTO.setTraceId(TracerUtils.getTraceId()); reqDTO.setUserId(userId); reqDTO.setUserType(getUserType().getValue()); reqDTO.setUsername(mobile); reqDTO.setUserAgent(ServletUtils.getUserAgent()); reqDTO.setUserIp(getClientIP()); reqDTO.setResult(loginResult.getResult()); loginLogApi.createLoginLog(reqDTO); // 更新最后登录时间 if (userId != null && Objects.equals(LoginResultEnum.SUCCESS.getResult(), loginResult.getResult())) { userService.updateUserLogin(userId, getClientIP()); } } @Override public void logout(String token) { // 删除访问令牌 OAuth2AccessTokenRespDTO accessTokenRespDTO = oauth2TokenApi.removeAccessToken(token); if (accessTokenRespDTO == null) { return; } // 删除成功,则记录登出日志 createLogoutLog(accessTokenRespDTO.getUserId()); } @Override public void sendSmsCode(Long userId, AppAuthSmsSendReqVO reqVO) { // 情况 1:如果是修改手机场景,需要校验新手机号是否已经注册,说明不能使用该手机了 if (Objects.equals(reqVO.getScene(), SmsSceneEnum.MEMBER_UPDATE_MOBILE.getScene())) { MemberUserDO user = userService.getUserByMobile(reqVO.getMobile()); if (user != null && !Objects.equals(user.getId(), userId)) { throw exception(AUTH_MOBILE_USED); } } // 情况 2:如果是重置密码场景,需要校验手机号是存在的 if (Objects.equals(reqVO.getScene(), SmsSceneEnum.MEMBER_RESET_PASSWORD.getScene())) { MemberUserDO user = userService.getUserByMobile(reqVO.getMobile()); if (user == null) { throw exception(USER_MOBILE_NOT_EXISTS); } } // 情况 3:如果是修改密码场景,需要查询手机号,无需前端传递 if (Objects.equals(reqVO.getScene(), SmsSceneEnum.MEMBER_UPDATE_PASSWORD.getScene())) { MemberUserDO user = userService.getUser(userId); // TODO 芋艿:后续 member user 手机非强绑定,这块需要做下调整; reqVO.setMobile(user.getMobile()); } // 执行发送 smsCodeApi.sendSmsCode(AuthConvert.INSTANCE.convert(reqVO).setCreateIp(getClientIP())); } @Override public void validateSmsCode(Long userId, AppAuthSmsValidateReqVO reqVO) { smsCodeApi.validateSmsCode(AuthConvert.INSTANCE.convert(reqVO)); } @Override public AppAuthLoginRespVO refreshToken(String refreshToken) { OAuth2AccessTokenRespDTO accessTokenDO = oauth2TokenApi.refreshAccessToken(refreshToken, OAuth2ClientConstants.CLIENT_ID_DEFAULT); return AuthConvert.INSTANCE.convert(accessTokenDO, null); } private void createLogoutLog(Long userId) { LoginLogCreateReqDTO reqDTO = new LoginLogCreateReqDTO(); reqDTO.setLogType(LoginLogTypeEnum.LOGOUT_SELF.getType()); reqDTO.setTraceId(TracerUtils.getTraceId()); reqDTO.setUserId(userId); reqDTO.setUserType(getUserType().getValue()); reqDTO.setUsername(getMobile(userId)); reqDTO.setUserAgent(ServletUtils.getUserAgent()); reqDTO.setUserIp(getClientIP()); reqDTO.setResult(LoginResultEnum.SUCCESS.getResult()); loginLogApi.createLoginLog(reqDTO); } private String getMobile(Long userId) { if (userId == null) { return null; } MemberUserDO user = userService.getUser(userId); return user != null ? user.getMobile() : null; } private UserTypeEnum getUserType() { return UserTypeEnum.MEMBER; } }