设备验证
设备验证是登录流程的一部分(并非独立的微服务或独立控制器),用于在用户登录时识别登录设备、对新设备/未受信设备做二次验证或提醒,从而提升账号安全。
- 设备记录与信任逻辑由领域服务
IUserLoginDeviceManager(UserLoginDeviceManager)实现。 - 对外接口集成在
AccountController(api/identity/account)的 登录 与 设备验证 两个接口中。
⚠️ 注意:本模块没有
DeviceService/IDeviceService类,也没有AddDeviceVerification(...)扩展方法,更没有Security:DeviceVerification配置节或独立的GET/POST/DELETE /api/identity/device*接口。设备验证是默认内置行为,无需显式注册。
工作原理
登录时(POST /api/identity/account/login)的处理顺序:
- 校验用户名/邮箱与密码。
- 计算设备指纹
SHA256(UserAgent)取前 32 位十六进制字符串,调用CheckAndRecordDeviceAsync记录或更新该设备的登录信息。 - 调用
IsDeviceTrustedAsync判断当前设备是否受信任(已信任且未过期)。 - 统一判断是否需要二次验证:
- 需要验证 的条件:
RequiresTwoFactor(开启了 2FA)或 设备未受信。 - 不需要验证:受信设备且未开启 2FA → 直接签发 Token 返回。
- 需要验证 的条件:
- 若需要验证:
- 开启 2FA,或新设备且
Device.NewDeviceMode = verify→ 生成 6 位邮箱验证码与VerifyToken(缓存 5 分钟),发送验证邮件,返回「需要验证 + VerifyToken + 脱敏邮箱 + 是否可信任」。 - 新设备且
Device.NewDeviceMode = notify(默认)→ 仅发送「新设备登录提醒」邮件,不阻断登录,直接签发 Token 返回。
- 开启 2FA,或新设备且
- 前端拿到
VerifyToken后调用POST /api/identity/account/verify-device,传入VerifyToken/VerifyCode/TrustDevice完成验证并签发 Token;若TrustDevice = true,则将该设备标记为受信(默认有效期 180 天)。
设备指纹
DeviceFingerprint = SHA256(UserAgent) 的前 32 位十六进制
- 基于
UserAgent识别设备,IP 变化(如切换 WiFi / 4G)不影响设备识别。 - 同一浏览器在不同网络下仍会被识别为同一设备。
配置
设备验证相关配置有两类,均不是 Security:DeviceVerification:
| 配置 | 位置 | 默认值 | 说明 |
|---|---|---|---|
Device.NewDeviceMode | 系统设置(动态,可在后台「系统设置 → 设备验证」中配置) | notify | 新设备登录模式:notify = 仅发送提醒邮件(不阻断登录);verify = 要求邮箱验证码 |
Device:TrustExpireDays | appsettings.json | 180 | 信任设备的有效期(天),到期后需重新验证 |
示例(appsettings.json):
{
"Device": {
"TrustExpireDays": 180
}
}
没有
MaxDevices设备数量上限逻辑,文档中若出现「设备数量超限」属于错误描述。
实体 UserLoginDevice
设备记录实体以 UserId 为外键,核心字段是 DeviceFingerprint(指纹)、IsTrusted(是否受信)、TrustExpireTime(信任过期时间,为 null 表示未信任或已过期)。IsTrustedAndValid 是运行时计算字段(已信任且未过期),IsCurrentDevice 标记是否为当前登录设备。完整字段请看源码 FreeKit.Identity/Models/UserLoginDevice.cs。
文档中出现的
OnlineUser(含ConnectionId/ SignalR)实体与本模块无关,属于错误内容。
异常
- 设备验证相关错误统一抛出
BusinessException(如「验证码已过期,请重新登录」「验证码错误」「设备不存在」),由全局过滤器转换为 400,而非NotFoundException。