65.9K
CodeProject 正在变化。 阅读更多。
Home

ASP.NET Identity 2.0:Identity 2.0 和 Web API 2.2 入门

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.84/5 (21投票s)

2014 年 9 月 26 日

CPOL

21分钟阅读

viewsIcon

134165

在最近的帖子中,我介绍了在 MVC 应用程序中使用 ASP.NET Identity 2.0 的大量内容。自今年 3 月发布 RTM 版本以来,Identity 2.0 大幅扩展了 MVC 应用程序可用的身份验证/授权工具集。同样,Identity 2.0

Lock-320在最近的帖子中,我介绍了在 MVC 应用程序中使用 ASP.NET Identity 2.0 的大量内容。自今年 3 月发布 RTM 版本以来,Identity 2.0 大幅扩展了 MVC 应用程序可用的身份验证/授权工具集。

同样,Identity 2.0 扩展了通过 ASP.NET Web API 可用的身份管理工具。虽然工作方式略有不同,但仍提供了相同的灵活性和可扩展性。

在这篇文章中,我将快速浏览一个基于 Identity 2.0 的基本 Web API 应用程序,并介绍一些基本用法示例。如果您是一位经验丰富的 Web 开发人员,这里可能没有新内容——这旨在作为一个 101 级别(初级)的介绍。如果您是 Identity 2.0 和/或 ASP.NET Web API 的新手,您可能会发现其中一些信息很有用。

图片来自 Universal Pops  |  保留部分权利

在接下来的几篇文章中,我们将更深入地研究如何在 Web API 中使用 Identity 2.0。我们将了解如何扩展 Identity 模型,类似于我们为 MVC 使用 Identity 2.0 所做的工作。我们将创建一个更易于扩展的 Identity/Web API 项目。我们还将探讨各种身份验证和授权方案,并更好地理解有效的 Web API 安全性。

有关在 ASP.NET MVC 项目中使用 Identity 2.0 的回顾,请参阅

现在,我们将高层次地了解 Visual Studio 附带的基本 Web API 项目模板,并熟悉其基本结构。

入门 - 创建新的 ASP.NET Web API 项目

截至 Visual Studio 2013 Update 3,WebApi 2.0 和 Identity 2.0 已作为开箱即用的 Web API 项目模板的一部分。虽然这个默认项目模板对我来说有点杂乱,但它是一个熟悉基础知识的良好起点。

我们将在 Visual Studio 中创建一个基本的 Web API 项目,然后更新各种 Nuget 包。部分原因是我们通常希望使用最新的包版本,但更重要的是因为我们希望使用Web API 的最新版本(截至本文撰写时是 2.2 版,于 2014 年 7 月发布)。

首先,执行 文件 => 新建项目,然后选择 ASP.NET Web 应用程序

创建一个新的 Web 应用程序项目

New Project

更新解决方案的 Nuget 包

然后,打开 Nuget 包管理器并更新解决方案的 Nuget 包。从左侧的侧边栏中选择“更新”,然后选择“全部更新”。

更新解决方案的 Nuget 包

update-nuget-packages

当您进行到更新过程的某个阶段时,您会看到一个看起来很可怕的警告窗口,警告您某些项目文件将被覆盖。这些是 .cshtml 视图文件,**应该**被覆盖,因为它们正在更新以与新库一起使用。

通过单击“全部是”解决文件冲突

nuget-file-conflict-yes-to-all

你说的是视图文件?我以为我们在制作一个 Web Api 项目?

是的。但正如我所说,默认的 Web Api 项目包含了一些 MVC 的内容,用于“帮助”页面和基本主页。稍后会详细介绍。

运行并测试默认 Web API 项目

既然我们已经创建了一个新的 Web API 项目,让我们快速测试一下,看看一切是否正常运行。

打开另一个 Visual Studio 实例,并创建一个控制台应用程序。接下来,使用 Nuget 包管理器安装 Web API 客户端库。

将 Web API 客户端库安装到控制台应用程序中

install-webapi-client-libraries-in-console-app

