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

ASP.NET Web API:理解 OWIN/Katana 身份验证/授权第三部分:添加 Identity

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.67/5 (4投票s)

2015年3月22日

CPOL

17分钟阅读

viewsIcon

24126

这是系列文章的第三篇,在该系列中,我们基本上从零开始构建了一个最小化的、自托管的、基于 OWIN 的 Web Api 应用程序。在这篇文章中,我们将引入一个最小化的 ASP.NET Identity 实现。

 
 
 

eyball-500这是系列文章的第三篇,在该系列中,我们基本上从零开始构建了一个最小化的、自托管的、基于 OWIN 的 Web Api 应用程序。我们的目标是更好地理解在基于 OWIN 的环境中,各个组件是如何组合和交互的,并且不创建对 IIS 或重量级框架的任何依赖。System.Web.dll.

到目前为止,我们已经创建了一个基本的 Web Api,并使用基本的 OWIN 授权和我们自己的一套模型,实现了我们自己的身份验证/授权机制。我们到目前为止构建的内容,代表了在一个基于 OWIN 的应用程序中身份验证和授权工作方式的基本模型。为了保持一个简单易懂的结构,我们采取了一些架构上的捷径,因为我们的目标到目前为止一直强调概念而非细节。

Imagebyalles-schlumpf  |  保留部分权利

先前的文章,按顺序

在这篇文章中,我们将引入 ASP.NET Identity 框架,理想情况下,我们将再次更好地理解 Identity 如何融入一个通用的 Web Api 应用程序,并且我们将让 Identity 为我们处理一些繁重的工作,比如加密和安全细节等困难且即使是专家也难以做对的细节。

示例源代码

我们正在通过一系列文章来构建一个项目。为了让每篇文章的源代码都有意义,我正在设置一些分支来阐述每篇文章的概念。

在 Github 上,Web Api 仓库到目前为止的分支如下:

  • 分支: Master - 始终是最新版本,包含所有更改。
  • 分支: auth-db - 我们在上一篇文章中构建的代码,为我们的认证系统添加了一个持久化层。
  • 分支: auth-identity - 我们将在本文中构建的代码。我们将从上一篇文章结束的地方开始,并进行修改以引入一个使用 Identity 框架的最小实现。

API 客户端应用程序的代码在另一个仓库中,其分支如下:

  • 分支: Master - 始终是最新版本,包含所有更改。
  • 分支: owin-auth - 添加了异步方法,以及对 Web Api 应用程序的基于令牌的身份验证调用。客户端应用程序的代码保持不变,除非我们需要更换用户凭据。

在上一篇文章中,我们创建了这些类:MyUserMyUserClaim这些类是为了在 OWIN/Katana 环境中实现我们的授权和认证机制,并且它们也成为了我们通过 Entity Framework 实现数据库持久化的 code-first 模型。我们还创建了MyUserStore类,它包含了从我们的后备存储中保存和检索用户身份验证数据的必要方法。

我们已经将这些非常基础的类组合成了一个功能性的身份验证和授权框架,并在我们的应用程序中使用它们来执行正确验证用户所需的基本功能,并基于每个用户拥有的角色声明建立了一个最小的授权机制。

在本文中,我们将改用 ASP.NET Identity,用一个功能齐全(尽管是基础的)的认证系统来取代我们粗糙的、自制的机制。

核心 Identity 框架

要理解 Identity 如何融入我们的应用程序以及 OWIN/Katana 环境,研究 Identity 框架本身的结构是很有帮助的。

实际的核心 Identity 库是Microsoft.AspNet.Identity.Core。这个库定义了许多 Identity 功能所基于的接口,以及少量以这些接口表示的具体实现类。实际上,我们的项目中已经有了这个库,因为我们之前引入了Microsoft.AspNet.Identity.Owin库(通过 Nuget)。到目前为止我们还没有使用任何 Identity 组件,但是我们使用了那个 Nuget 包中包含的依赖库的一些项目,例如Microsoft.Owin.SecurityMicrosoft.Owin.OAuth

总的来说,应用程序中可能需要使用的模型都以接口的形式表示,而框架的内部则为这些接口提供实现。应用程序和/或引入的任何其他框架需要为这些模型接口提供具体的实现。

例如,核心 Identity 框架为我们提供了一对接口来表示一个用户

