基于OAuth2与Bearer Token的Web API2 2FA实现技术求助(OWIN+Identity2)
我之前正好帮朋友搞定过类似的2FA场景,完全匹配你说的两种Token返回逻辑,下面给你拆解思路和可直接复用的代码示例:
核心思路
关键是通过Claims区分Token权限:
- 对需要2FA的用户,返回带
TwoFactorRequired=trueClaim的受限Token - 自定义授权特性,拦截持有受限Token的请求,拒绝访问带
[Authorize]的接口 - 用户完成2FA验证后,生成移除该Claim的完整授权Token
具体实现步骤
1. 重写GrantResourceOwnerCredentials方法
这是处理用户名密码登录的核心入口,我们在这里分支处理两种场景:
public override async Task GrantResourceOwnerCredentials(OAuthGrantResourceOwnerCredentialsContext context) { var userManager = context.OwinContext.GetUserManager<ApplicationUserManager>(); ApplicationUser user = await userManager.FindAsync(context.UserName, context.Password); if (user == null) { context.SetError("invalid_grant", "用户名或密码错误"); return; } // 场景2:用户开启了2FA,返回受限Token if (await userManager.GetTwoFactorEnabledAsync(user.Id)) { // 生成临时会话ID,用于后续验证2FA时关联用户 var twoFactorSessionId = Guid.NewGuid().ToString(); // 会话ID存入缓存(生产环境建议用Redis/分布式缓存) MemoryCache.Default.Add(twoFactorSessionId, user.Id, DateTimeOffset.Now.AddMinutes(15)); // 创建受限身份标识,添加2FA要求的Claim var restrictedIdentity = new ClaimsIdentity(context.Options.AuthenticationType); restrictedIdentity.AddClaim(new Claim(ClaimTypes.NameIdentifier, user.Id)); restrictedIdentity.AddClaim(new Claim("TwoFactorRequired", "true")); restrictedIdentity.AddClaim(new Claim(ClaimTypes.Name, user.UserName)); // 返回受限Token,同时在响应附加会话ID var props = new AuthenticationProperties(new Dictionary<string, string> { { "two_factor_session_id", twoFactorSessionId } }); var ticket = new AuthenticationTicket(restrictedIdentity, props); context.Validated(ticket); } // 场景1:用户无需2FA,返回完整授权Token else { var fullIdentity = await userManager.CreateIdentityAsync(user, context.Options.AuthenticationType); // 附加角色等权限Claim var roles = await userManager.GetRolesAsync(user.Id); foreach (var role in roles) { fullIdentity.AddClaim(new Claim(ClaimTypes.Role, role)); } context.Validated(fullIdentity); } }
2. 自定义授权特性拦截受限Token
替换默认的[Authorize],或者叠加使用,用来检查Token是否完成2FA:
public class TwoFactorAuthorizeAttribute : AuthorizeAttribute { protected override void HandleUnauthorizedRequest(HttpActionContext actionContext) { var principal = actionContext.RequestContext.Principal as ClaimsPrincipal; if (principal != null && principal.HasClaim(c => c.Type == "TwoFactorRequired" && c.Value == "true")) { // 用户已登录但未完成2FA,返回自定义403响应 actionContext.Response = actionContext.Request.CreateResponse(HttpStatusCode.Forbidden, new { Message = "需要完成双因素认证才能访问此资源", RequiresTwoFactor = true }); } else { base.HandleUnauthorizedRequest(actionContext); } } }
之后你的API控制器就可以这样使用:
[TwoFactorAuthorize] public IHttpActionResult GetSecureData() { // 只有持有完整Token的用户才能访问 return Ok("敏感数据内容"); }
3. 实现2FA验证接口
接收用户输入的安全码,验证通过后返回完整Token:
[AllowAnonymous] [HttpPost] [Route("api/Account/VerifyTwoFactor")] public async Task<IHttpActionResult> VerifyTwoFactor(TwoFactorVerificationModel model) { // 从缓存获取会话对应的用户ID var userId = MemoryCache.Default.Get(model.TwoFactorSessionId) as string; if (userId == null) { return BadRequest("验证会话已过期,请重新登录"); } var userManager = Request.GetOwinContext().GetUserManager<ApplicationUserManager>(); var user = await userManager.FindByIdAsync(userId); if (user == null) { return BadRequest("用户不存在"); } // 用Identity2验证安全码(这里假设你用Email方式发送验证码) var isValidCode = await userManager.VerifyTwoFactorTokenAsync(user.Id, "Email", model.SecurityCode); if (!isValidCode) { return BadRequest("安全码无效"); } // 生成完整授权Token var fullIdentity = await userManager.CreateIdentityAsync(user, DefaultAuthenticationTypes.ApplicationCookie); var roles = await userManager.GetRolesAsync(user.Id); foreach (var role in roles) { fullIdentity.AddClaim(new Claim(ClaimTypes.Role, role)); } // 生成Bearer Token var tokenFormat = Request.GetOwinContext().Get<ISecureDataFormat<AuthenticationTicket>>("OAuthBearerAuthenticationOptions.AccessTokenFormat"); var accessToken = tokenFormat.Protect(new AuthenticationTicket(fullIdentity, new AuthenticationProperties())); // 清理过期的会话缓存 MemoryCache.Default.Remove(model.TwoFactorSessionId); return Ok(new { AccessToken = accessToken, ExpiresIn = 3600 // 匹配你的Token过期时间设置 }); } // 对应的请求模型 public class TwoFactorVerificationModel { public string TwoFactorSessionId { get; set; } public string SecurityCode { get; set; } }
额外注意事项
- 缓存选型:生产环境不要用
MemoryCache,建议用Redis或SQL Server分布式缓存,避免多服务器部署时会话ID无法共享的问题 - Token权限:受限Token可以只保留用户ID、用户名等基础信息,不包含角色、权限等敏感Claim
- 前端处理:拿到受限Token后,调用受保护接口会返回403,此时需要引导用户输入安全码,调用验证接口获取完整Token
内容的提问来源于stack exchange,提问作者Benji Britain
相关产品推荐
相关产品推荐

