未使用ASP.NET Core Identity的.NET Core项目添加TOTP MFA的实现方案
无ASP.NET Core Identity 环境下 .NET 6.0 API 集成TOTP多因素认证方案
TOTP是基于RFC 6238标准的公开算法,本身和ASP.NET Core Identity没有绑定关系。官方文档中的MFA示例与Identity强耦合,只是因为微软为Identity封装了开箱即用的扩展方法,你完全可以在自定义认证体系中独立实现,不需要引入任何Identity相关依赖,原有JWT认证、角色校验逻辑可以完全保留,具体实现步骤如下:
前置依赖准备
- 不需要安装任何ASP.NET Core Identity相关包,直接引入轻量TOTP实现库
Google.Authenticator即可,该库兼容Google Authenticator、Microsoft Authenticator等所有符合标准的TOTP客户端应用。
数据库结构调整
在你现有的自定义Users表中新增3个字段即可:
TwoFactorEnabled:bit类型,标记当前用户是否已开启MFATwoFactorSecret:nvarchar(128)类型,存储用户专属TOTP密钥(注意该字段必须加密存储,禁止明文落库)TwoFactorRecoveryCodes:nvarchar(max)类型,存储哈希后的一次性恢复码,存储逻辑和用户密码一致,禁止明文存储
核心逻辑实现
1. MFA开启流程
用户完成密码校验、处于已登录状态时,可请求开启MFA:
- 使用
System.Security.Cryptography.RandomNumberGenerator生成16字节以上的密码学安全随机数,编码为Base32格式作为用户专属TOTP密钥 - 按照TOTP标准格式生成认证地址:
otpauth://totp/[你的系统名称]:[用户名]?secret=[生成的密钥]&issuer=[你的系统名称],将该地址和手动录入用的明文密钥返回给前端,由前端生成二维码供用户用认证App扫描 - 此时不要直接标记用户MFA为开启状态,需要等用户提交认证App上显示的6位验证码,校验通过后再将加密后的密钥存入
TwoFactorSecret字段,标记TwoFactorEnabled = true - 同时生成5-10个随机字符串作为恢复码,哈希后存入
TwoFactorRecoveryCodes字段,将明文恢复码返回给用户妥善保存,用于认证App不可用时的应急登录
核心代码参考:
using Google.Authenticator; // 生成MFA配置信息 var tfa = new TwoFactorAuthenticator(); var setupResult = tfa.GenerateSetupCode( issuer: "你的业务系统名称", accountTitle: user.Username, accountSecretKey: userSecretKey, // 前面生成的Base32密钥 qrPixelsPerModule: 3, generateQrCode: false // 建议由前端用qrcode类库根据返回的ProvisioningUrl生成二维码,减少后端图片生成开销 ); // 返回 setupResult.ManualEntryKey(手动录入密钥)、setupResult.ProvisioningUrl(二维码内容)给前端 // 校验用户提交的TOTP验证码 var isCodeValid = tfa.ValidateTwoFactorPIN( accountSecretKey: storedDecryptedSecret, // 数据库取出后解密的密钥 twoFactorCode: userSubmitCode, timeTolerance: TimeSpan.FromMinutes(1) // 允许前后1分钟的时间偏移,兼容客户端时间误差 );
2. 登录流程改造
原有用户名密码校验通过后直接颁发JWT的逻辑,调整为两阶段校验:
- 第一阶段校验用户名+密码,密码错误直接返回认证失败结果;密码校验通过后判断用户MFA状态:
- 若用户未开启MFA:直接颁发完整权限的正式JWT,和原有逻辑一致,携带角色等所有业务Claims
- 若用户已开启MFA:仅颁发有效期5分钟的临时JWT,该JWT只携带一个特殊Claim:
"mfa_required": true,不携带角色等权限Claims,仅允许访问MFA校验接口
- 第二阶段MFA校验:用户拿到临时JWT后,提交认证App显示的6位验证码或者恢复码,请求MFA校验接口:
- 校验临时JWT合法,取出对应用户信息,优先校验TOTP验证码是否有效;如果TOTP校验不通过,再校验提交内容是否为未使用的有效恢复码(恢复码校验通过后立即标记为已用,不可重复使用)
- 任意一种校验通过后,为用户颁发完整权限的正式JWT,流程结束
3. 授权策略调整
原有JWT认证配置不需要修改,仅需新增授权策略区分临时JWT和正式JWT即可:
builder.Services.AddAuthorization(options => { // 默认授权策略:要求已认证且已完成MFA校验(无mfa_required标记) options.DefaultPolicy = new AuthorizationPolicyBuilder() .RequireAuthenticatedUser() .RequireAssertion(ctx => !ctx.User.HasClaim(c => c.Type == "mfa_required")) .Build(); // MFA步骤专用策略:仅允许携带临时MFA校验标识的令牌访问 options.AddPolicy("MfaStepRequired", policy => { policy.RequireAuthenticatedUser() .RequireClaim("mfa_required", "true"); }); });
- 所有普通业务接口、角色授权接口保持原有
[Authorize]、[Authorize(Roles = "Administrator")]特性不变,会自动拦截未完成MFA的临时令牌 - MFA校验接口标记
[Authorize(Policy = "MfaStepRequired")],仅允许临时令牌访问
4. MFA关闭流程
用户完成密码+TOTP验证码二次校验后,清空TwoFactorSecret、TwoFactorRecoveryCodes字段值,将TwoFactorEnabled设为false即可。
安全注意事项
- TOTP密钥、恢复码均为高敏感数据,必须加密存储,禁止明文落库
- TOTP校验时间窗口不要设置过大,最大不超过前后1分钟,避免暴力破解风险
- 记录每个用户最近一次TOTP校验成功的时间戳,同一时间窗口的验证码校验通过后立即作废,防止重放攻击
- 临时JWT有效期不得超过10分钟,且仅开放MFA校验接口的访问权限,不得放行其他业务接口
- 密钥生成必须使用密码学安全随机数生成器,禁止使用
Random、Guid等非安全随机方式生成密钥
内容的提问来源于stack exchange,提问作者variable
相关产品推荐
相关产品推荐