IUser 接口的两个版本
// Interface with generic type argument for the Key:
public interface IUser<out TKey>
{
    TKey Id
    {
        get;
    }
 
    string UserName
    {
        get;
        set;
    }
}
// Interface derived from IUser<Tkey> Specifying string Key:
public interface IUser : IUser<string> { }

以类似的方式,核心 Identity 框架中定义的大多数用于表示持久化模型的接口,都允许我们指定要使用的主键类型。

此外,许多 Identity 接口依赖于框架中的其他接口。在这些情况下,接口是以代表依赖项具体实现的泛型类型参数来表示的。例如,核心 Identity 框架提供了两个接口来表示 UserStore

IUserStore 接口的两个版本
// Interface with generic type arguments for User, and the User Key:
public interface IUserStore<TUser, in TKey> : IDisposable
where TUser : class, IUser<TKey>
{
    Task CreateAsync(TUser user);
    Task DeleteAsync(TUser user);
    Task<TUser> FindByIdAsync(TKey userId);
    Task<TUser> FindByNameAsync(string userName);
    Task UpdateAsync(TUser user);
}
 
// Interface expressing IUserStore in terms of generic User type, 
// and specifying a string User Key
public interface IUserStore<TUser> : IUserStore<TUser, string>, IDisposable
where TUser : class, IUser<string>
{
}

如果你使用像 Telerik 出色的 Just Decompile 这样的免费工具来探索这个Microsoft.AspNet.Identity.Core库,你可以研究各种接口和具体类,并了解它们之间的关系。然而,即使只用 VS 对象浏览器快速查看一下,也会发现我们需要的用于在应用程序中实现 Identity 的基本模型接口并没有具体的实现。为此,我们要么需要自己实现,要么引入另一个提供现成实现的库。

由于我们已经在使用 Entity Framework,在我们的情况下这很简单。

Identity 与 Entity Framework

Microsoft.AspNet.Identity.EntityFramework库提供了从使用 Entity Framework 的应用程序中使用 Identity 所需的具体实现类。在这个库中,我们找到了一些可以直接使用或者可以根据需要进行扩展和自定义的模型类。例如,IdentityUser类为IUser<Tkey> :

提供了具体的实现。

基础 IdentityUser 类

// Base implements IUser<TKey> and is expressed with generic type arguments
// for other model types required by Identity Framework:
public class IdentityUser<TKey, TLogin, TRole, TClaim> : IUser<TKey>
    where TLogin : IdentityUserLogin<TKey>
    where TRole : IdentityUserRole<TKey>
    where TClaim : IdentityUserClaim<TKey>
{
    public IdentityUser()
    {
        this.Claims = new List<TClaim>();
        this.Roles = new List<TRole>();
        this.Logins = new List<TLogin>();
    }
 
    public virtual TKey Id { get; set; }
    public virtual string UserName { get; set; }
    public virtual string Email { get; set; }
    public virtual bool EmailConfirmed { get; set; }
    public virtual string PhoneNumber { get; set; }
    public virtual bool PhoneNumberConfirmed { get; set; }
 
    public virtual string SecurityStamp { get; set; }
    public virtual bool TwoFactorEnabled { get; set; }
    public virtual string PasswordHash { get; set; }
 
    public virtual int AccessFailedCount { get; set; }
    public virtual bool LockoutEnabled { get; set; }
    public virtual DateTime? LockoutEndDateUtc { get; set; }
 
    public ICollection<TLogin> Logins { get; set; }
    public ICollection<TRole> Roles { get; set; }
    public ICollection<TClaim> Claims { get; set; }
}
 
 
// Alternate implementation derives from Generic implementation, 
// and expresses Generic model types in terms of classes defined 
// within Identity.EntityFrameowrk Library: 
public class IdentityUser : IdentityUser<string, IdentityUserLogin, 
    IdentityUserRole, IdentityUserClaim>, IUser, IUser<string>
{
    public IdentityUser()
    {
        this.Id = Guid.NewGuid().ToString();
    }
 
    public IdentityUser(string userName) : this()
    {
        this.UserName = userName;
    }
}

在上面的例子中我们可以看到,基础的IdentityUser实现中已经有很多现成的功能。另外,请注意,基础实现中提供的各种泛型类型参数再次被提供,以便我们可以用自定义实现来扩展我们的 Identity 模型类,例如,如果我们想使用整型主键而不是字符串