安装完成后,检查更新。然后,将以下代码添加到 Program.cs 文件中(请务必在类顶部的 using 语句中添加 System.Net.HttpNewtonsoft.Json

针对 Web API 应用程序运行的示例代码
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Net.Http;
using Newtonsoft.Json;
 
namespace TestApi
{
    class Program
    {
        static void Main(string[] args)
        {
            string userName = "john@example.com";
            string password = "Password@123";
            var registerResult = Register(userName, password);
 
            Console.WriteLine("Registration Status Code: {0}", registerResult);
 
            string token = GetToken(userName, password);
            Console.WriteLine("");
            Console.WriteLine("Access Token:");
            Console.WriteLine(token);
            Console.Read();
        }
 
 
        static string Register(string email, string password)
        {
            var registerModel = new 
            { 
                Email = email, 
                Password = password, 
                ConfirmPassword = password 
            };
            using (var client = new HttpClient())
            {
                var response =
                    client.PostAsJsonAsync(
                    "https://:62069/api/Account/Register", 
                    registerModel).Result;
                return response.StatusCode.ToString();
            } 
        }
 
 
        static string GetToken(string userName, string password)
        {
            var pairs = new List<KeyValuePair<string, string>>
                        {
                            new KeyValuePair<string, string>( "grant_type", "password" ), 
                            new KeyValuePair<string, string>( "username", userName ), 
                            new KeyValuePair<string, string> ( "Password", password )
                        };
            var content = new FormUrlEncodedContent(pairs);
            using (var client = new HttpClient())
            {
                var response = 
                    client.PostAsync("https://:62069/Token", content).Result;
                return response.Content.ReadAsStringAsync().Result;
            }
        }
    }
}

现在,运行您的 Web API 应用程序。一旦它完成启动,运行新的控制台应用程序。

我们在这里要做的是将表示新用户的内容 POST 到我们的 Api 的 AccountsController 上定义的 Register() 方法。接下来,我们将调用 Api 的 Token 终结点并检索访问令牌,我们可以使用该令牌在后续调用 Api 时进行身份验证。

如果您不确定上面代码中发生了什么,请暂时不用担心。我们稍后会更仔细地研究这里发生了什么(以及这个“令牌”是什么)。目前,我们只是看看我们的应用程序是否正常工作。

如果一切按预期进行,我们的控制台输出应该如下所示

Register 和 GetTokenDictionary 方法的控制台输出
resitration Status Code: OK
Access Token:
{"access_token":"8NeiVoKARt5Rm_50mP2ZfudNvvPRkm-FehohX8cLmUmrm1y8kZj0PTccsH1nKbT
PFTGuKoFSfi2mfD2KD-UMOEQWVJ0PJPfiSebJubSPLElzYfR7vk_V8gcbbkLK6cZ0zS7gWrMhdbgQrrQ
yDPyR83gbkjZcE1ooQQiv9d7AEfjCassj_R76Q44PW7goMHcbFZl66dZLBKGKhf9t7lpcvWStoyS6z8a
m7B3SWppVeaTjAC5BZ6uHOG1d_0mzL8FiR_eV8NaA1w3-GfV-upErG6xk5-qykdoLHoe7zmv4tX5Dm7-
w2n3G0gAdVlPcbfAJgSvu1AmUQe85g5ABbWJ6e0OXoPtH658kYZWC0FxbWiFPLGz66wPMbUCwjk_Hq_p
rLDjOshWP6lIE3qwQ88U5ScB1XcGAXbrYYFZ9AYkwSt4o5cC2Vpw8xP2OFdLdDOUs2ESPtVK8FThhaAh
yFUUDpXSlXwhQ2nEuu27ISw1MK0bh06-xx4vcnfoaW9XuHBXm","token_type":"bearer","expire
s_in":1209599,"userName":"john@example.com",".issued":"Wed, 17 Sep 2014 02:14:29
 GMT",".expires":"Wed, 01 Oct 2014 02:14:29 GMT"}

恭喜!您刚刚注册了一个用户,并从新的 Web API 应用程序中检索了一个访问令牌。

您在控制台输出中看到的首先是注册新用户的结果。之后您看到一堆 Json,因为它确实是 Json。我们将响应内容反序列化为字符串,而我们得到的就是一个巨大的 JSON blob。

仔细看,您会发现首先是 access_token 属性,最后还有其他几个属性,例如 token_type、令牌过期时间、userName 和其他几个属性。我们很快会更仔细地研究令牌。

现在我们看到一切正常,让我们看看 Web Api 应用程序的结构,并确定在使用 Web Api 和 Identity 时需要注意的重要部分。然后我们将更多地了解这个令牌业务。 

重要提示:上述代码严格用于查看我们的应用程序是否启动和运行,并作为获取访问令牌的示例。实际上,**您总是希望在使用这种方式发送/接收持有者令牌之前实现 SSL**。

Web API 项目结构 - 概述

如前所述,默认项目模板包含一些我们可能在“真实”Web API 项目中使用的无关内容。有一个名为“Areas”的文件夹,其中包含大量用于管理“帮助”页面的内容;有一个 Views 文件夹,其中包含一些 .cshtml 视图;还有其他一些文件夹,主要支持我们项目的 MVC 部分,例如 Content 文件夹、fonts 文件夹等。目前,我们可以安全地说我们可以忽略以下项目文件夹(尽管它们可能在以后根据您的需求有用)

我们可以忽略的 VS Web API 项目文件夹(暂时)

  • Areas 文件夹
  • Content 文件夹
  • Fonts 文件夹
  • Scripts 文件夹
  • Views 文件夹

另一方面,当我们开始使用 Web API 和 Identity 2.0 时,以下文件夹对我们**很重要**,并且构成了项目结构的关键部分。我们需要熟悉这些文件夹才能根据我们的需求扩展/修改/自定义默认项目。

我们感兴趣的 VS Web API 项目文件夹

  • App-Start 文件夹
  • Controllers 文件夹
  • Models 文件夹
  • Providers 文件夹

App_Start 文件夹

App_Start 文件夹包含需要在应用程序启动时发生的各种配置项。从基于 Identity 的 Web Api 应用程序的角度来看,我们特别感兴趣的文件是 IdentityConfig.csWebApiConfig.csStartup.Auth.cs

正如这些文件的名称所暗示的,这些文件分别用于设置和/或修改每个指示服务的配置选项。IdentityConfig.csStartup.Auth.cs 是我们配置应用程序大部分身份验证和授权选项的地方。WebApiConfig.cs 是我们为传入请求设置默认路由的地方。

Controllers 文件夹

这对于任何使用过基本 MVC 项目的人来说都应该很熟悉。但是,请注意,Web Api 应用程序中的控制器与 MVC 应用程序中的控制器不同——Web Api 控制器都继承自 ApiController 基类,而熟悉的 MVC 控制器继承自 Controller 基类。

控制器是我们处理传入的 Http 请求的地方,并返回请求的资源或响应适当的 Http 状态码。我们很快会更仔细地研究这一点。

Models 文件夹

在 Web Api 应用程序中,与 MVC 应用程序非常相似,我们根据模型、视图模型和绑定模型定义应用程序使用的业务实体。模型文件夹通常是我们找到使应用程序工作所需的业务对象的地方。

通常,模型表示核心业务对象,通常持久化在我们的后端数据存储中。视图模型通常表示我们的 Api 用户所需的数据。绑定模型与视图模型相似,但它们通常表示传入数据,并由控制器用于将传入的 Xml 或 Json 内容反序列化为我们的应用程序可以使用的对象。

Providers 文件夹

在默认的 Visual Studio Web Api 解决方案中,Providers 文件夹包含一个类,即 ApplicationOAuthProvider。在默认配置中,这个类由 Startup.Auth 中定义的 ConfigureAuth() 方法使用,并定义了身份验证和授权令牌的处理方式。

Identity 2.0,因此 Web Api 2.2,严重依赖 Owin 作为默认的授权和身份验证中间件。

Web API 的默认身份模型

如果我们从 IdentityModels.cs 文件开始,我们可以看到这里没有太多内容。我们有一个基本的 ApplicationUser 类和 ApplicationDbContext,仅此而已。

身份模型类
public class ApplicationUser : IdentityUser
{
    public async Task<ClaimsIdentity> GenerateUserIdentityAsync(
        UserManager<ApplicationUser> manager, string authenticationType)
    {
        // Note the authenticationType must match the one defined in 
        // CookieAuthenticationOptions.AuthenticationType
        var userIdentity = await manager.CreateIdentityAsync(this, authenticationType);
  
        // Add custom user claims here
        return userIdentity;
    }
}
  
public class ApplicationDbContext : IdentityDbContext<ApplicationUser>
{
    public ApplicationDbContext()
        : base("DefaultConnection", throwIfV1Schema: false)
    {
    }
  
    public static ApplicationDbContext Create()
    {
        return new ApplicationDbContext();
    }

AccountViewModels.csAccountBindingModels.cs 文件包含 AccountController 方法使用的各种 ViewModel 和 BindingModel。打开它们,了解一下里面有什么。ViewModel 代表我们的 Api 期望序列化并作为响应内容**输出**的数据,而 BindingModel 通常代表我们期望在 HTTP 请求中**接收**的数据,在由适当的格式提供程序反序列化之后。

AccountController - 一个 ApiController

接下来我们来看看 AccountController。我们不会在这里详细介绍这个类中的代码,但可以快速浏览一下其中的各种方法。许多方法看起来与 MVC 项目中的 AccountController 惊人地相似。然而,有一些重要的区别。

首先,请注意 AccountController 上的方法要么返回某种数据实体(例如 GetUserInfo() 方法返回的 UserInfoViewModel),要么返回 IHttpActionResult 的实例。

换句话说,正如我们已经怀疑的那样,Web API 控制器上的 Action 方法不返回视图。它们最终返回 HttpResponse 消息(在控制器本身的下游协助下),其中任何内容都编码在响应正文中。

通过比较,我们发现同样位于 Controllers 文件夹中的 HomeController 继承自熟悉的 Controller 基类,并且在 HomeController 上定义的 Action 方法返回标准的 ActionResult,通常是视图。这是因为 HomeController 在这里被包含以支持我们的 Web Api 的 MVC 组件。我们暂时可以忽略 HomeController,因为它目前不提供对我们任何 Api 方法的访问。

身份配置

App_Start 文件夹中,打开 IdentityConfig.cs 文件。这里的内容比标准 ASP.NET MVC 项目中的类似文件少得多。本质上,这里唯一发生的事情是为 Web Api 应用程序定义了 ApplicationUserManager。然而,这里发生了一些重要的配置项。

IdentityConfig.cs 中定义的 ApplicationUserManager 类
    public static ApplicationUserManager Create(
        IdentityFactoryOptions<ApplicationUserManager> options, 
        IOwinContext context)
    {
        var manager = new ApplicationUserManager(
            new UserStore<ApplicationUser>(context.Get<ApplicationDbContext>()));
  
        // Configure validation logic for usernames
        manager.UserValidator = new UserValidator<ApplicationUser>(manager)
        {
            AllowOnlyAlphanumericUserNames = false,
            RequireUniqueEmail = true
        };
        // Configure validation logic for passwords
        manager.PasswordValidator = new PasswordValidator
        {
            RequiredLength = 6,
            RequireNonLetterOrDigit = true,
            RequireDigit = true,
            RequireLowercase = true,
            RequireUppercase = true,
        };
        
        var dataProtectionProvider = options.DataProtectionProvider;
        if (dataProtectionProvider != null)
        {
            manager.UserTokenProvider = new DataProtectorTokenProvider<ApplicationUser>(
                dataProtectionProvider.Create("ASP.NET Identity"));
        }
        return manager;
    }
}

在此文件中,我们定义了应用程序的密码要求,并对用户名和电子邮件地址施加了潜在限制。

虽然 Web Api 项目的默认实现中没有包含,但这里也是我们可能定义用于电子邮件或短信的双因素身份验证提供程序的地方,也是我们可能定义电子邮件帐户确认配置的地方。

如果我们想扩展 Web Api 应用程序中的 Identity 实现以包含角色和一些数据库初始化,那么此文件将是我们最有可能定义和配置 RoleManagerDbInitializer 的地方。

启动身份验证和授权配置

Web API 中的主要身份验证和授权策略是基于令牌的。我们很快会更仔细地了解这意味着什么,但现在,假设为了访问 Api 的任何受保护部分,您需要在任何传入的 Http 请求中提供访问令牌。

Startup.Auth.cs 文件中,我们看到了身份验证处理配置的执行位置。我们看到 Startup.Auth.cs 文件包含一个 partial class,即 Startup,它扩展了我们项目根级别定义的默认 Startup 类。

如果我们查看后者(项目根级别的 Startup 类),我们会看到一个方法调用,即 ConfigureAuth() 方法。

核心启动类
public partial class Startup
{
    public void Configuration(IAppBuilder app)
    {
        ConfigureAuth(app);
    }
}

ConfigureAuth() 方法在 Startup.Auth.cs 中的部分类中定义

Startup.Auth.cs 中的 ConfigureAuth 方法
public void ConfigureAuth(IAppBuilder app)
{
    // Configure the db context and user manager to use a single instance per request
    app.CreatePerOwinContext(ApplicationDbContext.Create);
    app.CreatePerOwinContext<ApplicationUserManager>(ApplicationUserManager.Create);
  
    // Enable the application to use a cookie to store information for the signed in user
    // and to use a cookie to temporarily store information about a 
    // user logging in with a third party login provider
    app.UseCookieAuthentication(new CookieAuthenticationOptions());
    app.UseExternalSignInCookie(DefaultAuthenticationTypes.ExternalCookie);
  
    // Configure the application for OAuth based flow
    PublicClientId = "self";
    OAuthOptions = new OAuthAuthorizationServerOptions
    {
        TokenEndpointPath = new PathString("/Token"),
        Provider = new ApplicationOAuthProvider(PublicClientId),
        AuthorizeEndpointPath = new PathString("/api/Account/ExternalLogin"),
        AccessTokenExpireTimeSpan = TimeSpan.FromDays(14),
        AllowInsecureHttp = true
    };
  
    // Enable the application to use bearer tokens to authenticate users
    app.UseOAuthBearerTokens(OAuthOptions);
  
    // Uncomment the following lines to enable logging in with 
    // third party login providers
    //app.UseMicrosoftAccountAuthentication(
    //    clientId: "",
    //    clientSecret: "");
  
    //app.UseTwitterAuthentication(
    //    consumerKey: "",
    //    consumerSecret: "");
  
    //app.UseFacebookAuthentication(
    //    appId: "",
    //    appSecret: "");
  
    //app.UseGoogleAuthentication(new GoogleOAuth2AuthenticationOptions()
    //{
    //    ClientId = "",
    //    ClientSecret = ""
    //});
}

在此方法中,我们配置 cookie 和令牌身份验证的选项,并可以选择允许通过各种第三方提供商(如 Facebook 或 Twitter)登录。

我们特别感兴趣的是突出显示的那行,它配置应用程序使用持有者令牌。我们很快就会发现这为什么很重要。目前,请记住 Startup.Auth.cs 文件是您配置整个应用程序的授权和身份验证选项,并确定您的应用程序将接受和不接受哪些凭据的地方。

令牌身份验证和持有者令牌

标准的 Visual Studio Web Api 模板配置为使用 OAuth 持有者令牌作为主要的身份验证方式。持有者令牌正是其名称所暗示的——Web Api 将持有令牌的人视为经过正确身份验证的用户(前提是令牌未过期,根据 Startup.Auth.cs 中的配置设置)。

这可能会带来一些严重的安全隐患。如果恶意行为者能够拦截客户端请求并从请求头中获取持有者令牌,他们将能够以经过身份验证的 Api 用户身份获取访问权限。

因此,如果您计划部署使用默认持有者令牌身份验证方案的 Web Api 应用程序,**生产应用程序必须实现 SSL/TSL(即 HTTPS)**以加密和保护客户端与您的 Api 之间的流量,这一点至关重要。

如果我们再次查看之前运行应用程序时的控制台输出,我们会看到从令牌中获取的信息。

默认访问令牌的内容
registration Status Code: OK

Access Token:
{"access_token":"8NeiVoKARt5Rm_50mP2ZfudNvvPRkm-FehohX8cLmUmrm1y8kZj0PTccsH1nKbT
PFTGuKoFSfi2mfD2KD-UMOEQWVJ0PJPfiSebJubSPLElzYfR7vk_V8gcbbkLK6cZ0zS7gWrMhdbgQrrQ
yDPyR83gbkjZcE1ooQQiv9d7AEfjCassj_R76Q44PW7goMHcbFZl66dZLBKGKhf9t7lpcvWStoyS6z8a
m7B3SWppVeaTjAC5BZ6uHOG1d_0mzL8FiR_eV8NaA1w3-GfV-upErG6xk5-qykdoLHoe7zmv4tX5Dm7-
w2n3G0gAdVlPcbfAJgSvu1AmUQe85g5ABbWJ6e0OXoPtH658kYZWC0FxbWiFPLGz66wPMbUCwjk_Hq_p
rLDjOshWP6lIE3qwQ88U5ScB1XcGAXbrYYFZ9AYkwSt4o5cC2Vpw8xP2OFdLdDOUs2ESPtVK8FThhaAh
yFUUDpXSlXwhQ2nEuu27ISw1MK0bh06-xx4vcnfoaW9XuHBXm", "token_type":"bearer" ,"expire
s_in":1209599,"userName":"john@example.com",".issued":"Wed, 17 Sep 2014 02:14:29
 GMT",".expires":"Wed, 01 Oct 2014 02:14:29 GMT"}

我们看到 token_type 属性将其标识为持有者令牌。

用一个相当陈词滥调的例子来说,持有者令牌可以比作货币——它们对任何出示它们的人都有效。

我们目前为止以及本文其余部分使用的示例都将使用标准 HTTP。在您开发应用程序或尝试示例时,这很好。但是,如果您要部署生产应用程序,您肯定会希望实现 SSL/TSL,或者研究替代的身份验证/授权方案。

我们将在下一篇文章中更深入地研究令牌和替代身份验证机制。

使用令牌身份验证和从客户端应用程序访问 Web API

我们已经了解了如何从我们的 Api 获取一个基本的持有者令牌。现在让我们更详细地了解如何获取令牌,然后针对基本的默认 Web Api 项目发出一些请求。

首先,让我们稍微修改一下代码。我们要做的是将 JSON 令牌分解成一个 Dictionary<string, string>,这样我们就可以访问 access_token 属性。我们需要在后续的 Api 请求头中提交这个属性,以便进行身份验证。

如下修改 GetToken() 方法,使其返回 Dictionary 而不是字符串。我们也可以将其重命名为 GetTokenDictionary() 以更好地反映其功能。

新的 GetTokenDictionary() 方法
static Dictionary<string, string> GetTokenDictionary(
    string userName, string password)
{
    var pairs = new List<KeyValuePair<string, string>>
                {
                    new KeyValuePair<string, string>( "grant_type", "password" ), 
                    new KeyValuePair<string, string>( "username", userName ), 
                    new KeyValuePair<string, string> ( "Password", password )
                };
    var content = new FormUrlEncodedContent(pairs);
 
    using(var client = new HttpClient())
    {
        var response =
            client.PostAsync("https://:62069/Token", content).Result;
        var result = response.Content.ReadAsStringAsync().Result;
 
        // Deserialize the JSON into a Dictionary<string, string>
        Dictionary<string, string> tokenDictionary =
            JsonConvert.DeserializeObject<Dictionary<string, string>>(result);
        return tokenDictionary;
   }
}

现在让我们更新 Main() 方法以使用我们的新字典,并将各种令牌属性单独写入控制台。

更新 Main() 方法以使用反序列化的令牌信息
static void Main(string[] args)
{
    string userName = "john@example.com";
    string password = "Password@123";
    var registerResult = Register(userName, password);
 
    Console.WriteLine("resitration Status Code: {0}", registerResult);
 
    //string token = GetToken(userName, password);
    Dictionary<string, string> token = GetTokenDictionary(userName, password);
    Console.WriteLine("");
    Console.WriteLine("Access Token:");
    Console.WriteLine(token);
 
    // Write each item in the dictionary out to the console:
    foreach (var kvp in token)
    {
        Console.WriteLine("{0}: {1}", kvp.Key, kvp.Value);
    }
 
    Console.WriteLine("");
    Console.Read();
}

如果我们现在运行这两个应用程序,我们会看到控制台输出略有不同

使用 GetTokenDictionary() 方法的控制台输出
Registration Status Code: BadRequest

Access Token:
System.Collections.Generic.Dictionary`2[System.String,System.String]
access_token: zPrC7-CyHljhkPYQIDawhu7kgRd86p7oSJpRqHGifuANmEtM61syU5t6ciPGJrA3RX
I9u79IIOFaV3w5_GAQeF28DlUnc2HSkCxZsnqaYojLWfJ6gc8gfUlZo76SeJ7iO7MT6fdo8C5XgM_Geq
yun_8ykut9N456F41dI5PrrR6CyNc0ss_hy9OzdxnoqUdERglooNUrEcEt7WdZ9FHJ-cAi15fVPfV4z4
dUZZylrIyHuNSLVReet-zL769IEPvhgYixrp_hMgGQ6lDx8YMPTWvK_SVbe4W89DrHl1PbqfkiVgbJgJ
M09kmmIytNCFl_ua_GOdx1WyxXfPv0TLOmAgPX3klI4r_pglZl1QA0vihTN7zLsP2bkxIbMCBac3kq8z
4JT1JalxZ0OgArkW-Gy2qZJ-o-mPATCPUXLHtEd3z4lze17ECuCJyZzfLts3NN-hJgNwbmcqvGNvcakp
Y6SQ6U_ACdBJ3Q2JgZZeWf75pDupjeQbMhTqAPWUq9n35k
token_type: bearer
expires_in: 1209599
userName: john@example.com
.issued: Wed, 17 Sep 2014 02:55:45 GMT
.expires: Wed, 01 Oct 2014 02:55:45 GMT

首先,为什么我们的注册状态码显示“Bad Request”?我们根本没有更改那部分代码……这就是问题所在。

我们已经注册了一个名为 john@example.com 的用户,Web Api 已经抛出了一个验证错误(其详细信息实际上隐藏在响应内容中,但我们稍后再讨论)。

我们现在可以忽略它。

我们控制台输出的下一部分是我们感兴趣的。请注意,虽然大的加密 access_token 仍然看起来像一堆乱码,但我们的整个令牌被整齐地分割成每行的组件属性。

现在,让我们尝试从我们的 Api 访问一些 Identity 信息。如果查看 Web Api 项目中的 AccountController,我们会看到一个名为 GetUserInfo() 的控制器方法。因为我们的 GetUserInfo() 使用 [Route] 属性“UserInfo”进行修饰,并且我们的帐户控制器 [Route] 属性指定路由为“api/Account”,我们知道可以使用路由“<host>/api/Account/UserInfo”访问此方法。

将以下静态方法添加到我们的控制台应用程序中的 Program 类。

向 Program 类添加 GetUserInfo() 方法
static string GetUserInfo()
{
    using(var client = new HttpClient())
    {
        var response = 
            client.GetAsync("https://:62069/api/Account/UserInfo").Result;
        return response.Content.ReadAsStringAsync().Result;
    }
}

这个简单的方法使用 HttpClient 实例访问我们 AccountController 上的 GetUserInfo() 方法,将响应内容读取为字符串,并返回结果。

现在,我们可以从控制台程序的 Main() 方法中调用此方法,看看我们得到了什么。如下更新 Program 类的 Main() 方法。

向 Program 类的 Main() 方法添加对 GetUserInfo() 的调用
static void Main(string[] args)
{
    string userName = "john@example.com";
    string password = "Password@123";
    var registerResult = Register(userName, password);
 
    Console.WriteLine("Registration Status Code: {0}", registerResult);
 
    Dictionary<string, string> token = GetTokenDictionary(userName, password);
    Console.WriteLine("");
    Console.WriteLine("Access Token:");
    Console.WriteLine(token);
 
    // Write each item in the dictionary out to the console:
    foreach (var kvp in token)
    {
        Console.WriteLine("{0}: {1}", kvp.Key, kvp.Value);
    }
 
    Console.WriteLine("");
 
    Console.WriteLine("Getting User Info:");
    Console.WriteLine(GetUserInfo());
}

现在,再次启动您的 Web Api 项目,等待它完全启动,然后运行控制台应用程序。您的控制台输出应该如下所示:

更新客户端调用 GetUserInfo() 方法后的控制台输出
Registration Status Code: BadRequest

Access Token:
System.Collections.Generic.Dictionary`2[System.String,System.String]
access_token: c47uW0q-1qsIm88et81YqFzGz0Nt2GflLZJ3nLLDUPIS8epwMiBMkG9lCmF7-Rk8Ji
KA33JJkgtFl-3mjn78N-iQcX2pLxEsYf4h65njj2BaSRCSheCyfWY5WcS2MPipRFfwr1e-wx49R4Awo3
DHk2nJmMe_ARIASzw7Ger4gpJgNrqxt8B4QWcJyjgrr7RwK95alKQ4MY-ZlzJyNdWthdCSeykTvzLJQ-
rGjH7KT-SYwknyt62Fm2bwE7WzcudFgs1RIq8HDzuPiM9Fx9dBLhhPT2sCq8iV1dDFrCTnDsNoLA5ncG
y9BncGFb5fmkqibP1tV8k2xW0OxqaAuVz4jaS212--o2P9JQ5kCmC6gSYH0faBgNva3SV8uZnM64YoyY
gJ3i_lOql0pxxjtqJYUYQLYSDwzYVJXXl_PiKLHGrjZhtn480kzSdfkgdv1i-jiqap2ymzIeJjGcd6Tm
fePqCXeiKLUEO3FsOJ4VeRkXFXEkXpFBbuGWIe6N64M4M3
token_type: bearer
expires_in: 1209599
userName: john@example.com
.issued: Sun, 21 Sep 2014 00:56:49 GMT
.expires: Sun, 05 Oct 2014 00:56:49 GMT
 
Getting User Info:
{"Message":"Authorization has been denied for this request."}

但是……等等。发生了什么?最后一行,我们对 GetUserInfo() 的调用结果显示授权被拒绝。我们缺少了什么?

请注意,我们的 AccountController 类在类声明中用 [Authorize] 属性修饰。您可能已经意识到,这意味着不是任何人都可以调用 AccountController。类本身用 [Authorize] 属性修饰,因此默认情况下只有授权用户才能使用 HTTP 调用类上定义的 Action 方法。Register() 方法用 [AllowAnonymous] 属性修饰,因此未经授权/未注册的用户可以注册,但其余方法对未经授权的用户不可访问。

在我们的案例中,尽管我们实际上通过调用 Web Api 应用程序的 Token 终结点从 Web Api 获取了访问令牌,但我们实际上并未在对 GetUserInfo() 的请求中提供令牌。

设置 Web API 请求的默认访问令牌

为了在我们的 HTTP 请求中传递令牌,我们可以像这样使用 HttpClient 的 DefaultRequestHeaders 属性:

为客户端请求设置默认授权头
HttpClient client = new HttpClient();
client.DefaultRequestHeaders.Authorization = 
    new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token);

我们可以像这样修改 Program 类中的 GetUserInfo() 方法

为 GetUserInfo() 方法的请求设置默认授权头
static string GetUserInfo(string token)
{
    using(var client = new HttpClient())
    {
        client.DefaultRequestHeaders.Authorization = 
        new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token);
 
        var response = client.GetAsync("https://:62069/api/Account/UserInfo").Result;
        return response.Content.ReadAsStringAsync().Result;
    }
}

请注意,我们现在将访问令牌作为方法参数传入,指定它是一个持有者令牌,然后使用该令牌设置我们的默认授权头。

现在,我们只需在从 Main() 调用方法时将该令牌传递给它。

从 Main() 调用时将访问令牌传递给 GetUserInfo()
static void Main(string[] args)
{
    string userName = "john@example.com";
    string password = "Password@123";
    var registerResult = Register(userName, password);
 
    Console.WriteLine("Registration Status Code: {0}", registerResult);
 
    Dictionary<string, string> token = GetTokenDictionary(userName, password);
    Console.WriteLine("");
    Console.WriteLine("Access Token:");
    Console.WriteLine(token);
 
    // Write each item in the dictionary out to the console:
    foreach (var kvp in token)
    {
        Console.WriteLine("{0}: {1}", kvp.Key, kvp.Value);
    }
 
    Console.WriteLine("");
 
    Console.WriteLine("Getting User Info:");
    Console.WriteLine(GetUserInfo(token["access_token"]));
}

请注意——我们没有在这里传入从 GetToken() 方法返回的整个令牌字典——只有实际的 access_token 本身。在此代码中,我们在将其作为参数传递给 GetUserInfo() 方法时,使用“access_token”键从令牌字典中取出它。

如果我们现在运行控制台应用程序,我们会得到以下结果:

运行控制台应用程序并传递正确的身份验证令牌
Registration Status Code: BadRequest
Access Token:
System.Collections.Generic.Dictionary`2[System.String,System.String]
access_token: ygYowtlVKbwyd3J9Lown2Py2IcMEwGgjfS5YAbJJjlhADh4HURG6upqIah4zQjqLgH
MjlyuiwKEcpzDv95Y0OpIqGO5pU_I4MmHNnLttMFORDFo-u4B0q9KUsiGskHjt_q25cIy5ZZNAejmA4B
u8qJKuxWagK33-XlQYMD_USVTShfUFkjMpi7IxffPmjpzWl5ipUzxnu4t-4LpR87QuWwIv7novf_o8Sl
9EAXc7ySqDZ0SzB1WgtDK4or7oLeIFMkouwOD9PK-E3FJTTmfpPtXT6RIdL93FEYM5oxgxTiHSLt_cRL
1Mb5kyIILcl6dCR7OuGn_8QN3jabKOmXg5q5XE52m--BMzJwUESTzXjDge-_2XoNWI09jTki9RXWg2fV
PL7DIhSwSfIff8AE0hiZm2cvEYaqPHzej221TKI_YX9DQGOrtmfLLpxx_lmtfbN1rbnwYYSa51d_vPDV
yzsfZbC2vA-xzxWJS3LP4Qm_I8ZvJp-JKVu47Q-Y5Z0ZG_
token_type: bearer
expires_in: 1209599
userName: john@example.com
.issued: Sun, 21 Sep 2014 02:23:36 GMT
.expires: Sun, 05 Oct 2014 02:23:36 GMT
 
Getting User Info:
{"Email":"john@example.com","HasRegistered":true,"LoginProvider":null}

现在,这才像话。

清理那些该死的代码!

您可能已经注意到,我们的示例已经开始出现一些“代码异味”。好吧,首先,这些只是示例,该死的。但是,我们可以立即看到一些机会来整理一下,进行一些重构。

您实际处理这个问题的方式将完全取决于您的具体项目需求。但我们可以立即看到,每次我们从客户端访问 Web Api 时,都需要一个 HttpClient 实例。此外,很可能我们通常需要在请求中包含一个身份验证令牌。

我们可以添加一个静态工厂方法 CreateClient() 来为我们处理这个问题,然后从我们的每个客户端方法中调用它。

添加 CreateClient() 方法(例如)
static HttpClient CreateClient(string accessToken = "")
{
    var client = new HttpClient();
    if(!string.IsNullOrWhiteSpace(accessToken))
    {
        client.DefaultRequestHeaders.Authorization = 
            new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token);
    }
    return client;            
}

现在,如果提供了令牌,将添加默认的授权头。如果未提供,将返回一个没有授权头的 HttpClient

现在,我们的控制台应用程序中的 GetUserInfo() 方法可能看起来像这样:

修改后的 GetUserInfo() 方法
static string GetUserInfo(string token)
{
    using(var client = CreateClient(token))
    {
        var response = client.GetAsync("https://:62069/api/Account/UserInfo").Result;
        return response.Content.ReadAsStringAsync().Result;
    }
}

我们仍然有 using 块,并且我们实际上只节省了一行代码。但是,HttpClient 的初始化和访问令牌的设置现在都在同一个地方进行。

关于安全性、持有者令牌和 SSL 的说明

如前所述,当使用默认的 Visual Studio 项目模板时,ASP.NET Web Api 项目开箱即用持有者令牌。为了实现适当的安全性,使用此类身份验证的 Api 应**始终**使用 SSL/TLS,尤其是在接受用户凭据以便在 Token 终结点提供访问令牌时。

默认 ASP.NET Web API 项目中没有的内容

您可能会注意到,在基于 Identity 的基本 Web Api 项目中,没有角色管理方法,也没有 RolesAdminController(甚至没有 UserAdminController)。事实上,在这个早期阶段根本就没有角色。AccountController 包含足以注册新用户、执行一些基本管理(例如更改密码)的方法,仅此而已。

如果我们的 Web Api 项目需要基于角色的授权,我们将需要自己添加。同样,如果我们需要通过 Api(或者甚至使用应用程序内的一些 MVC 页面进行基于 GUI 的管理)实现更多管理灵活性,我们也需要自己添加。

最后,在我们急于添加角色等之前,我们应该检查基于角色的授权是否最适合我们的需求。Identity 2.0 和 Web Api 轻松支持基于声明的授权,这为更复杂的授权场景提供了一些独特的优势。

我们将在接下来的帖子中探讨所有这些以及更多内容。

深入研究 Identity 2.0 和 Web API

在这篇文章中,我们非常广泛地了解了 Web Api 2.2 项目的结构,以及 Identity 2.0 框架的主要部分如何契合。我们研究了使用持有者令牌作为默认身份验证机制,如何从 Api 令牌端点检索令牌,以及如何执行一些非常基本的 Api 访问。

在接下来的帖子中,我们将:

  • 更仔细地研究基于令牌的身份验证和授权,以及它与我们熟悉的“用户和角色”模型的关系。
  • 在 ASP.NET Web API 的上下文中扩展 Identity 2.0 模型。
  • 将模型数据序列化和反序列化到 HTTP 请求和响应中。
  • 为 Web Api 定制身份管理。
  • 使用 Identity 2.0 和 ASP.NET Web Api 创建身份验证服务。

这基本上是使用 Identity 2.0 和 ASP.NET Web Api 的 101 级(初级)介绍。未来还有更多内容,并且需要了解很多才能在 Web Api 上下文中充分利用 ASP.NET Identity 框架并正确保护 Web Api。

其他资源和感兴趣的项目

© . All rights reserved.