You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

基于Identity Server 4实现类WhatsApp的二维码登录问题

基于IdentityServer4实现WhatsApp式扫码登录方案

核心思路

WhatsApp的扫码登录本质是已认证Web用户对移动端设备直接授权,标准OIDC没有完全匹配的现成流程,但可以通过改造设备流实现——把默认设备流中“用户输入账号密码”的环节,替换成“Web端已登录用户扫码确认授权”,全程遵循OIDC规范。

解决你遇到的具体问题

1. 手动插入DeviceCodes表返回invalid_grant的原因

IdentityServer的DeviceCodes表存储的记录包含签名校验、状态标识、过期时间等关键逻辑,不是直接插入就能生效的。必须通过官方的deviceauthorization端点生成合法的device code记录,手动插入的记录缺少必要的签名和上下文信息,会被token端点判定为无效授权。

2. 调用token端点返回authorization_pending的处理

这个响应是正常状态,说明当前device code还未被授权。你需要在Web端用户扫码后,触发以下动作:

  • 在IdentityServer中新增一个自定义API端点(比如/api/authorize-device),仅允许已登录Web用户调用。
  • 该端点接收扫码得到的device_code,将当前Web用户的身份信息(如sub用户ID)绑定到对应的DeviceCode记录上,标记其为已授权状态。
  • 移动端继续轮询token端点,此时IdentityServer会识别到device code已完成授权,返回合法令牌。

3. 为特定用户生成令牌的实现

要绑定特定用户,关键是在授权阶段把用户身份关联到device code:

  • Web端扫码后,将当前登录用户的sub(唯一标识)传递给IdentityServer的自定义授权端点。
  • 在自定义端点中,通过IDeviceFlowStore找到对应的DeviceCode记录,将用户的Claims(如sub、name)写入记录的Data字段,同时设置IsAuthorized=true。
  • 移动端轮询token端点时,IdentityServer会读取该DeviceCode的用户信息,生成对应用户的令牌。

具体代码实现要点

1. IdentityServer端自定义授权端点

[Route("api/[controller]")]
[ApiController]
public class DeviceAuthController : ControllerBase
{
    private readonly IDeviceFlowStore _deviceStore;
    private readonly IProfileService _profileService;

    public DeviceAuthController(IDeviceFlowStore deviceStore, IProfileService profileService)
    {
        _deviceStore = deviceStore;
        _profileService = profileService;
    }

    [HttpPost("confirm")]
    [Authorize] // 仅允许已登录Web用户调用
    public async Task<IActionResult> ConfirmDevice([FromBody] DeviceConfirmRequest request)
    {
        var deviceCode = await _deviceStore.FindByDeviceCodeAsync(request.DeviceCode);
        if (deviceCode == null) return NotFound("无效设备码");

        // 获取当前Web登录用户的身份标识
        var userId = User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
        if (string.IsNullOrEmpty(userId)) return BadRequest("用户未认证");

        // 加载用户的Profile信息
        var profileContext = new ProfileDataRequestContext
        {
            Subject = new ClaimsIdentity(new[] { new Claim(ClaimTypes.NameIdentifier, userId) }),
            RequestedClaimTypes = deviceCode.RequestedScopes.ToList()
        };
        await _profileService.GetProfileDataAsync(profileContext);

        // 更新设备码状态,绑定用户信息
        deviceCode.IsAuthorized = true;
        deviceCode.Subject = new ClaimsPrincipal(profileContext.Subject);
        await _deviceStore.UpdateByDeviceCodeAsync(request.DeviceCode, deviceCode);

        return Ok("授权成功");
    }
}

public class DeviceConfirmRequest
{
    public string DeviceCode { get; set; }
}

2. 移动端轮询token端点逻辑

移动端从deviceauthorization端点拿到device_code后,每隔3-5秒发起轮询:

POST /connect/token
Content-Type: application/x-www-form-urlencoded

grant_type=urn:ietf:params:oauth:grant-type:device_code
&device_code=你的设备码
&client_id=移动端客户端ID
&client_secret=移动端客户端密钥(机密客户端需传)

Web端完成授权后,该请求会返回包含access_token、id_token的响应。

3. Web端扫码后调用授权端点

// 扫码获取deviceCode后调用
fetch('/api/deviceauth/confirm', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'Authorization': 'Bearer ' + webUserToken // Web端用户的访问令牌
  },
  body: JSON.stringify({ DeviceCode: deviceCode })
})
.then(res => res.json())
.then(data => {
  alert('授权成功,移动端可登录');
});

注意事项

  • 合理设置device code的过期时间(默认15分钟),避免用户扫码超时。
  • 自定义授权端点要做好权限校验,防止未授权用户调用。
  • 移动端轮询频率不要过高,建议3-5秒一次,减少服务器压力。

内容的提问来源于stack exchange,提问作者mahdi

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.20 13:13:03