Identity.EntityFramework库为 Identity.Core 中定义的其他接口提供了类似的实现。此外,正如我们可能预料的那样,Identity.EntityFramework还包含了IdentityDbContext类,它将各种模型类整合在一起,以便在 EF/Code-First 应用程序中使用。

这里的重点是理解Identity.Core库提供了我们需要的接口,并实现了这些接口之间的交互。我们需要自己为这些接口提供实现,要么自己编写,要么使用特定于我们持久化模型的库,例如Identity.EntityFramework.

通过 Nuget 添加 Identity.EntityFramework 库

由于我们在我们的最小化 OWIN Web Api 项目中使用了 Entity Framework,我们将引入上面讨论的Identity.EntityFramework包,以利用 Identity 团队提供的现成 Identity 实现。

注意: 这篇文章假设您使用的是 ASP.NET < 5.0 和 Identity 2.1。截至本文发布之日,该库的最新包是针对 ASP.NET 5 的 Identity 3.0 预发布版。除非您正在使用 ASP.NET 5.0(“vNext”),否则请确保您引入的是 2.1 版本。

添加 Microsoft.AspNet.Identity.EntityFramework Nuget 包
PM> Install-Package Microsoft.AspNet.Identity.EntityFramework -Version 2.1.0

我们的示例项目中已经有了Identity.Core库,因为它是在我们引入Identity.OwinNuget 包时包含进来的,这在本系列的第一部分中已经提到。

向自托管 Web Api 添加 Identity - 模型和存储

首先,我们将从上一篇文章结束的地方继续。回想一下,我们之前添加了一个 AuthModels.cs 文件,并编写了一个MyUser类,一个MyUserClaim类,一个MyPasswordHasher类,以及一个MyUserStore类。

我们现在可以把这些都删掉了,删除 AuthModels.cs 文件。

为了将(基本上)即用型的 Identity 框架添加到我们的项目中,让我们添加一个名为 IdentityModels.cs 的新代码文件,并添加以下代码:

添加 Identity 模型
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
 
// Add using statements:
using Microsoft.Owin;
using Microsoft.AspNet.Identity;
using Microsoft.AspNet.Identity.Owin;
using Microsoft.AspNet.Identity.EntityFramework;
 
namespace MinimalOwinWebApiSelfHost.Models
{
    public class ApplicationUser : IdentityUser
    {
        // A default Constructor:
        public ApplicationUser() { }
        
        public ApplicationUser(string email) : base(email)
        {
            // Use the email for both user name AND email:
            UserName = email;
        }
    }
 
 
    public class ApplicationUserManager 
        : UserManager<ApplicationUser>
    {
        public ApplicationUserManager(IUserStore<ApplicationUser> store) 
            : base(store) { }
 
 
        public static ApplicationUserManager Create(
            IdentityFactoryOptions<ApplicationUserManager> options,
            IOwinContext context)
        {
            return new ApplicationUserManager(
                new UserStore<ApplicationUser>(
                    context.Get<ApplicationDbContext>()));
        }
    }
}

现在,让我们仔细看看上面的代码中发生了什么。

首先,我们添加了一个ApplicationUser类,它派生自IdentityUser. IdentityUserIdentity.Core IUser接口的一个具体实现,由我们之前讨论过的Identity.EntityFramework库提供。目前我们在这个派生类中没有做太多事情,但稍后会对其进行补充。

接下来,我们有ApplicationUserManager类,它派生自UserManager。与IdentityUser不同,UserManager类是Identity.Core库的一部分。换句话说,任何使用 Identity 框架的应用程序都可以期望访问到UserManager,或者像我们上面那样的一个派生类。

注意,我们的ApplicationUserManager期望一个IUserStore类型的参数作为构造函数参数。这是什么?

我们将简要地讨论一下UserManagerUserStore以及它们各自的用途。

UserManager 和 UserStore - 有什么区别?

核心 Identity 框架定义了UserManager类,以实现根据 Identity 框架本身或在开发过程中设置的配置选项所建立的业务规则和配置来管理用户信息的各种功能。

这些框架的“规则”和配置项独立于用于保存和检索用户身份数据的特定持久化存储。换句话说,UserManager理解并使用 Identity 模型对象,而不关心应用程序底层的具体数据库。

