如何在无视图 WebAPI ASP.NET 核心应用程序中使用 [授权] 和防伪造?

How to use [Authorize] and antiforgery in a View-less WebAPI ASP.NET Core app?

我无法保证在严格(即 Viewless)ASP.NET Core WebAPI 项目中使用 [Authorize] 注释时遇到问题客户将使用的平台。也就是说,该应用程序需要是真正的 API,不需要特定平台即可访问。

Note: When I say, "strict WebAPI", my project actually started life as an MVC project generated by...

dotnet new mvc --auth Individual

... from which I immediately deleted all the views, etc, and changed the routing preferences to match WebAPI conventions.


我在尝试什么

当我通过 AJAX 访问标准登录功能(在下面的粘贴中精简到要点)时,我得到一个 JSON 有效负载并返回一个 cookie。

[HttpPost("apiTest")]
[AllowAnonymous]
public async Task<IActionResult> ApiLoginTest([FromBody] LoginViewModel model, string returnUrl = null)
{
    object ret = new { Error = "Generic Error" };

    if (ModelState.IsValid)
    {
        var result = await _signInManager.PasswordSignInAsync(model.Email, model.Password, model.RememberMe, lockoutOnFailure: false);
        if (result.Succeeded)
            ret = new { Success = true };
        else
            ret = new { Error = "Invalid login attempt" };
    }

    return new ObjectResult(ret);
}

成功后,returns 一个类似于以下内容的 cookie:

.AspNetCore.Identity.Application=CfDJ8Ge9E-[many characters removed]; path=/; domain=localhost; HttpOnly; Expires=Fri, 16 Mar 2018 16:27:47 GMT;

问题

在看似成功登录后,我尝试访问两个 API 做完全相同事情的端点,一个注释 AllowAnonymous 和一个 Authorized:

private IActionResult _getStatus()
{
    object ret = new { Error = "Generic Error" };

    var isSignedIn = _signInManager.IsSignedIn(User);
    var userName = _userManager.GetUserName(User);

    return new ObjectResult(
        new {
            SignedIn = isSignedIn,
            Name = userName
        }
    );
}

[HttpGet("authorizedTest")]
[Authorize]
public IActionResult GetCurrentLoginInfo2()
{
    return _getStatus();
}

[HttpGet("anonymousTest")]
[AllowAnonymous]
public IActionResult GetCurrentLoginInfo()
{
    return _getStatus();
}

anonymousTest 端点在登录前后都可以访问,尽管它告诉我我没有登录(SignedInfalse),即使在登录后也是如此。 authorizedTest 端点永远无法访问。

我的猜测是单个 cookie 不足以通过 [Authorized] 标记。我 相信 我还需要一个防伪值,要么来自 @Html.AntiForgeryToken() 生成的表单中的隐藏值,要么来自 View 似乎发送的第二个 cookie默认。那个饼干看起来像这样...

.AspNetCore.Antiforgery.0g4CU0eoNew=CfDJ8Ge9E-[many characters removed]; path=/; domain=localhost; HttpOnly; Expires=Tue, 19 Jan 2038 03:14:07 GMT;

失败的解决方案

我看过很多关于如何使用 pure AJAX 的答案,基本上都是说“get the anti-forgery from the hidden form" or "read it from the headers", but I don't have a View; there's no hidden form. Nor do I really want to kludge sending down a partial view for the clients to scrape.

我见过的最佳答案是 this one talking about using iOS。情况似乎类似:

Because we're not delivering HTML to the client, we can't use the standard @Html.AntiForgeryToken(), so instead we have to use AntiForgery.GetTokens to acquire and distribute the tokens to our clients.

但是即使我的 Intellisense "sees" AntiForgery.GetTokens,它也不会编译,即使在抓取似乎是正确的 nuget 包之后也是如此:

dotnet add package microsoft-web-helpers --version 2.1.20710.2

那么我的问题是:如何在 Razor 之外使用防伪 但使用 ASP.NET Core 来创建身份受限 Web API式项目?


为什么我认为这是一个防伪问题

有一段时间,我想不通为什么我的代码在严格的 WebAPI 项目中不起作用,但是移植到标准标识 ASP.NET 核心项目的(一个从 dotnet new mvc --auth Individual) AccountController 创建。

钥匙? options.LoginPath。当我在 WebAPI 登陆时,我转发到 一个 API 端点 以登录:

options.LoginPath = "/api/accounts/requestLogin";

在stock项目中,默认是View在加载时提供防伪cookie

options.LoginPath = "/Account/Login"; // As soon as Login.cshtml loads, BAM. .AspNetCore.Antiforgery cookie.

奇怪的是,有时我可以在 Postman 中删除第二个 AspNetCore.Antiforgery cookie,并且仍然可以访问 [Authorize]-注释的方法,所以我不是 100% 确定我没有咆哮这里的树错了,但这是我目前为止最好的引导...

首先,请求是幂等的:每个请求都是独一无二的,并且必须包含为请求提供服务所需的所有信息,包括授权。传统网站使用 cookie-based 身份验证,Web 浏览器在每次请求时将所有 cookie 一起发送回服务器,无需用户干预。与请求一起发送的 cookie 用于授权请求,使用户看起来已经通过身份验证,而无需不断传递用户名和密码。

API 是另一种野兽。由于发出请求的通常不是实际的 Web 浏览器,因此添加授权是一项手动操作。这通常是通过 Authorization 请求 header 处理的,其中包括不记名令牌之类的东西。如果您未能通过您的请求传递类似这样的内容,那么就好像用户从未通过身份验证一样,因为实际上他们没有。服务器不会唯一标识特定请求的来源,也不知道或不关心特定客户端之前发出过请求。 HTTP 协议在设计上是去中心化的。

其次,防伪令牌是完全不同的东西。它们旨在防止 XSRF 攻击,在这种情况下,恶意网站可能会尝试从您的站点托管表单的副本以捕获信息,然后 post 到您的站点,以便用户不知道发生了什么。例如,假设我在 http://faceboook.com 建立了一个站点(注意额外的 o)。大多数用户不会注意到 link 中的这种细微差异,甚至可能会不小心在地址栏中键入它。然后,我的恶意网站会创建一个与真实 Facebook 主页完全相同的副本,并带有一个登录位置。用户将他们的 Facebook 登录凭据输入我的表单(我登录后用于窃取他们的帐户),但我随后 post 将这些凭据发送给与真实 Facebook 网站使用的相同处理程序。最终结果是用户最终访问 Facebook,登录,然后继续他们的 posting 猫视频等业务。但是,现在我有了他们的登录凭据。

防伪令牌可以防止这种情况。该令牌已加密,因此我无法在我的恶意网站上创建能够在 Facebook 端成功验证的令牌。当验证失败时,请求被拒绝。基于存在要解决的防伪令牌问题,它们大多与 APIs 不兼容,因为通常 APIs 打算 供第三方使用。任何受防伪令牌保护的 API 端点将无法被该应用程序或域之外的任何东西使用。无论如何,无论哪种方式,它们都与 authentication/authorization 的请求无关。