RuoYi框架集成钉钉登录时,遭遇“用户不存在/密码错误”的深度解析与正确实践

120 阅读5分钟

在为若依(RuoYi)框架集成第三方登录(如钉钉、微信)时,许多开发者会遇到一个看似匪夷所思的问题:明明已经通过第三方回调成功获取了用户信息,并且在本地数据库中也找到了对应的用户,但在最后调用系统登录服务生成Token时,却抛出了 UserPasswordNotMatchException: 用户不存在/密码错误 的异常。

本文将详细剖析这个问题的根源,并提供一套在若依框架中集成第三方登录的正确、标准的代码实践。

问题场景重现

通常,我们的钉钉登录回调处理逻辑会像下面这样:

  1. 接收钉钉服务器回调的请求体(Body),其中包含用户的手机号、昵称等信息。
  2. 通过回调中的手机号,查询本地 sys_user 表,找到与之关联的系统用户 SysUser 对象。
  3. 获取到 sysUser 对象后,尝试调用若依框架自带的 SysLoginService 来完成登录并获取Token。

错误的尝试代码:

codeJava

// ... Controller中
@Autowired
private ISysUserService iSysUserService;

@Autowired
private SysLoginService loginService;

public AjaxResult dingTalkCallback(String body) {
    // 1. 解析钉钉回调信息
    DingTalkLoginBody loginBody = JSON.parseObject(body, DingTalkLoginBody.class);

    // 2. 根据手机号查询本地用户
    SysUser sysUser = iSysUserService.selectUserByPhoneNumber(loginBody.getMobile());
    if (sysUser == null) {
        return AjaxResult.error("登录失败,用户未关联系统账号");
    }

    // 3. 错误的关键点:尝试使用用户的账号和数据库中的加密密码进行登录
    //    这里的 sysUser.getPassword() 是一个加密后的哈希值!
    String token = loginService.login(sysUser.getUserName(), sysUser.getPassword());

    AjaxResult ajax = AjaxResult.success();
    ajax.put(Constants.TOKEN, token);
    return ajax;
}

代码执行到 loginService.login(...) 时,便会抛出文章开头提到的密码错误异常。

核心问题分析:为何密码会错误?

这个问题的根源在于对若依(及Spring Security)标准登录流程的误用

SysLoginService.login(String username, String password) 这个方法,是为传统的、基于用户名和密码的登录场景设计的。它的内部工作流如下:

  1. 接收一个明文用户名和明文密码作为输入。
  2. 调用 AuthenticationManager(Spring Security的核心认证管理器)。
  3. AuthenticationManager 会找到 UserDetailsServiceImpl,根据 username 从数据库加载用户信息,其中包括加密后的密码。
  4. Spring Security 使用 PasswordEncoder(通常是BCrypt)将用户输入的明文密码进行加密。
  5. 比较:将刚刚加密后的密码,与从数据库中取出的加密密码进行比对。
  6. 只有当两者匹配时,认证才算成功。

在我们的错误代码中,我们传给 login 方法的第二个参数 sysUser.getPassword() 是什么?它是我们从数据库里直接查出来的、已经被加密过的密码字符串(例如 2a2a10$xxxxxxxx...)。

当这个加密字符串被当作“明文密码”传入 login 方法后,Spring Security 会对它再次进行加密,然后才去和数据库里的原始加密字符串比较。结果必然是 encrypt("加密字符串") 不等于 "加密字符串",认证失败,从而抛出“用户不存在/密码错误”的异常。

一个形象的比喻:  这就好比你的保险箱密码是123。你把写着123的纸条给管家,他能打开。但现在,你把一张写着“保险箱密码是123”的纸条锁进另一个盒子里,然后把这个锁上的盒子交给管家,让他用这个“盒子”去开保险箱,他当然打不开了。

正确的解决思路:信任与授权,而非密码验证

对于钉钉这类第三方登录,其认证逻辑是基于信任与授权,而非密码。用户在钉钉上确认登录,钉钉告诉我们的系统:“这个用户是合法的,他的身份标识是xxx(例如手机号)”。我们的系统在收到这个信息后,要做的不是去验证他的密码,而是:

  1. 信任钉钉的认证结果。
  2. 根据钉钉提供的身份标识,找到我们系统内的对应用户。
  3. 直接为这个已确认身份的用户颁发一个访问令牌(Token)。

这个过程完全不需要用户的密码参与。

标准的解决方案代码

正确的做法是绕过 loginService.login() 方法,在确认用户身份后,手动构建 LoginUser 对象,并直接调用 TokenService 来生成令牌。

第一步:在Controller中注入所需服务

codeJava

@Autowired
private ISysUserService iSysUserService;

@Autowired
private TokenService tokenService; // 核心:用于生成Token

@Autowired
private SysPermissionService permissionService; // 核心:用于获取用户权限

@Autowired
private SysLoginService loginService; // 辅助:用于记录登录日志

第二步:重写回调方法

codeJava

public AjaxResult dingTalkCallback(String body) {
    System.out.println("body = " + body);
    DingTalkLoginBody loginBody = JSON.parseObject(body, DingTalkLoginBody.class);

    if (loginBody == null || StringUtils.isBlank(loginBody.getMobile())) {
        return AjaxResult.error("钉钉登录失败,无法获取手机号");
    }

    // 1. 根据手机号查询系统用户
    SysUser sysUser = iSysUserService.selectUserByPhoneNumber(loginBody.getMobile());

    // 2. 必要的健壮性校验
    if (sysUser == null) {
        return AjaxResult.error("登录失败,该手机号尚未关联系统账号");
    }
    if (UserStatus.DELETED.getCode().equals(sysUser.getDelFlag())) {
        return AjaxResult.error("对不起,您的账号已被删除");
    }
    if (UserStatus.DISABLE.getCode().equals(sysUser.getStatus())) {
        return AjaxResult.error("对不起,您的账号已被停用");
    }

    // --- 核心修改:绕过密码验证,直接构建认证主体并生成Token ---

    // 3. 手动构建 LoginUser 对象
    // LoginUser 是若依在Security中实际使用的用户认证对象,包含了权限信息
    LoginUser loginUser = new LoginUser(sysUser.getUserId(), sysUser.getDeptId(), sysUser,
            permissionService.getMenuPermission(sysUser));

    // 4. (推荐) 记录登录日志,保持行为一致性
    loginService.recordLoginInfo(sysUser.getUserId());

    // 5. 直接调用 TokenService 生成令牌
    String token = tokenService.createToken(loginUser);

    // 6. 返回成功结果
    AjaxResult ajax = AjaxResult.success("登录成功");
    ajax.put(Constants.TOKEN, token);
    return ajax;
}

总结

关键的认知转变:

  • 账号密码登录 = 验证凭证,需要调用 loginService.login()。
  • 第三方授权登录 = 信任授权,需要绕过密码验证,直接为已识别的用户 SysUser 构建 LoginUser 并调用 tokenService.createToken()。