Procházet zdrojové kódy

去除 Spring Security 的 Admin 的 loadUsername,使用自己定义的 login0 实现

YunaiV před 2 roky
rodič
revize
3bd7e8e682
10 změnil soubory, kde provedl 153 přidání a 186 odebrání
  1. 0 10
      yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/core/authentication/MultiUserDetailsAuthenticationProvider.java
  2. 0 7
      yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/core/service/SecurityAuthFrameworkService.java
  3. 7 0
      yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/service/auth/MemberAuthService.java
  4. 0 1
      yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/enums/ErrorCodeConstants.java
  5. 0 2
      yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/convert/auth/AuthConvert.java
  6. 9 2
      yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/auth/AdminAuthService.java
  7. 25 53
      yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/auth/AdminAuthServiceImpl.java
  8. 9 1
      yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/user/AdminUserService.java
  9. 19 9
      yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/user/AdminUserServiceImpl.java
  10. 84 101
      yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/service/auth/AuthServiceImplTest.java

+ 0 - 10
yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/core/authentication/MultiUserDetailsAuthenticationProvider.java

@@ -105,16 +105,6 @@ public class MultiUserDetailsAuthenticationProvider extends AbstractUserDetailsA
         return selectService(request).verifyTokenAndRefresh(token);
     }
 
