在 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 模块。

有关这些选项的更多详细信息,请参阅在 Visual Studio 2013 中创建 ASP.NET Web 项目

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

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

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

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

示例窗体的图像

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

在此过程中,我将介绍:

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

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

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

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

本地登录凭据流

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

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

本地登录凭据流的示意图

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

在 Web A P I 中选择单个帐户时的示意图

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

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

发送未经授权的请求

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

结果错误消息的图像

调用 API ”按钮将 AJAX 请求发送到 ~/api/values,这会调用 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);

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

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

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!”的内容,其中包含大写字母、小写字母、数字和非字母数字字符。 为了简化应用,我遗漏了客户端验证,因此,如果密码格式有问题,则会出现“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 本地运行应用,用户帐户将存储在 AspNetUsers 表中的 LocalDB 中。 若要查看 Visual Studio 中的表,请单击“ 视图 ”菜单,选择“ 服务器资源管理器”,然后展开“ 数据连接”。

数据连接的图像

获取访问令牌

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

log in 节的图像

登录 ”按钮将请求发送到令牌终结点。 请求正文包含以下格式 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 ”按钮以查看此按钮。

单击“调用 A 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 模型。

下面是实现这些功能main应用程序类:

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

main应用程序类的图像

配置授权服务器

在 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 (未授权) 错误。

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

其他资源