受保护的 Web API:验证范围和应用角色

本文介绍如何将授权添加到 Web API。 这种保护可确保只有以下对象才能调用 API:

  • 代表具有适当范围和角色的用户的应用程序。
  • 具有适当应用程序角色的守护程序应用。

本文中的代码片段摘自 GitHub 上的以下代码示例:

若要保护 ASP.NET 或 ASP.NET Core Web API,必须在下列其中一项中添加 [Authorize] 属性:

  • 控制器本身(若要保护所有控制器操作)
  • API 的单个控制器操作
    [Authorize]
    public class TodoListController : Controller
    {
     // ...
    }

但是,这种保护并不足够。 它只能保证 ASP.NET 和 ASP.NET Core 对该令牌进行验证。 你的 API 需要验证用来调用 API 的令牌是否是使用预期的声明请求的。 具体而言,这些声明需要验证:

  • 作用域(如果代表用户调用 API)。
  • 应用角色(如果可从守护程序应用调用 API)。

在代表用户调用的 API 中验证作用域

如果某个客户端应用代表用户调用了你的 API,则该 API 需要请求具有该 API 的特定作用域的持有者令牌。 有关详细信息,请参阅代码配置 | 持有者令牌

在 ASP.NET Core 中,可以使用 Microsoft.Identity.Web 来验证每个控制器操作的范围。 还可以在控制器级别或针对整个应用程序对其进行验证。

验证每个控制器操作的范围

可以使用 [RequiredScope] 属性验证控制器操作的范围。 此属性具有多个覆盖。 一个直接采用所需范围,另一个采用配置的密钥。

使用硬编码范围验证控制器操作的范围

以下代码片段显示了如何使用 [RequiredScope] 属性与硬编码范围。

using Microsoft.Identity.Web

[Authorize]
public class TodoListController : Controller
{
    /// <summary>
    /// The web API will accept only tokens that have the `access_as_user` scope for
    /// this API.
    /// </summary>
    const string scopeRequiredByApi = "access_as_user";

    // GET: api/values
    [HttpGet]
    [RequiredScope(scopeRequiredByApi)]
    public IEnumerable<TodoItem> Get()
    {
        // Do the work and return the result.
        // ...
    }
 // ...
}
使用配置中定义的范围验证控制器操作的范围

还可以在配置中声明这些必需的范围,并引用配置密钥:

例如,如果 appsettings.json 中有以下配置:

{
 "AzureAd" : {
   // more settings
   "Scopes" : "access_as_user access_as_admin"
  }
}

然后,在 [RequiredScope] 属性中对其进行引用:

using Microsoft.Identity.Web

[Authorize]
public class TodoListController : Controller
{
    // GET: api/values
    [HttpGet]
    [RequiredScope(RequiredScopesConfigurationKey = "AzureAd:Scopes")]
    public IEnumerable<TodoItem> Get()
    {
        // Do the work and return the result.
        // ...
    }
 // ...
}
有条件地验证范围

在某些情况下,需要有条件地验证范围。 可以使用 HttpContext 上的 VerifyUserHasAnyAcceptedScope 扩展方法执行此操作。

using Microsoft.Identity.Web

[Authorize]
public class TodoListController : Controller
{
    /// <summary>
    /// The web API will accept only tokens 1) for users, 2) that have the `access_as_user` scope for
    /// this API.
    /// </summary>
    static readonly string[] scopeRequiredByApi = new string[] { "access_as_user" };

    // GET: api/values
    [HttpGet]
    public IEnumerable<TodoItem> Get()
    {
         HttpContext.VerifyUserHasAnyAcceptedScope(scopeRequiredByApi);
        // Do the work and return the result.
        // ...
    }
 // ...
}

验证控制器级别的范围

还可以验证整个控制器的范围

使用硬编码范围验证控制器的范围

以下代码片段显示了如何在控制器上使用 [RequiredScope] 属性与硬编码范围。 若要使用 RequiredScopeAttribute,需要执行以下操作之一:

  • Startup.cs 中使用 AddMicrosoftIdentityWebApi,如代码配置中所示
  • 或以其他方式将 ScopeAuthorizationRequirement 添加到授权策略中,如授权策略中所述。
using Microsoft.Identity.Web

[Authorize]
[RequiredScope(scopeRequiredByApi)]
public class TodoListController : Controller
{
    /// <summary>
    /// The web API will accept only tokens 1) for users, 2) that have the `access_as_user` scope for
    /// this API.
    /// </summary>
    static readonly string[] scopeRequiredByApi = new string[] { "access_as_user" };

