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

基于OAuth2与Bearer Token的Web API2 2FA实现技术求助(OWIN+Identity2)

我之前正好帮朋友搞定过类似的2FA场景,完全匹配你说的两种Token返回逻辑,下面给你拆解思路和可直接复用的代码示例:

核心思路

关键是通过Claims区分Token权限:

  • 对需要2FA的用户,返回带TwoFactorRequired=true Claim的受限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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.15 04:17:47