基于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
相关产品推荐
相关产品推荐