UserManager提供的功能示例包括以下方法:

  • CreateAsync(TUser user, string Password)
  • AddToRoleAsync(TKey userId, string role)
  • AddClaimAsync(Tkey userId, Claim claim)

UserManager类是根据代表数据库特定实现模型抽象的各种接口来定义的。例如,基本的UserManager是通过一个必须指定的IUser实现的泛型类型参数来定义的。这个UserManager类还需要一个实现了IUserStore接口。

的构造函数参数。

一个IUserStore的具体实现代表了应用程序的底层持久化层。换句话说,一个UserStore的实现知道如何与特定的数据存储(如 MongoDb、SQL Server、RavenDb 等)“对话”。

在我们的代码中,我们使用的是 EntityFramework 提供的默认UserStore实现(它本身也是我们 SQL CE 或 SQL Server 数据库的一个抽象…)。

UserManager中定义一个Identity.Core基类,并使用一个接口IUserStore的想法是为了在管理 Identity 模型对象之间身份验证和授权的业务规则与应用程序特定后备存储的细节之间实现清晰的分离。

在我们应用程序内部,当我们使用 Identity 时,通常应该使用UserManager(或其派生版本)直接处理我们的身份模型对象。我们很少需要直接使用UserStore的实例,除非是为了将其作为构造函数参数注入到UserManager.

中。

UserManager、UserStore 和 Identity

usermanager-userstore

还要注意在我们的代码中,我们添加了一个静态方法,Create()。我们稍后也会讨论这个。但首先,我们将更新数据库上下文的代码。

更新 DbContext 以继承自 IdentityDbContext

当本系列上一篇文章结束时,我们更新了ApplicationDbContext代码文件,使其看起来像这样:

之前的 DbContext
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
 
// Add using:
using System.Data.Entity;
using System.Security.Claims;
 
namespace MinimalOwinWebApiSelfHost.Models
{
    public class ApplicationDbContext : DbContext
    {
        public ApplicationDbContext()
            : base("MyDatabase")
        {
 
        }
 
        static ApplicationDbContext()
        {
            Database.SetInitializer(new ApplicationDbInitializer());
        }
 
        public IDbSet<Company> Companies { get; set; }
        public IDbSet<MyUser> Users { get; set; }
        public IDbSet<MyUserClaim> Claims { get; set; }
    }
 
 
    public class ApplicationDbInitializer 
        : DropCreateDatabaseAlways<ApplicationDbContext>
    {
        protected async override void Seed(ApplicationDbContext context)
        {
            context.Companies.Add(new Company { Name = "Microsoft" });
            context.Companies.Add(new Company { Name = "Apple" });
            context.Companies.Add(new Company { Name = "Google" });
            context.SaveChanges();
 
            // Set up two initial users with different role claims:
            var john = new MyUser { Email = "john@example.com" };
            var jimi = new MyUser { Email = "jimi@Example.com" };
 
            john.Claims.Add(new MyUserClaim 
            { 
                ClaimType = ClaimTypes.Name, 
                UserId = john.Id, 
                ClaimValue = john.Email 
            });
            john.Claims.Add(new MyUserClaim 
            { 
                ClaimType = ClaimTypes.Role, 
                UserId = john.Id, 
                ClaimValue = "Admin" 
            });
 
            jimi.Claims.Add(new MyUserClaim 
            { 
                ClaimType = ClaimTypes.Name, 
                UserId = jimi.Id, 
                ClaimValue = jimi.Email 
            });
            jimi.Claims.Add(new MyUserClaim 
            { 
                ClaimType = ClaimTypes.Role, 
                UserId = john.Id, 
                ClaimValue = "User" 
            });
 
            var store = new MyUserStore(context);
            await store.AddUserAsync(john, "JohnsPassword");
            await store.AddUserAsync(jimi, "JimisPassword");
        }
    }
}

 

在同一个代码文件中,我们定义了我们的ApplicationDbContext和一个数据库初始化器。

现在我们有了Identity.CoreIdentity.EntityFramework库的访问权限,我们可以将此过程简化一些。

首先,Identity.EntityFramework为我们提供了IdentityDbContext,顾名思义,它是DbContext的一个针对 Identity 的特定实现。底层还有一些我们这里不会涉及的内容,但基本上,IdentityDbContext为我们提供了一个可以继承的基类,并且提供了一个DbContext实现,它可以与我们将要使用的基础Identity.EntityFramework模型一起工作。

