在 ASP.NET Web API 2.2 中使用单个帐户和本地登录名保护 Web API

作者:Mike Wasson

下载示例应用

本主题演示如何使用 OAuth2 保护 Web API,以针对成员身份数据库进行身份验证。

本教程中使用的软件版本

在 Visual Studio 2013 中,Web API 项目模板提供三种身份验证选项:

  • 个人帐户。 应用使用成员身份数据库。
  • 组织帐户。 用户使用其 Azure Active Directory、Office 365 或本地 Active Directory 凭据登录。
  • Windows 身份验证。 此选项适用于 Intranet 应用程序,并使用 Windows 身份验证 IIS 模块。

个人帐户为用户提供两种方法来登录:

  • 本地登录名。 用户在站点上注册,输入用户名和密码。 应用将密码哈希存储在成员身份数据库中。 当用户登录时,ASP.NET 标识系统会验证密码。
  • 社交登录。 用户使用外部服务(如 Facebook、Microsoft 或 Google)登录。 应用仍在成员身份数据库中为用户创建一个条目,但不存储任何凭据。 用户通过登录到外部服务进行身份验证。

本文介绍本地登录方案。 对于本地登录和社交登录,Web API 使用 OAuth2 对请求进行身份验证。 但是,对于本地登录和社交登录,凭据流有所不同。

在本文中,我将演示一个简单的应用,该应用允许用户登录并向 Web API 发送经过身份验证的 AJAX 调用。 可以在此处下载示例代码。 自述文件介绍如何从头开始在 Visual Studio 中创建示例。

示例表单的图像

示例应用使用Knockout.js进行数据绑定,jQuery 用于发送 AJAX 请求。 我将重点介绍 AJAX 调用,因此无需了解本文的Knockout.js。

在此过程中,我将介绍:

  • 应用在客户端上执行的操作。
  • 服务器上发生的情况。
  • 中间的 HTTP 流量。