-    /**
-     * 基于 token 退出登录
-     *
-     * @param request 请求
-     * @param token token
-     */
-    public void logout(HttpServletRequest request, String token) {
-        selectService(request).logout(token);
-    }
-
     private SecurityAuthFrameworkService selectService(HttpServletRequest request) {
         // 第一步,获得用户类型
         UserTypeEnum userType = getUserType(request);

+ 0 - 7
yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/core/service/SecurityAuthFrameworkService.java

@@ -21,13 +21,6 @@ public interface SecurityAuthFrameworkService extends UserDetailsService {
     LoginUser verifyTokenAndRefresh(String token);
 
     /**
-     * 基于 token 退出登录
-     *
-     * @param token token
-     */
-    void logout(String token);
-
-    /**
      * 获得用户类型。每个用户类型,对应一个 SecurityAuthFrameworkService 实现类。
      *
      * @return 用户类型

+ 7 - 0
yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/service/auth/MemberAuthService.java

@@ -25,6 +25,13 @@ public interface MemberAuthService extends SecurityAuthFrameworkService {
     String login(@Valid AppAuthLoginReqVO reqVO, String userIp, String userAgent);
 
     /**
+     * 基于 token 退出登录
+     *
+     * @param token token
+     */
+    void logout(String token);
+
+    /**
      * 手机 + 验证码登陆
      *
      * @param reqVO 登陆信息

+ 0 - 1
yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/enums/ErrorCodeConstants.java

@@ -12,7 +12,6 @@ public interface ErrorCodeConstants {
     // ========== AUTH 模块 1002000000 ==========
     ErrorCode AUTH_LOGIN_BAD_CREDENTIALS = new ErrorCode(1002000000, "登录失败,账号密码不正确");
     ErrorCode AUTH_LOGIN_USER_DISABLED = new ErrorCode(1002000001, "登录失败,账号被禁用");
-    ErrorCode AUTH_LOGIN_FAIL_UNKNOWN = new ErrorCode(1002000002, "登录失败"); // 登录失败的兜底,未知原因
     ErrorCode AUTH_LOGIN_CAPTCHA_NOT_FOUND = new ErrorCode(1002000003, "验证码不存在");
     ErrorCode AUTH_LOGIN_CAPTCHA_CODE_ERROR = new ErrorCode(1002000004, "验证码不正确");
     ErrorCode AUTH_THIRD_LOGIN_NOT_BIND = new ErrorCode(1002000005, "未绑定账号,需要进行绑定");

+ 0 - 2
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/convert/auth/AuthConvert.java

@@ -26,8 +26,6 @@ public interface AuthConvert {
 
     SpringSecurityUser convert2(AdminUserDO user);
 
-    LoginUser convert(SpringSecurityUser bean);
-
     default AuthPermissionInfoRespVO convert(AdminUserDO user, List<RoleDO> roleList, List<MenuDO> menuList) {
         return AuthPermissionInfoRespVO.builder()
             .user(AuthPermissionInfoRespVO.UserVO.builder().id(user.getId()).nickname(user.getNickname()).avatar(user.getAvatar()).build())

+ 9 - 2
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/auth/AdminAuthService.java

@@ -1,14 +1,14 @@
 package cn.iocoder.yudao.module.system.service.auth;
 
-import cn.iocoder.yudao.module.system.controller.admin.auth.vo.auth.*;
 import cn.iocoder.yudao.framework.security.core.service.SecurityAuthFrameworkService;
+import cn.iocoder.yudao.module.system.controller.admin.auth.vo.auth.*;
 
 import javax.validation.Valid;
 
 /**
  * 管理后台的认证 Service 接口
  *
- * 提供用户的账号密码登录、token 的校验等认证相关的功能
+ * 提供用户的登录、登出的能力
  *
  * @author 芋道源码
  */
@@ -25,6 +25,13 @@ public interface AdminAuthService extends SecurityAuthFrameworkService {
     String login(@Valid AuthLoginReqVO reqVO, String userIp, String userAgent);
 
     /**
+     * 基于 token 退出登录
+     *
+     * @param token token
+     */
+    void logout(String token);
+
+    /**
      * 短信验证码发送
      *
      * @param reqVO 发送请求

+ 25 - 53
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/auth/AdminAuthServiceImpl.java

@@ -1,12 +1,12 @@
 package cn.iocoder.yudao.module.system.service.auth;
 
+import cn.hutool.core.util.ObjectUtil;
+import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
 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.framework.common.util.validation.ValidationUtils;
 import cn.iocoder.yudao.framework.security.core.LoginUser;
-import cn.iocoder.yudao.framework.security.core.authentication.MultiUsernamePasswordAuthenticationToken;
-import cn.iocoder.yudao.framework.security.core.authentication.SpringSecurityUser;
 import cn.iocoder.yudao.module.system.api.logger.dto.LoginLogCreateReqDTO;
 import cn.iocoder.yudao.module.system.api.sms.SmsCodeApi;
 import cn.iocoder.yudao.module.system.controller.admin.auth.vo.auth.*;
@@ -19,18 +19,11 @@ import cn.iocoder.yudao.module.system.service.common.CaptchaService;
 import cn.iocoder.yudao.module.system.service.logger.LoginLogService;
 import cn.iocoder.yudao.module.system.service.social.SocialUserService;
 import cn.iocoder.yudao.module.system.service.user.AdminUserService;
+import com.google.common.annotations.VisibleForTesting;
 import lombok.extern.slf4j.Slf4j;
-import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.context.annotation.Lazy;
-import org.springframework.security.authentication.AuthenticationManager;
-import org.springframework.security.authentication.BadCredentialsException;
-import org.springframework.security.authentication.DisabledException;
-import org.springframework.security.core.Authentication;
-import org.springframework.security.core.AuthenticationException;
 import org.springframework.security.core.userdetails.UserDetails;
 import org.springframework.security.core.userdetails.UsernameNotFoundException;
 import org.springframework.stereotype.Service;
-import org.springframework.util.Assert;
 
 import javax.annotation.Resource;
 import javax.validation.Validator;
@@ -50,11 +43,6 @@ import static cn.iocoder.yudao.module.system.enums.ErrorCodeConstants.*;
 public class AdminAuthServiceImpl implements AdminAuthService {
 
     @Resource
-    @Lazy // 延迟加载,因为存在相互依赖的问题
-    private AuthenticationManager authenticationManager;
-
-    @Autowired
-    @SuppressWarnings("SpringJavaAutowiredFieldsWarningInspection") // UserService 存在重名
     private AdminUserService userService;
     @Resource
     private CaptchaService captchaService;
@@ -72,17 +60,6 @@ public class AdminAuthServiceImpl implements AdminAuthService {
     private SmsCodeApi smsCodeApi;
 
     @Override
-    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
-        // 获取 username 对应的 AdminUserDO
-        AdminUserDO user = userService.getUserByUsername(username);
-        if (user == null) {
-            throw new UsernameNotFoundException(username);
-        }
-        // 创建 LoginUser 对象
-        return AuthConvert.INSTANCE.convert2(user);
-    }
-
-    @Override
     public String login(AuthLoginReqVO reqVO, String userIp, String userAgent) {
         // 判断验证码是否正确
         verifyCaptcha(reqVO);
@@ -124,7 +101,8 @@ public class AdminAuthServiceImpl implements AdminAuthService {
                 LoginLogTypeEnum.LOGIN_MOBILE, userIp, userAgent);
     }
 
-    private void verifyCaptcha(AuthLoginReqVO reqVO) {
+    @VisibleForTesting
+    void verifyCaptcha(AuthLoginReqVO reqVO) {
         // 如果验证码关闭,则不进行校验
         if (!captchaService.isCaptchaEnable()) {
             return;
@@ -149,46 +127,36 @@ public class AdminAuthServiceImpl implements AdminAuthService {
         captchaService.deleteCaptchaCode(reqVO.getUuid());
     }
 
-    private LoginUser login0(String username, String password) {
+    @VisibleForTesting
+    LoginUser login0(String username, String password) {
         final LoginLogTypeEnum logTypeEnum = LoginLogTypeEnum.LOGIN_USERNAME;
-        // 用户验证
-        Authentication authentication;
-        try {
-            // 调用 Spring Security 的 AuthenticationManager#authenticate(...) 方法,使用账号密码进行认证
-            // 在其内部,会调用到 loadUserByUsername 方法,获取 User 信息
-            authentication = authenticationManager.authenticate(new MultiUsernamePasswordAuthenticationToken(
-                    username, password, getUserType()));
-        } catch (BadCredentialsException badCredentialsException) {
+        // 校验账号是否存在
+        AdminUserDO user = userService.getUserByUsername(username);
+        if (user == null) {
             createLoginLog(null, username, logTypeEnum, LoginResultEnum.BAD_CREDENTIALS);
             throw exception(AUTH_LOGIN_BAD_CREDENTIALS);
-        } catch (DisabledException disabledException) {
-            createLoginLog(null, username, logTypeEnum, LoginResultEnum.USER_DISABLED);
+        }
+        if (!userService.isPasswordMatch(password, user.getPassword())) {
+            createLoginLog(user.getId(), username, logTypeEnum, LoginResultEnum.BAD_CREDENTIALS);
+            throw exception(AUTH_LOGIN_BAD_CREDENTIALS);
+        }
+        // 校验是否禁用
+        if (ObjectUtil.notEqual(user.getStatus(), CommonStatusEnum.ENABLE.getStatus())) {
+            createLoginLog(user.getId(), username, logTypeEnum, LoginResultEnum.USER_DISABLED);
             throw exception(AUTH_LOGIN_USER_DISABLED);
-        } catch (AuthenticationException authenticationException) {
-            log.error("[login0][username({}) 发生未知异常]", username, authenticationException);
-            createLoginLog(null, username, logTypeEnum, LoginResultEnum.UNKNOWN_ERROR);
-            throw exception(AUTH_LOGIN_FAIL_UNKNOWN);
         }
-        Assert.notNull(authentication.getPrincipal(), "Principal 不会为空");
+
         // 构建 User 对象
-        return AuthConvert.INSTANCE.convert((SpringSecurityUser) authentication.getPrincipal())
-                .setUserType(getUserType().getValue());
+        return buildLoginUser(user);
     }
 
     private void createLoginLog(Long userId, String username,
                                 LoginLogTypeEnum logTypeEnum, LoginResultEnum loginResult) {
-        // 获得用户
-        if (userId == null) {
-            AdminUserDO user = userService.getUserByUsername(username);
-            userId = user != null ? user.getId() : null;
-        }
         // 插入登录日志
         LoginLogCreateReqDTO reqDTO = new LoginLogCreateReqDTO();
         reqDTO.setLogType(logTypeEnum.getType());
         reqDTO.setTraceId(TracerUtils.getTraceId());
-        if (userId != null) {
-            reqDTO.setUserId(userId);
-        }
+        reqDTO.setUserId(userId);
         reqDTO.setUserType(getUserType().getValue());
         reqDTO.setUsername(username);
         reqDTO.setUserAgent(ServletUtils.getUserAgent());
@@ -293,4 +261,8 @@ public class AdminAuthServiceImpl implements AdminAuthService {
         return user != null ? user.getUsername() : null;
     }
 
+    @Override
+    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
+        return null;
+    }
 }

+ 9 - 1
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/user/AdminUserService.java

@@ -105,7 +105,6 @@ public interface AdminUserService {
      */
     AdminUserDO getUserByMobile(String mobile);
 
-
     /**
      * 获得用户分页列表
      *
@@ -209,4 +208,13 @@ public interface AdminUserService {
      */
     List<AdminUserDO> getUsersByStatus(Integer status);
 
+    /**
+     * 判断密码是否匹配
+     *
+     * @param rawPassword 未加密的密码
+     * @param encodedPassword 加密后的密码
+     * @return 是否匹配
+     */
+    boolean isPasswordMatch(String rawPassword, String encodedPassword);
+
 }

+ 19 - 9
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/user/AdminUserServiceImpl.java

@@ -148,7 +148,7 @@ public class AdminUserServiceImpl implements AdminUserService {
         checkOldPassword(id, reqVO.getOldPassword());
         // 执行更新
         AdminUserDO updateObj = new AdminUserDO().setId(id);
-        updateObj.setPassword(passwordEncoder.encode(reqVO.getNewPassword())); // 加密密码
+        updateObj.setPassword(encodePassword(reqVO.getNewPassword())); // 加密密码
         userMapper.updateById(updateObj);
     }
 
@@ -172,7 +172,7 @@ public class AdminUserServiceImpl implements AdminUserService {
         // 更新密码
         AdminUserDO updateObj = new AdminUserDO();
         updateObj.setId(id);
-        updateObj.setPassword(passwordEncoder.encode(password)); // 加密密码
+        updateObj.setPassword(encodePassword(password)); // 加密密码
         userMapper.updateById(updateObj);
     }
 
@@ -205,11 +205,6 @@ public class AdminUserServiceImpl implements AdminUserService {
         return userMapper.selectByUsername(username);
     }
 
-    /**
-     * 通过手机号获取用户
-     * @param mobile
-     * @return
-     */
     @Override
     public AdminUserDO getUserByMobile(String mobile) {
         return userMapper.selectByMobile(mobile);
@@ -395,7 +390,7 @@ public class AdminUserServiceImpl implements AdminUserService {
         if (user == null) {
             throw exception(USER_NOT_EXISTS);
         }
-        if (!passwordEncoder.matches(oldPassword, user.getPassword())) {
+        if (!isPasswordMatch(oldPassword, user.getPassword())) {
             throw exception(USER_PASSWORD_FAILED);
         }
     }
@@ -421,7 +416,7 @@ public class AdminUserServiceImpl implements AdminUserService {
             AdminUserDO existUser = userMapper.selectByUsername(importUser.getUsername());
             if (existUser == null) {
                 userMapper.insert(UserConvert.INSTANCE.convert(importUser)
-                        .setPassword(passwordEncoder.encode(userInitPassword))); // 设置默认密码
+                        .setPassword(encodePassword(userInitPassword))); // 设置默认密码
                 respVO.getCreateUsernames().add(importUser.getUsername());
                 return;
             }
@@ -443,4 +438,19 @@ public class AdminUserServiceImpl implements AdminUserService {
         return userMapper.selectListByStatus(status);
     }
 
+    @Override
+    public boolean isPasswordMatch(String rawPassword, String encodedPassword) {
+        return passwordEncoder.matches(rawPassword, encodedPassword);
+    }
+
+    /**
+     * 对密码进行加密
+     *
+     * @param password 密码
+     * @return 加密后的密码
+     */
+    private String encodePassword(String password) {
+        return passwordEncoder.encode(password);
+    }
+
 }

+ 84 - 101
yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/service/auth/AuthServiceImplTest.java

@@ -1,5 +1,6 @@
 package cn.iocoder.yudao.module.system.service.auth;
 
+import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
 import cn.iocoder.yudao.framework.security.core.LoginUser;
 import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest;
 import cn.iocoder.yudao.framework.test.core.util.AssertUtils;
@@ -9,7 +10,6 @@ import cn.iocoder.yudao.module.system.dal.dataobject.user.AdminUserDO;
 import cn.iocoder.yudao.module.system.enums.logger.LoginLogTypeEnum;
 import cn.iocoder.yudao.module.system.enums.logger.LoginResultEnum;
 import cn.iocoder.yudao.module.system.service.common.CaptchaService;
-import cn.iocoder.yudao.module.system.service.dept.PostService;
 import cn.iocoder.yudao.module.system.service.logger.LoginLogService;
 import cn.iocoder.yudao.module.system.service.social.SocialUserService;
 import cn.iocoder.yudao.module.system.service.user.AdminUserService;
@@ -17,23 +17,16 @@ import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
 import org.springframework.boot.test.mock.mockito.MockBean;
 import org.springframework.context.annotation.Import;
-import org.springframework.security.authentication.AuthenticationManager;
-import org.springframework.security.authentication.BadCredentialsException;
-import org.springframework.security.authentication.DisabledException;
-import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
-import org.springframework.security.core.Authentication;
-import org.springframework.security.core.AuthenticationException;
-import org.springframework.security.core.userdetails.UsernameNotFoundException;
 
 import javax.annotation.Resource;
 import javax.validation.Validator;
-import java.util.Set;
 
+import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertPojoEquals;
 import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertServiceException;
-import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.*;
+import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomPojo;
+import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomString;
 import static cn.iocoder.yudao.module.system.enums.ErrorCodeConstants.*;
 import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.assertThrows;
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.*;
 
@@ -46,10 +39,6 @@ public class AuthServiceImplTest extends BaseDbUnitTest {
     @MockBean
     private AdminUserService userService;
     @MockBean
-    private AuthenticationManager authenticationManager;
-    @MockBean
-    private Authentication authentication;
-    @MockBean
     private CaptchaService captchaService;
     @MockBean
     private LoginLogService loginLogService;
@@ -58,8 +47,6 @@ public class AuthServiceImplTest extends BaseDbUnitTest {
     @MockBean
     private SocialUserService socialService;
     @MockBean
-    private PostService postService;
-    @MockBean
     private SmsCodeApi smsCodeApi;
 
     @MockBean
@@ -71,131 +58,124 @@ public class AuthServiceImplTest extends BaseDbUnitTest {
     }
 
     @Test
-    public void testLoadUserByUsername_success() {
+    public void testLogin0_success() {
         // 准备参数
         String username = randomString();
-        // mock 方法
-        AdminUserDO user = randomPojo(AdminUserDO.class, o -> o.setUsername(username));
+        String password = randomString();
+        // mock user 数据
+        AdminUserDO user = randomPojo(AdminUserDO.class, o -> o.setUsername(username)
+                .setPassword(password).setStatus(CommonStatusEnum.ENABLE.getStatus()));
         when(userService.getUserByUsername(eq(username))).thenReturn(user);
+        // mock password 匹配
+        when(userService.isPasswordMatch(eq(password), eq(user.getPassword()))).thenReturn(true);
 
         // 调用
-        LoginUser loginUser = (LoginUser) authService.loadUserByUsername(username);
+        LoginUser loginUser = authService.login0(username, password);
         // 校验
-        AssertUtils.assertPojoEquals(user, loginUser, "updateTime");
+        assertPojoEquals(user, loginUser);
     }
 
     @Test
-    public void testLoadUserByUsername_userNotFound() {
+    public void testLogin0_userNotFound() {
         // 准备参数
         String username = randomString();
-        // mock 方法
+        String password = randomString();
 
         // 调用, 并断言异常
-        assertThrows(UsernameNotFoundException.class, // 抛出 UsernameNotFoundException 异常
-                () -> authService.loadUserByUsername(username),
-                username); // 异常提示为 username
+        AssertUtils.assertServiceException(() -> authService.login0(username, password),
+                AUTH_LOGIN_BAD_CREDENTIALS);
+        verify(loginLogService).createLoginLog(
+                argThat(o -> o.getLogType().equals(LoginLogTypeEnum.LOGIN_USERNAME.getType())
+                        && o.getResult().equals(LoginResultEnum.BAD_CREDENTIALS.getResult())
+                        && o.getUserId() == null)
+        );
     }
 
     @Test
-    public void testLogin_captchaNotFound() {
+    public void testLogin0_badCredentials() {
         // 准备参数
-        AuthLoginReqVO reqVO = randomPojo(AuthLoginReqVO.class);
-        String userIp = randomString();
-        String userAgent = randomString();
+        String username = randomString();
+        String password = randomString();
+        // mock user 数据
+        AdminUserDO user = randomPojo(AdminUserDO.class, o -> o.setUsername(username)
+                .setPassword(password).setStatus(CommonStatusEnum.ENABLE.getStatus()));
+        when(userService.getUserByUsername(eq(username))).thenReturn(user);
 
         // 调用, 并断言异常
-        assertServiceException(() -> authService.login(reqVO, userIp, userAgent), AUTH_LOGIN_CAPTCHA_NOT_FOUND);
-        // 校验调用参数
-        verify(loginLogService, times(1)).createLoginLog(
-            argThat(o -> o.getLogType().equals(LoginLogTypeEnum.LOGIN_USERNAME.getType())
-                    && o.getResult().equals(LoginResultEnum.CAPTCHA_NOT_FOUND.getResult()))
+        AssertUtils.assertServiceException(() -> authService.login0(username, password),
+                AUTH_LOGIN_BAD_CREDENTIALS);
+        verify(loginLogService).createLoginLog(
+                argThat(o -> o.getLogType().equals(LoginLogTypeEnum.LOGIN_USERNAME.getType())
+                        && o.getResult().equals(LoginResultEnum.BAD_CREDENTIALS.getResult())
+                        && o.getUserId().equals(user.getId()))
         );
     }
 
     @Test
-    public void testLogin_captchaCodeError() {
+    public void testLogin0_userDisabled() {
         // 准备参数
-        String userIp = randomString();
-        String userAgent = randomString();
-        AuthLoginReqVO reqVO = randomPojo(AuthLoginReqVO.class);
-
-        // mock 验证码不正确
-        String code = randomString();
-        when(captchaService.getCaptchaCode(reqVO.getUuid())).thenReturn(code);
+        String username = randomString();
+        String password = randomString();
+        // mock user 数据
+        AdminUserDO user = randomPojo(AdminUserDO.class, o -> o.setUsername(username)
+                .setPassword(password).setStatus(CommonStatusEnum.DISABLE.getStatus()));
+        when(userService.getUserByUsername(eq(username))).thenReturn(user);
+        // mock password 匹配
+        when(userService.isPasswordMatch(eq(password), eq(user.getPassword()))).thenReturn(true);
 
         // 调用, 并断言异常
-        assertServiceException(() -> authService.login(reqVO, userIp, userAgent), AUTH_LOGIN_CAPTCHA_CODE_ERROR);
-        // 校验调用参数
-        verify(loginLogService, times(1)).createLoginLog(
-            argThat(o -> o.getLogType().equals(LoginLogTypeEnum.LOGIN_USERNAME.getType())
-                    && o.getResult().equals(LoginResultEnum.CAPTCHA_CODE_ERROR.getResult()))
+        AssertUtils.assertServiceException(() -> authService.login0(username, password),
+                AUTH_LOGIN_USER_DISABLED);
+        verify(loginLogService).createLoginLog(
+                argThat(o -> o.getLogType().equals(LoginLogTypeEnum.LOGIN_USERNAME.getType())
+                        && o.getResult().equals(LoginResultEnum.USER_DISABLED.getResult())
+                        && o.getUserId().equals(user.getId()))
         );
     }
 
     @Test
-    public void testLogin_badCredentials() {
+    public void testCaptcha_success() {
         // 准备参数
-        String userIp = randomString();
-        String userAgent = randomString();
         AuthLoginReqVO reqVO = randomPojo(AuthLoginReqVO.class);
+
         // mock 验证码正确
         when(captchaService.getCaptchaCode(reqVO.getUuid())).thenReturn(reqVO.getCode());
-        // mock 抛出异常
-        when(authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(reqVO.getUsername(), reqVO.getPassword())))
-                .thenThrow(new BadCredentialsException("测试账号或密码不正确"));
 
-        // 调用, 并断言异常
-        assertServiceException(() -> authService.login(reqVO, userIp, userAgent), AUTH_LOGIN_BAD_CREDENTIALS);
-        // 校验调用参数
-        verify(captchaService, times(1)).deleteCaptchaCode(reqVO.getUuid());
-        verify(loginLogService, times(1)).createLoginLog(
-            argThat(o -> o.getLogType().equals(LoginLogTypeEnum.LOGIN_USERNAME.getType())
-                    && o.getResult().equals(LoginResultEnum.BAD_CREDENTIALS.getResult()))
-        );
+        // 调用
+        authService.verifyCaptcha(reqVO);
+        // 断言
+        verify(captchaService).deleteCaptchaCode(reqVO.getUuid());
     }
 
     @Test
-    public void testLogin_userDisabled() {
+    public void testCaptcha_notFound() {
         // 准备参数
-        String userIp = randomString();
-        String userAgent = randomString();
         AuthLoginReqVO reqVO = randomPojo(AuthLoginReqVO.class);
 
-        // mock 验证码正确
-        when(captchaService.getCaptchaCode(reqVO.getUuid())).thenReturn(reqVO.getCode());
-        // mock 抛出异常
-        when(authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(reqVO.getUsername(), reqVO.getPassword())))
-                .thenThrow(new DisabledException("测试用户被禁用"));
-
         // 调用, 并断言异常
-        assertServiceException(() -> authService.login(reqVO, userIp, userAgent), AUTH_LOGIN_USER_DISABLED);
+        assertServiceException(() -> authService.verifyCaptcha(reqVO), AUTH_LOGIN_CAPTCHA_NOT_FOUND);
         // 校验调用参数
-        verify(captchaService, times(1)).deleteCaptchaCode(reqVO.getUuid());
         verify(loginLogService, times(1)).createLoginLog(
             argThat(o -> o.getLogType().equals(LoginLogTypeEnum.LOGIN_USERNAME.getType())
-                    && o.getResult().equals(LoginResultEnum.USER_DISABLED.getResult()))
+                    && o.getResult().equals(LoginResultEnum.CAPTCHA_NOT_FOUND.getResult()))
         );
     }
 
     @Test
-    public void testLogin_unknownError() {
+    public void testCaptcha_codeError() {
         // 准备参数
-        String userIp = randomString();
-        String userAgent = randomString();
         AuthLoginReqVO reqVO = randomPojo(AuthLoginReqVO.class);
-        // mock 验证码正确
-        when(captchaService.getCaptchaCode(reqVO.getUuid())).thenReturn(reqVO.getCode());
-        // mock 抛出异常
-        when(authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(reqVO.getUsername(), reqVO.getPassword())))
-                .thenThrow(new AuthenticationException("测试未知异常") {});
+
+        // mock 验证码不正确
+        String code = randomString();
+        when(captchaService.getCaptchaCode(reqVO.getUuid())).thenReturn(code);
 
         // 调用, 并断言异常
-        assertServiceException(() -> authService.login(reqVO, userIp, userAgent), AUTH_LOGIN_FAIL_UNKNOWN);
+        assertServiceException(() -> authService.verifyCaptcha(reqVO), AUTH_LOGIN_CAPTCHA_CODE_ERROR);
         // 校验调用参数
-        verify(captchaService, times(1)).deleteCaptchaCode(reqVO.getUuid());
-        verify(loginLogService, times(1)).createLoginLog(
+        verify(loginLogService).createLoginLog(
             argThat(o -> o.getLogType().equals(LoginLogTypeEnum.LOGIN_USERNAME.getType())
-                    && o.getResult().equals(LoginResultEnum.UNKNOWN_ERROR.getResult()))
+                    && o.getResult().equals(LoginResultEnum.CAPTCHA_CODE_ERROR.getResult()))
         );
     }
 
@@ -204,29 +184,32 @@ public class AuthServiceImplTest extends BaseDbUnitTest {
         // 准备参数
         String userIp = randomString();
         String userAgent = randomString();
-        AuthLoginReqVO reqVO = randomPojo(AuthLoginReqVO.class);
+        AuthLoginReqVO reqVO = randomPojo(AuthLoginReqVO.class, o ->
+                o.setUsername("test_username").setPassword("test_password"));
 
         // mock 验证码正确
         when(captchaService.getCaptchaCode(reqVO.getUuid())).thenReturn(reqVO.getCode());
-        // mock authentication
-        Long userId = randomLongId();
-        Set<Long> userRoleIds = randomSet(Long.class);
-        LoginUser loginUser = randomPojo(LoginUser.class, o -> o.setId(userId));
-        when(authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(reqVO.getUsername(), reqVO.getPassword())))
-                .thenReturn(authentication);
-        when(authentication.getPrincipal()).thenReturn(loginUser);
+        // mock user 数据
+        AdminUserDO user = randomPojo(AdminUserDO.class, o -> o.setUsername("test_username")
+                .setPassword("test_password").setStatus(CommonStatusEnum.ENABLE.getStatus()));
+        when(userService.getUserByUsername(eq("test_username"))).thenReturn(user);
+        // mock password 匹配
+        when(userService.isPasswordMatch(eq("test_password"), eq(user.getPassword()))).thenReturn(true);
         // mock 缓存登录用户到 Redis
         String token = randomString();
-        when(userSessionService.createUserSession(loginUser, userIp, userAgent)).thenReturn(token);
+        when(userSessionService.createUserSession(argThat(argument -> {
+            AssertUtils.assertPojoEquals(user, argument);
+            return true;
+        }), eq(userIp), eq(userAgent))).thenReturn(token);
 
         // 调用, 并断言异常
-        String login = authService.login(reqVO, userIp, userAgent);
-        assertEquals(token, login);
+        String result = authService.login(reqVO, userIp, userAgent);
+        assertEquals(token, result);
         // 校验调用参数
-        verify(captchaService, times(1)).deleteCaptchaCode(reqVO.getUuid());
-        verify(loginLogService, times(1)).createLoginLog(
+        verify(loginLogService).createLoginLog(
             argThat(o -> o.getLogType().equals(LoginLogTypeEnum.LOGIN_USERNAME.getType())
-                    && o.getResult().equals(LoginResultEnum.SUCCESS.getResult()))
+                    && o.getResult().equals(LoginResultEnum.SUCCESS.getResult())
+                    && o.getUserId().equals(user.getId()))
         );
     }