首先,我们将修改上面我们ApplicationDbContext的代码,使其派生自IdentityDbContext。然后,我们将更新ApplicationDbInitializer以与我们的新DbContext和 Identity 模型协同工作。

修改 DbContext 以派生自 IdentityDbContext
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
 
// Add using:
using System.Data.Entity;
using System.Security.Claims;
 
// Add THESE to use Identity and Entity Framework:
using Microsoft.AspNet.Identity;
using Microsoft.AspNet.Identity.EntityFramework;
 
namespace MinimalOwinWebApiSelfHost.Models
{
    // Derive from IdentityDbContext:
    public class ApplicationDbContext : IdentityDbContext<ApplicationUser>
    {
        public ApplicationDbContext()
            : base("MyDatabase") { }
 
 
        static ApplicationDbContext()
        {
            Database.SetInitializer(
                new ApplicationDbInitializer());
        }
 
 
        // Add a static Create() method:
        public static ApplicationDbContext Create()
        {
            return new ApplicationDbContext();
        }
 
 
        // We still need a DbSet for our Companies 
        // (and any other domain objects):
        public IDbSet<Company> Companies { get; set; }
    }
 
 
    public class ApplicationDbInitializer 
        : DropCreateDatabaseAlways<ApplicationDbContext>
    {
        protected async override void Seed(ApplicationDbContext context)
        {
            context.Companies.Add(new Company { Name = "Microsoft" });
            context.Companies.Add(new Company { Name = "Apple" });
            context.Companies.Add(new Company { Name = "Google" });
            context.SaveChanges();
 
            // Set up two initial users with different role claims:
            var john = new ApplicationUser 
            { 
                Email = "john@example.com", 
                UserName = "john@example.com" 
            };
            var jimi = new ApplicationUser 
            { 
                Email = "jimi@Example.com", 
                UserName = "jimi@example.com" 
            };
 
            // Introducing...the UserManager:
            var manager = new UserManager<ApplicationUser>(
                new UserStore<ApplicationUser>(context));
 
            var result1 = await manager.CreateAsync(john, "JohnsPassword");
            var result2 = await manager.CreateAsync(jimi, "JimisPassword");
 
            // Add claims for user #1:
            await manager.AddClaimAsync(john.Id, 
                new Claim(ClaimTypes.Name, "john@example.com"));
 
            await manager.AddClaimAsync(john.Id, 
                new Claim(ClaimTypes.Role, "Admin"));
 
            // Add claims for User #2:
            await manager.AddClaimAsync(jimi.Id, 
                new Claim(ClaimTypes.Name, "jimi@example.com"));
 
            await manager.AddClaimAsync(jimi.Id, 
                new Claim(ClaimTypes.Role, "User"));
        }
    }
}

 

在上面的代码中,请注意我们现在通过派生自ApplicationDbContext实现了我们的IdentityDbContext<ApplicationUser>。这样做,我们指定了一个DbContext实现,它将立即可用,并且已经可以与我们的ApplicationUser类。

协同工作。

我们还更新了我们的ApplicationDbInitializer,利用我们ApplicationUserManager提供的便捷接口,轻松添加我们的测试用户和相关的用户声明。

请特别注意这里。还记得之前我们是如何创建了一个模拟的密码哈希器,以及所有关于实现一个合适的密码哈希加密机制的废话吗?在这里,Identity 已经为我们处理了所有这些。不需要模拟。正如我们所希望的,我们把加密的细节留给了懂行的人。记住,加密很难,即使对专家来说也是如此,而且它是一个已经解决的问题

现在,在继续之前,我们再绕个小弯。注意我们是如何像处理ApplicationUserManager一样,也在我们的Create()上定义了一个静态的ApplicationDbContext方法吗?我们现在来看看为什么这样做。

每个请求一个上下文和 CreatePerOwinContext

从我们数据模型的角度来看,我们希望确保我们总是在处理同一个数据库上下文实例,从而处理同一组对象。例如,如果在处理同一个 HTTP 请求时创建了两个独立的ApplicationDbContext实例(或者类似地,UserStoreUserManager),我们可能会对同一个用户的两个数据实例引入更改。