首先,我们需要定义一些 OAuth2 术语。

  • 资源。 可以保护的一些数据。
  • 资源服务器。 托管资源的服务器。
  • 资源所有者。 可以授予访问资源权限的实体。 (通常是用户。
  • 客户端:想要访问资源的应用。 在本文中,客户端是 Web 浏览器。
  • 访问令牌。 授予对资源的访问权限的令牌。
  • 持有者令牌。 特定类型的访问令牌,具有任何人都可以使用该令牌的属性。 换句话说,客户端不需要加密密钥或其他机密才能使用持有者令牌。 因此,持有者令牌只能通过 HTTPS 使用,并且到期时间应该相对较短。
  • 授权服务器。 提供访问令牌的服务器。

应用程序可以充当授权服务器和资源服务器。 Web API 项目模板遵循此模式。

本地登录凭据流

对于本地登录,Web API 使用 OAuth2 中定义的资源所有者密码流

  1. 用户向客户端输入名称和密码。
  2. 客户端将这些凭据发送到授权服务器。
  3. 授权服务器对凭据进行身份验证并返回访问令牌。
  4. 若要访问受保护的资源,客户端在 HTTP 请求的授权标头中包含访问令牌。

本地登录凭据流示意图

在 Web API 项目模板中选择 单个帐户 时,该项目包括一个授权服务器,用于验证用户凭据和颁发令牌。 下图显示了与 Web API 组件相同的凭据流。

在 Web A P I 中选择单个帐户时的关系图

在此方案中,Web API 控制器充当资源服务器。 身份验证筛选器验证访问令牌,并使用 [Authorize] 属性来保护资源。 当控制器或操作具有 [Authorize] 属性时,必须对该控制器或操作的所有请求进行身份验证。 否则,授权被拒绝,Web API 将返回 401(未授权)错误。

授权服务器和身份验证筛选器都调用 OWIN 中间件 组件,用于处理 OAuth2 的详细信息。 本教程稍后将更详细地介绍设计。

发送未经授权的请求

若要开始,请运行应用并单击“ 调用 API ”按钮。 请求完成后,应会在“结果”框中看到一条错误消息。 这是因为请求不包含访问令牌,因此请求是未经授权的。

结果错误消息的图像

调用 API 按钮向 ~/api/values 发送 AJAX 请求,该请求调用 Web API 控制器操作。 下面是发送 AJAX 请求的 JavaScript 代码部分。 在示例应用中,所有 JavaScript 应用代码都位于 Scripts\app.js 文件中。

// If we already have a bearer token, set the Authorization header.
var token = sessionStorage.getItem(tokenKey);
var headers = {};
if (token) {
    headers.Authorization = 'Bearer ' + token;
}

$.ajax({
    type: 'GET',
    url: 'api/values/1',
    headers: headers
}).done(function (data) {
    self.result(data);
}).fail(showError);

在用户登录之前,没有持有者令牌,因此请求中没有授权标头。 这会导致请求返回 401 错误。

下面是 HTTP 请求。 (我使用了 用于捕获 HTTP 流量的 Fiddler

GET https://localhost:44305/api/values HTTP/1.1
Host: localhost:44305
User-Agent: Mozilla/5.0 (Windows NT 6.3; WOW64; rv:32.0) Gecko/20100101 Firefox/32.0
Accept: */*
Accept-Language: en-US,en;q=0.5
X-Requested-With: XMLHttpRequest
Referer: https://localhost:44305/

HTTP 响应:

HTTP/1.1 401 Unauthorized
Content-Type: application/json; charset=utf-8
Server: Microsoft-IIS/8.0
WWW-Authenticate: Bearer
Date: Tue, 30 Sep 2014 21:54:43 GMT
Content-Length: 61

{"Message":"Authorization has been denied for this request."}

请注意,响应包含一个 Www-Authenticate 标头,并将质询集设置为 Bearer。 这表示服务器需要持有者令牌。

注册用户

应用的“注册”部分中,输入电子邮件和密码,然后单击“注册按钮。

无需对此示例使用有效的电子邮件地址,但实际应用会确认该地址。 (请参阅 使用登录、电子邮件确认和密码重置创建安全 ASP.NET MVC 5 Web 应用。对于密码,请使用类似于“Password1!”之类的内容,其中包含大写字母、小写字母、数字和非 alpha 数字字符。 为了保持应用简单,我排除了客户端验证,因此,如果密码格式出现问题,将收到 400 (请求错误) 错误。

注册用户部分的图像

注册 ”按钮将 POST 请求发送到 ~/api/Account/Register/。 请求正文是保存名称和密码的 JSON 对象。 下面是发送请求的 JavaScript 代码:

var data = {
    Email: self.registerEmail(),
    Password: self.registerPassword(),
    ConfirmPassword: self.registerPassword2()
};

$.ajax({
    type: 'POST',
    url: '/api/Account/Register',
    contentType: 'application/json; charset=utf-8',
    data: JSON.stringify(data)
}).done(function (data) {
    self.result("Done!");
}).fail(showError);

HTTP 请求,其中 $CREDENTIAL_PLACEHOLDER$ 是密码密钥值对的占位符:

POST https://localhost:44305/api/Account/Register HTTP/1.1
Host: localhost:44305
User-Agent: Mozilla/5.0 (Windows NT 6.3; WOW64; rv:32.0) Gecko/20100101 Firefox/32.0
Accept: */*
Content-Type: application/json; charset=utf-8
X-Requested-With: XMLHttpRequest
Referer: https://localhost:44305/
Content-Length: 84

{"Email":"alice@example.com",$CREDENTIAL_PLACEHOLDER1$,$CREDENTIAL_PLACEHOLDER2$"}

HTTP 响应:

HTTP/1.1 200 OK
Server: Microsoft-IIS/8.0
Date: Wed, 01 Oct 2014 00:57:58 GMT
Content-Length: 0

此请求由 AccountController 类处理。 在内部, AccountController 使用 ASP.NET 标识来管理成员身份数据库。

如果从 Visual Studio 本地运行应用,则用户帐户存储在 LocalDB 中,存储在 AspNetUsers 表中。 若要查看 Visual Studio 中的表,请单击“ 视图 ”菜单,选择“ 服务器资源管理器”,然后展开 “数据连接”。

数据连接的图像

获取访问令牌

到目前为止,我们没有执行任何 OAuth,但现在,当我们请求访问令牌时,我们将看到 OAuth 授权服务器正在执行。 在 示例应用的“登录” 区域中,输入电子邮件和密码,然后单击“ 登录”。

“登录”部分的图像

登录 ”按钮将请求发送到令牌终结点。 请求正文包含以下表单 URL 编码的数据:

  • grant_type:“password”
  • 用户名: <用户的电子邮件>
  • password: <password>

下面是发送 AJAX 请求的 JavaScript 代码:

var loginData = {
    grant_type: 'password',
    username: self.loginEmail(),
    password: self.loginPassword()
};

$.ajax({
    type: 'POST',
    url: '/Token',
    data: loginData
}).done(function (data) {
    self.user(data.userName);
    // Cache the access token in session storage.
    sessionStorage.setItem(tokenKey, data.access_token);
}).fail(showError);

如果请求成功,授权服务器在响应正文中返回访问令牌。 请注意,我们将令牌存储在会话存储中,稍后在向 API 发送请求时使用。 与某些形式的身份验证(如基于 Cookie 的身份验证)不同,浏览器不会在后续请求中自动包含访问令牌。 应用程序必须显式执行此操作。 这是一件好事,因为它限制了 CSRF 漏洞

HTTP 请求:

POST https://localhost:44305/Token HTTP/1.1
Host: localhost:44305
User-Agent: Mozilla/5.0 (Windows NT 6.3; WOW64; rv:32.0) Gecko/20100101 Firefox/32.0
Accept: */*
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
X-Requested-With: XMLHttpRequest
Referer: https://localhost:44305/
Content-Length: 68

grant_type=password&username=alice%40example.com&password=Password1!

可以看到请求包含用户的凭据。 必须使用 HTTPS 提供传输层安全性。

HTTP 响应:

HTTP/1.1 200 OK
Content-Length: 669
Content-Type: application/json;charset=UTF-8
Server: Microsoft-IIS/8.0
Date: Wed, 01 Oct 2014 01:22:36 GMT

{
  "access_token":"imSXTs2OqSrGWzsFQhIXziFCO3rF...",
  "token_type":"bearer",
  "expires_in":1209599,
  "userName":"alice@example.com",
  ".issued":"Wed, 01 Oct 2014 01:22:33 GMT",
  ".expires":"Wed, 15 Oct 2014 01:22:33 GMT"
}

为了便于阅读,我缩进了 JSON 并截断了访问令牌,这是相当长的。

access_tokentoken_typeexpires_in属性由 OAuth2 规范定义。其他属性(userName.issued.expires)仅用于信息性目的。 可以在 /Providers/ApplicationOAuthProvider.cs 文件中找到在方法中添加这些附加属性 TokenEndpoint 的代码。

发送经过身份验证的请求

有了持有者令牌后,即可向 API 发出经过身份验证的请求。 为此,请在请求中设置 Authorization 标头。 再次单击“调用 API”按钮可查看此内容。

已单击“调用 P I”按钮后的图像

HTTP 请求:

GET https://localhost:44305/api/values/1 HTTP/1.1
Host: localhost:44305
User-Agent: Mozilla/5.0 (Windows NT 6.3; WOW64; rv:32.0) Gecko/20100101 Firefox/32.0
Accept: */*
Authorization: Bearer imSXTs2OqSrGWzsFQhIXziFCO3rF...
X-Requested-With: XMLHttpRequest

HTTP 响应:

HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Server: Microsoft-IIS/8.0
Date: Wed, 01 Oct 2014 01:41:29 GMT
Content-Length: 27

"Hello, alice@example.com."

注销

由于浏览器不缓存凭据或访问令牌,因此注销只是“忘记”令牌的问题,方法是将其从会话存储中删除:

self.logout = function () {
    sessionStorage.removeItem(tokenKey)
}

了解单个帐户项目模板

在 ASP.NET Web 应用程序项目模板中选择 “个人帐户 ”时,该项目包括:

  • OAuth2 授权服务器。
  • 用于管理用户帐户的 Web API 终结点
  • 用于存储用户帐户的 EF 模型。

下面是实现这些功能的主要应用程序类:

  • AccountController。 提供用于管理用户帐户的 Web API 终结点。 该 Register 操作是我们本教程中唯一使用的操作。 类中的其他方法支持密码重置、社交登录和其他功能。
  • ApplicationUser,在 /Models/IdentityModels.cs 中定义。 此类是成员身份数据库中用户帐户的 EF 模型。
  • ApplicationUserManager,在 /App_Start/IdentityConfig.cs 此类派生自 UserManager ,对用户帐户执行操作,例如创建新用户、验证密码等,并自动保留对数据库的更改。
  • ApplicationOAuthProvider。 此对象插入 OWIN 中间件,并处理中间件引发的事件。 它派生自 OAuthAuthorizationServerProvider

主应用程序类的图像

配置授权服务器

在StartupAuth.cs中,以下代码配置 OAuth2 授权服务器。

PublicClientId = "self";
OAuthOptions = new OAuthAuthorizationServerOptions
{
    TokenEndpointPath = new PathString("/Token"),
    Provider = new ApplicationOAuthProvider(PublicClientId),
    AuthorizeEndpointPath = new PathString("/api/Account/ExternalLogin"),
    AccessTokenExpireTimeSpan = TimeSpan.FromDays(14),
    // Note: Remove the following line before you deploy to production:
    AllowInsecureHttp = true
};

// Enable the application to use bearer tokens to authenticate users
app.UseOAuthBearerTokens(OAuthOptions);

TokenEndpointPath 属性是授权服务器终结点的 URL 路径。 这是应用用于获取持有者令牌的 URL。

Provider 属性指定插入 OWIN 中间件的提供程序,并处理中间件引发的事件。

下面是应用想要获取令牌的基本流:

  1. 若要获取访问令牌,应用会将请求发送到 ~/Token。
  2. OAuth 中间件对提供程序调用 GrantResourceOwnerCredentials
  3. 提供程序调用 ApplicationUserManager 验证凭据并创建声明标识。
  4. 如果成功,提供程序将创建一个身份验证票证,该票证用于生成令牌。

授权流示意图

OAuth 中间件对用户帐户没有任何了解。 提供程序在中间件与 ASP.NET 标识之间通信。 有关实现授权服务器的详细信息,请参阅 OWIN OAuth 2.0 授权服务器

配置 Web API 以使用持有者令牌

WebApiConfig.Register 方法中,以下代码为 Web API 管道设置身份验证:

config.SuppressDefaultHostAuthentication();
config.Filters.Add(new HostAuthenticationFilter(OAuthDefaults.AuthenticationType));

HostAuthenticationFilter 类使用持有者令牌启用身份验证。

SuppressDefaultHostAuthentication 方法告知 Web API 忽略请求到达 Web API 管道之前发生的任何身份验证,无论是由 IIS 还是 OWIN 中间件。 如此,便可以限制 Web API 仅使用持有者令牌进行身份验证。

注意

具体而言,应用的 MVC 部分可能会使用表单身份验证,该身份验证将凭据存储在 Cookie 中。 基于 Cookie 的身份验证需要使用防伪令牌来防止 CSRF 攻击。 这是 Web API 的问题,因为 Web API 无法方便地将防伪令牌发送到客户端。 (有关此问题的更多背景信息,请参阅 防止 Web API 中的 CSRF 攻击。调用 SuppressDefaultHostAuthentication 可确保 Web API 不会容易受到 Cookie 中存储的凭据的 CSRF 攻击。

当客户端请求受保护的资源时,Web API 管道中会出现以下情况:

  1. HostAuthentication 筛选器调用 OAuth 中间件来验证令牌。
  2. 中间件将令牌转换为声明标识。
  3. 此时,请求已经过 身份验证 ,但未 获得授权。
  4. 授权筛选器检查声明标识。 如果声明授权用户访问该资源,则请求将得到授权。 默认情况下, [Authorize] 属性将授权任何经过身份验证的请求。 但是,可以按角色或其他声明授权。 有关详细信息,请参阅 Web API 中的身份验证和授权。
  5. 如果前面的步骤成功,控制器将返回受保护的资源。 否则,客户端会收到 401(未授权)错误。

客户端请求受保护资源时的示意图

其他资源