    // GET: api/values
    [HttpGet]
    public IEnumerable<TodoItem> Get()
    {
        // Do the work and return the result.
        // ...
    }
 // ...
}
使用配置中定义的范围验证控制器的范围

与针对操作类似,还可以在配置中声明这些必需的范围,并引用配置密钥:

using Microsoft.Identity.Web

[Authorize]
[RequiredScope(RequiredScopesConfigurationKey = "AzureAd:Scopes")]
public class TodoListController : Controller
{
    // GET: api/values
    [HttpGet]
    public IEnumerable<TodoItem> Get()
    {
        // Do the work and return the result.
        // ...
    }
 // ...
}

更全面地验证范围

建议的方法是为 Web API 定义粒度范围并验证每个控制器操作中的范围。 但是,也可以在应用程序或控制器级别验证范围。 有关详细信息,请参阅 ASP.NET 核心文档中的基于声明的授权

验证的内容

[RequiredScope] 属性和 VerifyUserHasAnyAcceptedScope 方法将执行诸如以下步骤的某些操作:

  • 验证是否存在名为 http://schemas.microsoft.com/identity/claims/scopescp 的声明。
  • 验证该声明的值是否包含 API 预期的作用域。

验证守护程序应用调用的 API 中的应用角色

如果 Web API 由某个守护程序应用调用,该应用应该对该 Web API 拥有应用程序权限。 如公开应用程序权限(应用角色)中所示,你的 API 将公开此类权限。 一个示例是 access_as_application 应用角色。

现在,你需要让 API 验证它收到的令牌是否包含 roles 声明,以及此声明是否具有预期的值。 验证代码类似于对委托权限进行验证的代码,不同之处在于,你的控制器操作针对角色进行测试,而非针对作用域进行测试:

以下代码片段显示了如何验证应用程序角色;

using Microsoft.Identity.Web

[Authorize]
public class TodoListController : ApiController
{
    public IEnumerable<TodoItem> Get()
    {
        HttpContext.ValidateAppRole("access_as_application");
        // ...
    }

或者,可以在控制器或操作(或 razor 页面)上使用 [Authorize(Roles = "access_as_application")] 属性。

[Authorize(Roles = "access_as_application")]
MyController : ApiController
{
    // ...
}

ASP.NET Core 中基于角色的授权列出了实现基于角色的授权的多种方法。 开发人员可以在其中选择一个适合其相应方案的方法。

若要查看工作示例,请参阅有关按角色和组进行授权的 Web 应用增量教程。

在代表用户调用的 API 中验证应用角色

用户还可以在用户分配模式下使用角色声明,如如何在应用程序中添加应用角色并在令牌中接收它们中所示。 如果角色可分配给用户和应用,只需选中相应的角色就能让应用以用户身份登录,以及让用户以应用身份登录。 建议为用户和应用声明不同的角色,以避免出现这种混淆。

如果已使用用户/组定义了应用角色,则还可以在 API 中验证角色声明以及范围。 在这种情况下,应用角色的验证逻辑将保持不变,就像 API 由守护程序应用调用一样,因为用户/组和应用程序的角色声明没有任何区别。

当 Web API 只能由守护程序应用调用时接受仅限应用的令牌

如果你希望只有守护程序应用能够调用 Web API,请在验证应用角色时添加一个条件,规定该令牌是仅限应用的令牌。

string oid = ClaimsPrincipal.Current.FindFirst("oid")?.Value;
string sub = ClaimsPrincipal.Current.FindFirst("sub")?.Value;
bool isAppOnly = oid != null && sub != null && oid == sub;

选中反向条件将只允许用于将用户登录的应用调用你的 API。

使用基于 ACL 的授权

除了基于应用角色的授权,还可以使用基于访问控制列表 (ACL) 的授权模式来保护 Web API,在没有 roles 声明的情况下控制令牌

如果在 ASP.NET Core 上使用 Microsoft.Identity.Web,则需声明使用的是基于 ACL 的授权,否则 Microsoft Identity Web 将在提供的声明中既没有角色也没有范围时引发异常:

System.UnauthorizedAccessException: IDW10201: Neither scope or roles claim was found in the bearer token.

若要避免此异常,请在 appsettings.json 中或以编程方式将 AllowWebApiToBeAuthorizedByACL 配置属性设置为 true

{
 "AzureAD"
 {
  // other properties
  "AllowWebApiToBeAuthorizedByACL" : true,
  // other properties
 }
}

如果将 AllowWebApiToBeAuthorizedByACL 设置为 true,则需负责确保 ACL 机制。

后续步骤

  • 在以下多部分教程系列中详细了解如何生成一个让用户登录的 ASP.NET Core Web 应用

  • 探索 Microsoft 标识平台 Web API 示例