我们希望确保当我们检索一个用户对象时,在单个 HTTP 请求的上下文中,它将始终引用该用户数据的同一个实例。

Microsoft.AspNet.Identity.Owin库提供了一个扩展方法,通过这个方法我们可以确保每个OwinContext都会创建一个对象的单个实例。这个CreatePerOwinContext()方法允许我们传入一个泛型类型参数和一个返回所需对象实例的函数引用。我们在 Owin 的Configuration()方法中进行设置。然后,当为每个传入的 HTTP 请求创建OwinContext对象时,每个 OWIN 上下文都会创建一个所需对象的独立实例。

要更深入地了解这个概念,请参阅 ASP.NET Identity 中 UserManager 类的每请求生命周期管理

在我们的应用程序中,我们希望确保在请求处理期间,我们只使用同一个ApplicationDbContext实例以及ApplicationUserManager实例。我们通过在Create()ApplicationDbContextApplicationUserManager上分别添加这些静态CreatePerOwinContext()方法,然后在 Owin 的Configuration()方法的新参数。

期间将它们的引用传递给ConfigureAuth()来实现这一点。

我们希望将以下代码添加到我们启动

中的
private void ConfigureAuth(IAppBuilder app)
{
    // Create per OWIN Context:
    app.CreatePerOwinContext<ApplicationDbContext>(ApplicationDbContext.Create);
    app.CreatePerOwinContext<ApplicationUserManager>(ApplicationUserManager.Create);
 
    var OAuthOptions = new OAuthAuthorizationServerOptions
    {
        TokenEndpointPath = new PathString("/Token"),
        Provider = new ApplicationOAuthServerProvider(),
        AccessTokenExpireTimeSpan = TimeSpan.FromDays(14),
 
        // Only do this for demo!!
        AllowInsecureHttp = true
    };
    app.UseOAuthAuthorizationServer(OAuthOptions);
    app.UseOAuthBearerAuthentication(new OAuthBearerAuthenticationOptions());
}

方法中:

为每个 Owin 上下文创建 ApplicationDbContext 和 ApplicationUserManager

回想一下我们之前对 OWIN 和 Katana 的探索,当我们像这样传递函数引用时,函数本身并不会在代码的这个点执行。相反,一个指向该函数的引用会被添加到 Owin 环境字典中,然后在每次为响应传入的 HTTP 请求而创建新的 Owin 上下文时被*调用*。Create()如果我们仔细看一下我们在ApplicationUserManager

上定义的静态
public static ApplicationUserManager Create(
    IdentityFactoryOptions<ApplicationUserManager> options,
    IOwinContext context)
{
    return new ApplicationUserManager(
        new UserStore<ApplicationUser>(
            context.Get<ApplicationDbContext>()));
}

 

方法,就能理解其中的一些逻辑。

来自 ApplicationUserManager 的 Create() 方法UserStore注意这里,我们是如何初始化一个ApplicationDbContext的实例,并通过从OwinContext实例中检索它来传入一个对DbContext的引用?这样,我们确保即使在这里,我们也在使用为每个请求专门创建的单个

为 Identity 更新 OAuth 服务器提供程序

对象实例。

现在我们已经实现了一个非常基础的 Identity 模型,我们可以修改我们的ApplicationOauthServerProvider了。在对GrantResourceOwnerCredentials()的调用中,我们可以利用我们的ApplicationUserManager和 Identity 模型来简化授权过程。

AddMicrosoft.AspNet.Identity.Owinusing语句添加到文件顶部的

中,然后按如下方式替换现有代码:

更新 GrantResourceOwnerCredentials 的代码以支持 Identity

using System.Threading.Tasks;
 
// Add Usings:
using Microsoft.Owin.Security;
using Microsoft.Owin.Security.OAuth;
using System.Security.Claims;
using MinimalOwinWebApiSelfHost.Models;
 
// Add to use Identity:
using Microsoft.AspNet.Identity.Owin;
 
