| | |
| | | import cn.iocoder.yudao.module.system.controller.admin.auth.vo.*; |
| | | import cn.iocoder.yudao.module.system.convert.auth.AuthConvert; |
| | | import cn.iocoder.yudao.module.system.dal.dataobject.oauth2.OAuth2AccessTokenDO; |
| | | import cn.iocoder.yudao.module.system.dal.dataobject.permission.RoleDO; |
| | | import cn.iocoder.yudao.module.system.dal.dataobject.user.AdminUserDO; |
| | | import cn.iocoder.yudao.module.system.dal.mysql.user.AdminUserMapper; |
| | | 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.event.SocialLoginEvent; |
| | | import cn.iocoder.yudao.module.system.service.logger.LoginLogService; |
| | | import cn.iocoder.yudao.module.system.service.member.MemberService; |
| | | import cn.iocoder.yudao.module.system.service.oauth2.OAuth2TokenService; |
| | | import cn.iocoder.yudao.module.system.service.permission.PermissionService; |
| | | import cn.iocoder.yudao.module.system.service.permission.RoleService; |
| | | import cn.iocoder.yudao.module.system.service.social.SocialUserService; |
| | | import cn.iocoder.yudao.module.system.service.user.AdminUserService; |
| | | import com.anji.captcha.model.common.ResponseModel; |
| | |
| | | import lombok.Setter; |
| | | import lombok.extern.slf4j.Slf4j; |
| | | import org.springframework.beans.factory.annotation.Value; |
| | | import org.springframework.context.ApplicationEventPublisher; |
| | | import org.springframework.security.crypto.password.PasswordEncoder; |
| | | import org.springframework.stereotype.Service; |
| | | import org.springframework.transaction.annotation.Transactional; |
| | | |
| | | import java.util.Objects; |
| | | import java.util.Set; |
| | | import java.util.UUID; |
| | | |
| | | import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; |
| | | import static cn.iocoder.yudao.framework.common.util.servlet.ServletUtils.getClientIP; |
| | |
| | | @Slf4j |
| | | public class AdminAuthServiceImpl implements AdminAuthService { |
| | | |
| | | /** |
| | | * 企业微信社交类型,对应 {@link cn.iocoder.yudao.module.system.enums.social.SocialTypeEnum#WECHAT_ENTERPRISE} |
| | | */ |
| | | private static final Integer WECHAT_ENTERPRISE_TYPE = 30; |
| | | |
| | | /** |
| | | * 企业微信扫码自动注册的账号,默认绑定的角色编码。 |
| | | * |
| | | * 需要在「系统管理 → 角色管理」里建好该编码的角色并勾选菜单,否则新账号登录后没有任何菜单。 |
| | | * 编码而非角色编号:角色编号在本地/测试/生产各库不一致,写死会在换库后静默失效。 |
| | | */ |
| | | private static final String WXCP_DEFAULT_ROLE_CODE = "wxcp_user"; |
| | | |
| | | @Resource |
| | | private AdminUserService userService; |
| | | @Resource |
| | | private AdminUserMapper userMapper; |
| | | @Resource |
| | | private LoginLogService loginLogService; |
| | | @Resource |
| | |
| | | private CaptchaService captchaService; |
| | | @Resource |
| | | private SmsCodeApi smsCodeApi; |
| | | @Resource |
| | | private PasswordEncoder passwordEncoder; |
| | | @Resource |
| | | private ApplicationEventPublisher eventPublisher; |
| | | @Resource |
| | | private RoleService roleService; |
| | | @Resource |
| | | private PermissionService permissionService; |
| | | |
| | | /** |
| | | * 验证码的开关,默认为 true |
| | |
| | | reqVO.getSocialType(), reqVO.getSocialCode(), reqVO.getSocialState())); |
| | | } |
| | | // 创建 Token 令牌,记录登录日志 |
| | | return createTokenAfterLoginSuccess(user.getId(), reqVO.getUsername(), LoginLogTypeEnum.LOGIN_USERNAME); |
| | | AuthLoginRespVO result = createTokenAfterLoginSuccess(user.getId(), reqVO.getUsername(), LoginLogTypeEnum.LOGIN_USERNAME); |
| | | |
| | | // 发布社交登录事件(绑定新社交账号时) |
| | | if (reqVO.getSocialType() != null) { |
| | | eventPublisher.publishEvent(new SocialLoginEvent( |
| | | reqVO.getSocialType(), user.getId(), user.getUsername(), "账号密码绑定")); |
| | | } |
| | | |
| | | return result; |
| | | } |
| | | |
| | | @Override |
| | |
| | | } |
| | | |
| | | @Override |
| | | @Transactional(rollbackFor = Exception.class) |
| | | public AuthLoginRespVO socialLogin(AuthSocialLoginReqVO reqVO) { |
| | | // 使用 code 授权码,进行登录。然后,获得到绑定的用户编号 |
| | | // 1. 使用 code 授权码,获取社交用户信息 |
| | | SocialUserRespDTO socialUser = socialUserService.getSocialUserByCode(UserTypeEnum.ADMIN.getValue(), reqVO.getType(), |
| | | reqVO.getCode(), reqVO.getState()); |
| | | if (socialUser == null || socialUser.getUserId() == null) { |
| | | if (socialUser == null) { |
| | | throw exception(AUTH_THIRD_LOGIN_NOT_BIND); |
| | | } |
| | | |
| | | // 获得用户 |
| | | AdminUserDO user = userService.getUser(socialUser.getUserId()); |
| | | if (user == null) { |
| | | throw exception(USER_NOT_EXISTS); |
| | | // 2. 判断是否已有绑定的系统用户 |
| | | Long bindUserId = socialUser.getUserId(); |
| | | AdminUserDO user; |
| | | |
| | | if (bindUserId == null) { |
| | | // 2.1 首次登录:自动注册新用户 |
| | | user = autoRegisterUser(socialUser, reqVO.getType()); |
| | | // 绑定社交账号 |
| | | socialUserService.bindSocialUser(new SocialUserBindReqDTO(user.getId(), getUserType().getValue(), |
| | | reqVO.getType(), reqVO.getCode(), reqVO.getState())); |
| | | log.info("[socialLogin][社交登录自动注册 userId={} username={} socialType={}]", |
| | | user.getId(), user.getUsername(), reqVO.getType()); |
| | | } else { |
| | | // 2.2 已有绑定:直接获取用户 |
| | | user = userService.getUser(bindUserId); |
| | | if (user == null) { |
| | | throw exception(USER_NOT_EXISTS); |
| | | } |
| | | } |
| | | |
| | | // 创建 Token 令牌,记录登录日志 |
| | | return createTokenAfterLoginSuccess(user.getId(), user.getUsername(), LoginLogTypeEnum.LOGIN_SOCIAL); |
| | | // 3. 创建 Token 令牌,记录登录日志 |
| | | AuthLoginRespVO result = createTokenAfterLoginSuccess(user.getId(), user.getUsername(), LoginLogTypeEnum.LOGIN_SOCIAL); |
| | | |
| | | // 4. 发布社交登录事件(供其他模块监听,如发送企业微信通知) |
| | | eventPublisher.publishEvent(new SocialLoginEvent( |
| | | reqVO.getType(), user.getId(), user.getUsername(), "社交扫码")); |
| | | |
| | | return result; |
| | | } |
| | | |
| | | /** |
| | | * 社交登录自动注册用户 |
| | | * |
| | | * 当用户首次通过社交账号(如企业微信)扫码且未绑定时,自动创建系统用户 |
| | | * |
| | | * @param socialUser 社交用户信息 |
| | | * @param socialType 社交类型 |
| | | * @return 新创建的用户 |
| | | */ |
| | | private AdminUserDO autoRegisterUser(SocialUserRespDTO socialUser, Integer socialType) { |
| | | // 1. 自动生成用户名(wx_ + 8位随机十六进制) |
| | | String rawUsername = "wx_" + UUID.randomUUID().toString().replaceAll("-", "").substring(0, 8); |
| | | |
| | | // 2. 确保用户名唯一(如果冲突则重试) |
| | | String username = rawUsername; |
| | | int retry = 0; |
| | | while (userService.getUserByUsername(username) != null && retry < 5) { |
| | | username = "wx_" + UUID.randomUUID().toString().replaceAll("-", "").substring(0, 8); |
| | | retry++; |
| | | } |
| | | if (retry >= 5) { |
| | | throw exception(USER_USERNAME_EXISTS); |
| | | } |
| | | |
| | | // 3. 生成随机密码 |
| | | String password = UUID.randomUUID().toString().replaceAll("-", "").substring(0, 16); |
| | | |
| | | // 4. 创建用户 |
| | | AdminUserDO user = new AdminUserDO(); |
| | | user.setUsername(username); |
| | | user.setNickname(socialUser.getNickname() != null ? socialUser.getNickname() : username); |
| | | user.setPassword(passwordEncoder.encode(password)); |
| | | user.setStatus(CommonStatusEnum.ENABLE.getStatus()); |
| | | userMapper.insert(user); |
| | | |
| | | // 5. 绑定默认角色 |
| | | // 不绑的话账号能登录,但 get-permission-info 返回空菜单,进去一片空白; |
| | | // 这种"能进但什么都看不到"的状态不会报错,很难定位,所以注册时就绑上 |
| | | bindDefaultRole(user); |
| | | |
| | | return user; |
| | | } |
| | | |
| | | /** |
| | | * 给自动注册的账号绑定默认角色 |
| | | * |
| | | * 角色不存在时只打警告、不抛异常:一个配置遗漏不应该让扫码登录整条链路不可用, |
| | | * 而且警告日志里已经写明了去哪儿补。 |
| | | * |
| | | * @param user 自动注册出来的用户 |
| | | */ |
| | | private void bindDefaultRole(AdminUserDO user) { |
| | | RoleDO role = roleService.getRoleByCode(WXCP_DEFAULT_ROLE_CODE); |
| | | if (role == null) { |
| | | log.warn("[bindDefaultRole][角色编码({}) 不存在,用户({}) 将没有任何菜单权限;" + |
| | | "请在「系统管理 → 角色管理」新建编码为 {} 的角色并分配菜单]", |
| | | WXCP_DEFAULT_ROLE_CODE, user.getUsername(), WXCP_DEFAULT_ROLE_CODE); |
| | | return; |
| | | } |
| | | permissionService.assignUserRole(user.getId(), Set.of(role.getId())); |
| | | log.info("[bindDefaultRole][用户({}) 已绑定默认角色({}/{})]", |
| | | user.getUsername(), role.getCode(), role.getName()); |
| | | } |
| | | |
| | | @VisibleForTesting |
| | |
| | | |
| | | userService.updateUserPassword(userByMobile.getId(), reqVO.getPassword()); |
| | | } |
| | | } |
| | | } |