namespace MinimalOwinWebApiSelfHost.OAuthServerProvider
{
    public class ApplicationOAuthServerProvider 
        : OAuthAuthorizationServerProvider
    {
        public override async Task ValidateClientAuthentication(
            OAuthValidateClientAuthenticationContext context)
        {
            // This call is required...
            // but we're not using client authentication, so validate and move on...
            await Task.FromResult(context.Validated());
        }
 
 
        public override async Task GrantResourceOwnerCredentials(
            OAuthGrantResourceOwnerCredentialsContext context)
        {
            // ** Use extension method to get a reference 
            // to the user manager from the Owin Context:
            var manager = context.OwinContext.GetUserManager<ApplicationUserManager>();
 
            // UserManager allows us to retrieve use with name/password combo:
            var user = await manager.FindAsync(context.UserName, context.Password);
            if (user == null)
            {
                context.SetError(
                    "invalid_grant", "The user name or password is incorrect.");
                context.Rejected();
                return;
            }
 
            // Add claims associated with this user to the ClaimsIdentity object:
            var identity = new ClaimsIdentity(context.Options.AuthenticationType);
            foreach (var userClaim in user.Claims)
            {
                identity.AddClaim(new Claim(userClaim.ClaimType, userClaim.ClaimValue));
            }
 
            context.Validated(identity);
        }
    }
}

再次注意,在上面的代码中,我们是如何确保从上下文对象中获取ApplicationUserManager实例的引用的?上下文对象很方便地提供了一个GetUserManager()扩展方法。

还要注意ApplicationUserManager如何使得通过用户名和密码检索用户对象变得非常方便。如果密码不匹配,将返回 null,并返回无效授权错误。

更新 CompaniesController 以使用每个请求的 DbContext

我们不一定非要这样做,但我们可以。我们可以更新我们的CompaniesController以利用 Identity 在这里提供的“每个请求一个上下文”的策略。

请确保在Microsoft.AspNet.Identity.Owinusing文件顶部添加CompaniesController语句。

更新 CompaniesController 以使用每个请求的上下文
public class CompaniesController : ApiController
{
    // Ditch THIS:
    //ApplicationDbContext dbContext = new ApplicationDbContext();
 
    // Replace with something like THIS:
    ApplicationDbContext dbContext
    {
        get
        {
            return Request.GetOwinContext().Get<ApplicationDbContext>();
        }
    }
 
    ... All the rest of the controller code....
 
}

 

至此,我们可以使用上一篇文章中的同一个 Api 客户端应用程序,来测试我们这个新的、改进过的应用程序了。

在 Identity 就位的情况下运行应用程序

如果我们启动我们的 Web Api,一切应该看起来都很好。

运行 Web Api 应用程序 - 一切正常

run-web-api-application

接下来,如果我们运行 API 客户端应用程序,确保我们使用有效的用户凭据(它们应该与Seed()方法中 Admin 用户的凭据匹配),一切都应该像以前一样工作。

运行 API 客户端应用程序

run-api-client-application-with-identity

同样,如果我们在客户端应用程序中修改代码并传递一个无效的密码,我们会得到一个无效授权错误。

客户端提交无效密码返回无效授权错误

run-api-client-application-with-identity-invalid-grant

并且,如果我们将客户端凭据更改为普通 User 角色的用户(该用户没有权限访问我们的CompaniesController),我们会收到一个 401/Unauthorized 错误。

权限不足的 API 客户端

run-api-client-application-with-identity-unauthorized

总结

在过去四篇文章的过程中,我们希望能够更好地理解基于 OWIN 的 Web Api 应用程序中各个部分的组合方式。随着应用程序的增长,各种框架之间的界限变得模糊,我的目标是以一种能够清晰说明什么在何处发生以及为何发生的方式来分解这些内容。

我们示例应用程序的结构是粗糙的,如果你要在此基础上更进一步,你肯定会想要改变一些东西,做一些重构,并实现更多的异常和错误处理。

同样,我们基于控制台的 API 客户端应用程序仅足以用于演示目的来测试我们的 Web Api。

从这里开始,我们可以在开发基于声明的授权模型方面走得更远。目前,我们的小示例应用程序确实使用了声明,但依赖于[Authorize]属性的内置能力来执行基于角色的授权检查。我们将在即将发布的文章中更广泛地探讨声明。

一如既往,非常欢迎反馈,特别是如果你发现我做了什么蠢事,或者在代码中发现明显的错误。欢迎对示例仓库提交 Pull Request,只要它们是关于改进或修复 bug 的。出于显而易见的原因,我希望保持代码示例与文章同步。

其他资源和感兴趣的项目

 
 
 

© . All rights reserved.