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

ASP.NET Web API 和 Identity 2.0 - 自定义 Identity 模型和实现基于角色的授权

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.97/5 (13投票s)

2014 年 11 月 17 日

CPOL

21分钟阅读

viewsIcon

86493

在上一篇文章中,我们大致了解了在 Web API 应用程序中使用 Identity 2.0。我们基本上探索并调整了默认的 Visual Studio Web API 项目模板,了解了各个部分的所在地,并对它的工作原理有了基本的认识。

hermself 240在上一篇文章中,我们大致了解了在 Web API 应用程序中使用 Identity 2.0。我们基本上探索并调整了默认的 Visual Studio Web API 项目模板,了解了各个部分的所在地,并对它的工作原理有了基本的认识。

然而,VS 项目模板非常基础,使用不记名令牌作为主要的身份验证机制,并且不提供任何开箱即用的高级授权方案支持。简而言之,VS 项目模板为我们提供了基本的基于令牌的身份验证,仅此而已。

此外,用户模型很简单,没有定义角色模型。这可能是故意的,因为在某些情况下,您可能将 Web API 项目用作简单的身份验证服务。但在其他情况下,您可能希望自定义用户模型和/或添加基于角色的身份验证。

Imagebymadamepsychosis  | 保留部分权利

我们之前研究过在 ASP.NET MVC 应用程序中自定义用户和角色模型,以及如何修改库存 MVC 项目以适应这些自定义。在这篇文章中,我们将对 Web API 项目做同样的事情。

您可以在 Github 上找到示例 Web API 项目的源代码

接下来要做的,仅仅是为了能够自定义您的领域模型,可能看起来工作量很大。从某种程度上说,确实如此。但我们需要考虑的是,我们是在一个预制模板项目的背景下进行此操作的。我在这里的目标是专门从 VS 提供的开箱即用的功能开始,并保留所有提供的功能,同时扩展模型。 

此外,这也是了解基于 Identity 的 Web API 项目内部结构的好方法。 

在决定身份验证策略之前考虑您的用例

在 Web API 项目中,有多种身份验证和授权策略可供选择。使用不记名令牌和基于角色的身份验证相对简单实现,但它不是授权解决方案最先进的架构。在决定采用传统的基于角色的授权之前,您可能需要检查您的身份验证和授权需求的范围,并确定是否需要更简单或更高级的解决方案。 

ASP.NET Web API 可以充分利用基于声明的授权,对于更复杂的系统来说,这可能是一个更好的选择。同样,如前所述,如果您的 Web API 的主要目的是充当身份验证服务,您可能需要使用更健壮的令牌系统(例如,共享私钥而不是默认使用的不记名令牌),并在此级别取消授权。

在需要适度的不同授权/访问级别,并且 Web API 可能是大型 MVC 或其他 ASP.NET 站点的一部分或与之相关联(其中角色用于管理授权)的项目中,基于角色的授权是一个很好的选择。考虑一个标准的 MVC 项目,其中少数角色足以管理授权,并且该项目既提供网页也提供 API 访问。

应用我们之前学到的知识

幸运的是,我们接下来要做的很多事情,我们之前都见过,我们甚至可以借用我们已经写过的一些代码片段。如果您刚开始熟悉 Identity 2.0,请随意回顾我们之前在 ASP.NET MVC 项目中对用户和角色进行类似自定义的文章。

现在我们对要处理的内容有了一些了解,让我们看看如何在 Web API 上下文中应用它。

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

首先,在 Visual Studio 中,创建一个新的 ASP.NET Web API 项目。项目创建后,更新解决方案中的 Nuget 包,可以使用“解决方案资源管理器”上下文菜单中的“管理解决方案包”,或使用“程序包管理器控制台”中的Update-Package命令。

这将更新所有 Nuget 包,并专门将 Web API 更新到版本 2.2。

添加角色模型并自定义 ApplicationUser

首先,让我们再次看看“Models => IdentityModes.cs”文件。目前,里面没有太多内容。

Web API 中的默认 IdentityModels.cs 文件
using System.Security.Claims;
using System.Threading.Tasks;
using Microsoft.AspNet.Identity;
using Microsoft.AspNet.Identity.EntityFramework;
using Microsoft.AspNet.Identity.Owin;
 
namespace AspNetIdentity2WebApiCustomize.Models
{
    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();
        }
    }
}

正如我们在 ASP.NET MVC 示例中所做的那样,我们首先修改并添加到“Models => IdentityModels.cs”中定义的现有模型。事实上,由于我们之前做了很多工作,我们将从ASP.NET 可扩展模板项目中“窃取”IdentityModels.cs 代码。这里要小心。我们可以通过将类粘贴到当前代码文件中定义的现有命名空间中,并暂时保持 using 语句不变,来省去一些麻烦。

更新的 IdentityModels.cs 代码
// You will not likely need to customize there, but it is necessary/easier to create our own 
// project-specific implementations, so here they are:
public class ApplicationUserLogin : IdentityUserLogin<string> { }
public class ApplicationUserClaim : IdentityUserClaim<string> { }
public class ApplicationUserRole : IdentityUserRole<string> { }
 
// Must be expressed in terms of our custom Role and other types:
public class ApplicationUser 
    : IdentityUser<string, ApplicationUserLogin, 
    ApplicationUserRole, ApplicationUserClaim>
{
    public ApplicationUser()
    {
        this.Id = Guid.NewGuid().ToString();
 
        // Add any custom User properties/code here
    }
 
 
    public async Task<ClaimsIdentity>
        GenerateUserIdentityAsync(ApplicationUserManager manager)
    {
        var userIdentity = await manager
            .CreateIdentityAsync(this, DefaultAuthenticationTypes.ApplicationCookie);
        return userIdentity;
    }
}
 
 
// Must be expressed in terms of our custom UserRole:
public class ApplicationRole : IdentityRole<string, ApplicationUserRole>
{
    public ApplicationRole() 
    {
        this.Id = Guid.NewGuid().ToString();
    }
 
    public ApplicationRole(string name)
        : this()
    {
        this.Name = name;
    }
 
    // Add any custom Role properties/code here
}
 
 
// Must be expressed in terms of our custom types:
public class ApplicationDbContext 
    : IdentityDbContext<ApplicationUser, ApplicationRole, 
    string, ApplicationUserLogin, ApplicationUserRole, ApplicationUserClaim>
{
    public ApplicationDbContext()
        : base("DefaultConnection")
    {
    }
 
    static ApplicationDbContext()
    {
        Database.SetInitializer<ApplicationDbContext>(new ApplicationDbInitializer());
    }
 
    public static ApplicationDbContext Create()
    {
        return new ApplicationDbContext();
    }
    // Add additional items here as needed
}
 
// Most likely won't need to customize these either, but they were needed because we implemented
// custom versions of all the other types:
public class ApplicationUserStore 
    :UserStore<ApplicationUser, ApplicationRole, string,
        ApplicationUserLogin, ApplicationUserRole, 
        ApplicationUserClaim>, IUserStore<ApplicationUser, string>, 
    IDisposable
{
    public ApplicationUserStore()
        : this(new IdentityDbContext())
    {
        base.DisposeContext = true;
    }
 
    public ApplicationUserStore(DbContext context)
        : base(context)
    {
    }
}
 
 
public class ApplicationRoleStore
: RoleStore<ApplicationRole, string, ApplicationUserRole>,
IQueryableRoleStore<ApplicationRole, string>,
IRoleStore<ApplicationRole, string>, IDisposable
{
    public ApplicationRoleStore()
        : base(new IdentityDbContext())
    {
        base.DisposeContext = true;
    }
 
    public ApplicationRoleStore(DbContext context)
        : base(context)
    {
    }
}

现在,我们需要在代码文件顶部添加一些额外的 using 语句,以引入新代码所需的引用。将以下内容添加到文件顶部的 using 语句中。

添加到 IdentityModels.cs 的附加 using 语句
using System.Data.Entity;
using System.ComponentModel.DataAnnotations;
using System;

我们刚才粘贴的代码有两个直接问题。第一个可能很明显,因为 VS 编译器可能会告诉您没有定义 `DBInitializer` 类。此外,如果您构建项目,VS 错误列表中还会出现其他一些问题。我们稍后会解决这些问题。

另一个则不那么明显。我们在此处粘贴的代码来自一个 MVC 项目。在大多数情况下,这没问题。但是,我们的 `ApplicationUser` 类定义了一个 `GenerateUserIdentityAsync` 方法。我们从 MVC 项目中“窃取”的代码具有此方法,但将其定义为 `ApplicationUserManager` 类型的单个参数。回想一下我们覆盖的代码,它定义了具有两个构造函数参数的 `ApplicationUser`。我们新复制的方法中缺少的参数是 `string` 类型,代表 `authenticationType`。

这很重要,因为当我们想检索用户的 ClaimsIdentity(它表示特定用户在我们的系统中拥有的各种声明)时,会调用 GenerateUserIdentityAsync

还没搞明白?我们暂时不需要担心 ClaimsIdentity 的细节。我们需要做的是更新 ApplicationUser 上定义的 GenerateUserIdentityAsync 方法,以接受一个表示 authenticationTypestring 参数。

更新 Web API 的 ApplicationUser

为了使我们的 ApplicationUser 类准备好在 Web API 上下文中使用,我们可以将 GenerateUserIdentityAsync 方法的代码替换为以下内容:

使用身份验证类型参数更新 GenerateUserIdentityAsync
// ** Add authenticationtype as method parameter:
public async Task<ClaimsIdentity>
    GenerateUserIdentityAsync(ApplicationUserManager 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;
}

添加 DBInitializer 和其他 Identity 配置项

我们之前提到过,编译器也正在提示您,我们从 Identity Extensible Template 项目中“窃取”的代码正在尝试使用一个在我们的 Web API 项目中不存在(尚未存在)的 `DBInitializer` 类。此外,您可能已经注意到(如果您在添加额外的 Identity 模型后构建了项目),我们的新 `ApplicationUser` 类似乎存在一些问题。

我们将通过再次从 Identity Extensible Template 项目中“窃取”选定的代码片段来解决大多数这些问题。

如果我们查看 Web API 项目中的“App_Start => IdentityConfig.cs”文件,我们会发现,与原始的“IdentityModels.cs”文件一样,里面没有太多内容。

Web API 项目中的默认 Identity.config 文件
using System.Threading.Tasks;
using Microsoft.AspNet.Identity;
using Microsoft.AspNet.Identity.EntityFramework;
using Microsoft.AspNet.Identity.Owin;
using Microsoft.Owin;
using AspNetIdentity2WebApiCustomize.Models;
 
namespace AspNetIdentity2WebApiCustomize
{
    // Configure the application user manager used in this application. UserManager 
    // is defined in ASP.NET Identity and is used by the application.
    public class ApplicationUserManager : UserManager<ApplicationUser>
    {
        public ApplicationUserManager(IUserStore<ApplicationUser> store)
            : base(store)
        {
        }
 
        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 项目中使用角色,我们将需要一个 `ApplicationRoleManager`,正如之前提到的,我们将从 Extensible Template 项目中添加 `ApplicationDbInitializer`。

首先,我们需要在 IdentityConfig.cs 文件的顶部添加以下 using 语句:

IdentityConfig.cs 文件的 Using 语句
using AspNetIdentity2WebApiCustomize.Models;
using Microsoft.AspNet.Identity;
using Microsoft.AspNet.Identity.EntityFramework;
using Microsoft.AspNet.Identity.Owin;
using Microsoft.Owin;
using System.Data.Entity;
using System.Web;

现在,将 Extensible Template 项目中的 `ApplicationRoleManager` 和 `ApplicationDbInitializer` 类添加到我们的 `IdentityConfig.cs` 文件中。

将 ApplicationRoleManager 和 ApplicationDbInitializer 添加到 IdentityConfig.cs
using AspNetIdentity2WebApiCustomize.Models;
using Microsoft.AspNet.Identity;
using Microsoft.AspNet.Identity.EntityFramework;
using Microsoft.AspNet.Identity.Owin;
using Microsoft.Owin;
using System.Data.Entity;
using System.Web;
 
 
namespace AspNetIdentity2WebApiCustomize
{
    public class ApplicationUserManager : UserManager<ApplicationUser>
    {
        public ApplicationUserManager(IUserStore<ApplicationUser> store)
            : base(store)
        {
        }
 
        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;
        }
    }
 
 
    public class ApplicationRoleManager : RoleManager<ApplicationRole>
    {
        public ApplicationRoleManager(IRoleStore<ApplicationRole, string> roleStore)
            : base(roleStore)
        {
        }
 
        public static ApplicationRoleManager Create(
            IdentityFactoryOptions<ApplicationRoleManager> options, 
            IOwinContext context)
        {
            return new ApplicationRoleManager(
                new ApplicationRoleStore(context.Get<ApplicationDbContext>()));
        }
    }
 
 
    public class ApplicationDbInitializer 
        : DropCreateDatabaseAlways<ApplicationDbContext>
    {
        protected override void Seed(ApplicationDbContext context)
        {
            InitializeIdentityForEF(context);
            base.Seed(context);
        }
 
        //Create User=Admin@Admin.com with password=Admin@123456 in the Admin role        
        public static void InitializeIdentityForEF(ApplicationDbContext db)
        {
            var userManager = HttpContext.Current
                .GetOwinContext().GetUserManager<ApplicationUserManager>();
 
            var roleManager = HttpContext.Current
                .GetOwinContext().Get<ApplicationRoleManager>();
 
            const string name = "admin@example.com";
            const string password = "Admin@123456";
            const string roleName = "Admin";
 
            //Create Role Admin if it does not exist
            var role = roleManager.FindByName(roleName);
            if (role == null)
            {
                role = new ApplicationRole(roleName);
                var roleresult = roleManager.Create(role);
            }
 
            var user = userManager.FindByName(name);
            if (user == null)
            {
                user = new ApplicationUser { UserName = name, Email = name };
                var result = userManager.Create(user, password);
                result = userManager.SetLockoutEnabled(user.Id, false);
            }
 
            // Add user admin to Role Admin if not already added
            var rolesForUser = userManager.GetRoles(user.Id);
            if (!rolesForUser.Contains(role.Name))
            {
                var result = userManager.AddToRole(user.Id, role.Name);
            }
        }
    }
}

现在,我们需要对 `ApplicationUserManager` 进行一些更改。由于我们已经添加了可自定义的模型,包括 `UserStore` 和 `RoleStore` 的修改版本,因此我们需要调整 `ApplicationUserManager` 以使其协同工作。我们用与 Web API 项目预期默认实现不同的类型参数来表达了我们的模型。具体来说,我们采用了 `IUserStore` 的自定义实现。我们没有使用 `Microsoft.AspNet.Identity.EntityFramework` 中定义的具体 `UserStore`,而是实现了我们自己的 `ApplicationUserStore`,它以特定的类型参数表示。

现在我们需要调整我们的 `ApplicationUserManager` 以便与我们的 `ApplicationUserStore` 协同工作。

IdentityConfig.csApplicationUserManager 的代码更改为以下内容:

修改后的 ApplicationUserManager
public class ApplicationUserManager 
    : UserManager<ApplicationUser, string>
{
    public ApplicationUserManager(IUserStore<ApplicationUser, string> store)
        : base(store)
    {
    }
 
    public static ApplicationUserManager Create(
        IdentityFactoryOptions<ApplicationUserManager> options, 
        IOwinContext context)
    {
        var manager = new ApplicationUserManager(
            new UserStore<ApplicationUser, ApplicationRole, string, 
                ApplicationUserLogin, ApplicationUserRole, 
                ApplicationUserClaim>(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 实现中我们无法直接使用的角色)奠定了最基本的基础。

不过,我们确实需要解决另一个问题。`AccountController` 似乎仍然依赖于 `Microsoft.AspNet.Identity.EntityFramework.IdentityUser`,我们需要它使用我们的新实现 `ApplicationUser`。

修改 AccountController 以使用 ApplicationUser

我们可以轻松纠正这个最后一个遗留问题。打开 `AccountController` 类,找到 `GetManageInfo()` 方法。我们可以看到一个局部变量 `user` 被声明,明确类型为 `IdentityUser`。

在此之下,我们可以在 `foreach()` 循环中看到,我们明确声明了一个迭代器变量 `linkedAccount`,类型为 `IdentityUserLogin`。

Web API AccountController GetManageInfo() 方法中的现有代码
[Route("ManageInfo")]
public async Task<ManageInfoViewModel> GetManageInfo(
    string returnUrl, bool generateState = false)
{
    IdentityUser user = 
        await UserManager.FindByIdAsync(User.Identity.GetUserId());
    if (user == null)
    {
        return null;
    }
 
    List<UserLoginInfoViewModel> logins = new List<UserLoginInfoViewModel>();
    foreach (IdentityUserLogin linkedAccount in user.Logins)
    {
        logins.Add(new UserLoginInfoViewModel
        {
            LoginProvider = linkedAccount.LoginProvider,
            ProviderKey = linkedAccount.ProviderKey
        });
    }
 
    if (user.PasswordHash != null)
    {
        logins.Add(new UserLoginInfoViewModel
        {
            LoginProvider = LocalLoginProvider,
            ProviderKey = user.UserName,
        });
    }
 
    return new ManageInfoViewModel
    {
        LocalLoginProvider = LocalLoginProvider,
        Email = user.UserName,
        Logins = logins,
        ExternalLoginProviders = GetExternalLogins(returnUrl, generateState)
    };
}

在这两种情况下,我们都实现了这些类型的自己的版本。在这里,我们可以选择在每种情况下更改声明以使用 `var` 关键字,这使我们摆脱了变量的类型约束(但有些人会争辩说,这会使我们的代码有点模糊),或者我们可以在每种情况下更改显式类型声明以使用我们自己的实现。

现在,让我们将显式类型声明更改为使用我们自己的实现。

GetManageInfo() 方法的修改代码
[Route("ManageInfo")]
public async Task<ManageInfoViewModel> GetManageInfo(
    string returnUrl, bool generateState = false)
{
    ApplicationUser user = 
        await UserManager.FindByIdAsync(User.Identity.GetUserId());
    if (user == null)
    {
        return null;
    }
 
    List<UserLoginInfoViewModel> logins = new List<UserLoginInfoViewModel>();
    foreach (ApplicationUserLogin linkedAccount in user.Logins)
    {
        logins.Add(new UserLoginInfoViewModel
        {
            LoginProvider = linkedAccount.LoginProvider,
            ProviderKey = linkedAccount.ProviderKey
        });
    }
 
    if (user.PasswordHash != null)
    {
        logins.Add(new UserLoginInfoViewModel
        {
            LoginProvider = LocalLoginProvider,
            ProviderKey = user.UserName,
        });
    }
 
    return new ManageInfoViewModel
    {
        LocalLoginProvider = LocalLoginProvider,
        Email = user.UserName,
        Logins = logins,
        ExternalLoginProviders = GetExternalLogins(returnUrl, generateState)
    };
}

上面,我们只是将本地用户变量的声明类型从 `IdentityUser` 更改为 `ApplicationUser`,并将迭代器变量 `linkedAccount` 从 `IdentityUserLogin` 更改为 `ApplicationUserLogin`。

在 Startup.Auth 中添加 ApplicationRoleManager 的初始化

回想我们对 ASP.NET Web API 和 Identity 的高级探索,Identity 2.0 的初始化和配置发生在 App_Start => Startup.Auth 中定义的 `Startup` 类中。

正如我们所看到的,原始的 VS Web API 模板并没有真正提供基于角色的任何功能,因此,在启动时也没有为我们最近添加的 ApplicationRoleManager 提供任何配置或初始化。我们需要在 Startup.Auth 文件中添加一行初始化代码。

在 Statup.Auth 中添加 ApplicationRoleManager 的初始化
public void ConfigureAuth(IAppBuilder app)
{
    app.CreatePerOwinContext(ApplicationDbContext.Create);
    app.CreatePerOwinContext<ApplicationUserManager>(ApplicationUserManager.Create);
    app.CreatePerOwinContext<ApplicationRoleManager>(ApplicationRoleManager.Create);
 
    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);
 
    // ... Code for third-part logins omitted for brevity ...
}

我们添加了一行代码,为每个传入请求初始化一个 ApplicationRoleManager 实例。

关于 ApplicationDbInitializer

通过我们目前引入的更改,我们应该能够对我们改进后的 Web API 项目进行测试,看看最基本的功能是否正常工作。

不过,在此之前,我们需要认识到,由于引入了自定义 `ApplicationDbInitializer`,我们已经从根本上改变了 EF/Code-First 数据库的生成方式。

回想一下我们对使用可扩展模型自定义 MVC 项目的探索,`ApplicationDbInitializer` 允许我们指定如何以及何时生成应用程序背后的数据库,并提供一些初始数据来使用。

当我们转向基于角色的授权和更严格的 API 安全模型时,这变得很重要。

我们目前配置 `ApplicationDbInitializer` 的方式是,它派生自 `DbDropCreateDatabaseAlways`,这意味着每次我们运行应用程序时,后端存储都会被销毁并从头开始重新创建。我们还将其设置为创建一个默认用户,并将该用户分配给 Admin 角色。通过这种方式,我们在启动应用程序时拥有一个具有 Admin 级别访问权限的用户。

默认的 VS Web API 项目没有开箱即用地利用这一点。如果我们查看 `AccountController` 的类声明,我们会看到该类本身用一个简单的 `[Authorize]` 属性进行修饰。这实际上将对该类上所有 Action 方法的访问限制为授权用户(除了那些专门用 `[AllowAnonymous]` 属性修饰的方法)。

换句话说,目前,任何已注册并成功登录并提交有效不记名令牌的用户都可以访问 `AccountController` 上的任何 Action 方法。

我们稍后将更详细地研究如何实现基于角色的身份验证。首先,我们将使用一些自定义属性来扩展我们的 ApplicationUser 和 ApplicationRole 类。

向 ApplicationUser 和 ApplicationRole 添加自定义属性

正如我们在检查 MVC 项目中的用户和角色自定义时所看到的,我们将向 ApplicationUser 和 ApplicationRole 模型添加一些简单属性。修改各自代码如下:

为 ApplicationUser 和 ApplicationRole 添加自定义属性
// Must be expressed in terms of our custom Role and other types:
public class ApplicationUser
    : IdentityUser<string, ApplicationUserLogin,
    ApplicationUserRole, ApplicationUserClaim> 
{
    public ApplicationUser()
    {
        this.Id = Guid.NewGuid().ToString();
    }
 
 
    public async Task<ClaimsIdentity>GenerateUserIdentityAsync(
        ApplicationUserManager 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;
    }
 
    // Add Custom Properties:
    public string Address { get; set; }
    public string City { get; set; }
    public string PostalCode { get; set; }
}
 
 
// Must be expressed in terms of our custom UserRole:
public class ApplicationRole : IdentityRole<string, ApplicationUserRole> 
{
    public ApplicationRole() 
    {
      this.Id = Guid.NewGuid().ToString();
    }
 
    public ApplicationRole(string name) : this() 
    {
      this.Name = name;
    }
 
    // Add Custom Property:
    public string Description { get; set; }
}

这里,我们为 `ApplicationUser` 添加了 `Address` 和相关属性,为 `ApplicationRole` 添加了一个简单的 `Description` 属性。

现在,让我们更新 `ApplicationDbInitializer` 以设置这些新属性的一些示例值。将 `InitializeIdentityForEF()` 方法的代码更新如下:

在 ApplicationDbInitializer 中设置自定义属性的初始值
public static void InitializeIdentityForEF(ApplicationDbContext db)
{
    var userManager = HttpContext.Current
        .GetOwinContext().GetUserManager<ApplicationUserManager>();
 
    var roleManager = HttpContext.Current
        .GetOwinContext().Get<ApplicationRoleManager>();
 
    const string name = "admin@example.com";
    const string password = "Admin@123456";
 
    // Some initial values for custom properties:
    const string address = "1234 Sesame Street";
    const string city = "Portland";
    const string state = "OR";
    const string postalCode = "97209";
 
    const string roleName = "Admin";
    const string roleDescription = "All access pass";
 
    //Create Role Admin if it does not exist
    var role = roleManager.FindByName(roleName);
    if (role == null)
    {
        role = new ApplicationRole(roleName);
 
        // Set the new custom property:
        role.Description = roleDescription;
        var roleresult = roleManager.Create(role);
    }
 
    var user = userManager.FindByName(name);
    if (user == null)
    {
        user = new ApplicationUser { UserName = name, Email = name };
 
        // Set the new custom properties:
        user.Address = address;
        user.City = city;
        user.State = state;
        user.PostalCode = postalCode;
 
        var result = userManager.Create(user, password);
        result = userManager.SetLockoutEnabled(user.Id, false);
    }
 
    // Add user admin to Role Admin if not already added
    var rolesForUser = userManager.GetRoles(user.Id);
    if (!rolesForUser.Contains(role.Name))
    {
        var result = userManager.AddToRole(user.Id, role.Name);
    }
}

有了这些,我们应该准备好看看一切是否至少能正常工作了……

创建一个简单的 Web API 客户端应用程序

为了检查到目前为止所有功能是否正常运行,我们将创建一个简单的控制台应用程序作为 API 客户端。

在 Visual Studio 中,创建一个新的控制台应用程序,然后使用“管理解决方案的 Nuget 包”添加Microsoft Asp.NET Web API 2.2 客户端库,或者使用“包管理器控制台”并执行:

通过 Nuget 包管理器控制台添加 Web API 2.2
PM> Install-Package Microsoft.AspNet.WebApi.Client

 

现在,我们已经在项目中拥有了所需的 Web API 客户端库,打开 Program.cs 文件。

确保文件顶部存在以下 using 语句。请注意,我们添加了对 `System.Net.Http` 和 `Newtonsoft.Json` 以及 `System.Threading` 的引用。

控制台 API 客户端应用程序所需的 Using 语句
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Net.Http;
using Newtonsoft.Json;

接下来,让我们添加一些非常基本的客户端代码,用于从我们的 Web API 的令牌端点检索令牌。将以下代码添加到 Program 类中:

添加客户端代码以从 Web API 令牌端点检索响应
// You will need to substitute your own host Url here:
static string host = "https://:63074/";
 
static void Main(string[] args)
{
    Console.WriteLine("Attempting to Log in with default admin user");
 
    // Get hold of a Dictionary representing the JSON in the response Body:
    var responseDictionary = 
        GetResponseAsDictionary("admin@example.com", "Admin@123456");
    foreach(var kvp in responseDictionary)
    {
        Console.WriteLine("{0}: {1}", kvp.Key, kvp.Value);
    }
    Console.Read();
}
 
 
static Dictionary<string, string> GetResponseAsDictionary(
    string userName, string password)
{
    HttpClient client = new HttpClient();
    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);
 
    // Attempt to get a token from the token endpoint of the Web Api host:
    HttpResponseMessage response =
        client.PostAsync(host + "Token", content).Result;
    var result = response.Content.ReadAsStringAsync().Result;
    // De-Serialize into a dictionary and return:
    Dictionary<string, string> tokenDictionary =
        JsonConvert.DeserializeObject<Dictionary<string, string>>(result);
    return tokenDictionary;
}

请注意,此时,我们已经向 Main() 方法添加了代码,该方法调用了一个单一的、有点人为的方法 GetResponseAsDictionary()。我们在这里基本上所做的就是向我们的 Web API 的 Token 端点提交一个 HTTP POST,然后将 JSON 响应体反序列化为一个 Dictionary<string, string>。

一旦我们有了 Dictionary,我们就会遍历每个键/值对,并将内容写入控制台。

如果一切都按预期工作,我们的控制台输出应该类似于以下内容:

令牌端点响应的控制台输出
Attempting to Log in with default admin user
access_token: AuzOQkgG3BYubP1rlljcPhAzW7R7gA4Vew8dHy_MScMn2-Rs3R6dNlwCU_SuFwKveq
uf5rflB7PCfamlcT_-KJ4q3lfx7kiFNpSF9SdMLwKP_mCSOXGbrxrK3jXfH7bum3sZdl7w8k5irLa27i
Bvp_RqtXgkSmgpcNWitCU8RBz7aOaHr8r-FCklg4wUkLNE26qlR6Sl42DAAiBZNLpUZUt-M7vaOs8TZB
W4YehAzrqFAuTX3peMJBQB8K8_XxaTkRnEhSEMz9DnUnqzQjjVr5rnSdFSGxQmrQA8dBBwq4RaUfwbCU
7au787CMn7EGiDO9KRcGHAsGHOJqb8P8Z7A-ssV7tfEqJayrNH-F_Z2p5kiasDODQrG53CZNUE0vuDT6
Fp4_xOavE6wkYcHTfXWZJWFEMokE4NB9mtAl3lReYSZQyzKkcHWFNQCMAj3LoNGSdnEVVM_jzZtRSfWj
IG2OmhyR1wZNRCHY_6NwEMOIHGLpA_L-kFFAJPgwQWi-WljeV-X2KiMQIeYlGGdskaNw
token_type: bearer
expires_in: 1209599
userName: admin@example.com
.issued: Sun, 26 Oct 2014 13:21:03 GMT
.expires: Sun, 09 Nov 2014 13:21:03 GMT

我们之前在概述文章中见过这种情况。上面反序列化的 JSON 代表了我们向 Web API 的 Token 端点发送 POST 请求的响应内容。响应的重要部分是 `access_token` 本身。

这看起来和以前没什么不同……

此时,反序列化的 JSON 响应看起来与我们之前的文章中没有添加所有花哨的新角色和自定义属性之前没有任何不同。它现在不应该包含一些新信息吗?角色、地址等等?

不支持。

关于不记名令牌的更多信息(但只是一点点)

在 Web API 的 Identity 介绍中,我们非常简要地了解了不记名令牌。如那里所述,我们将在另一篇文章中更深入地探讨令牌。

今天,我们只是在之前所学的基础上稍作扩展,足以理解我们上面从 JSON 响应中检索到的 `access_token` 如何融入我们新修改的 Web API 项目方案。

不记名令牌按设计对客户端是“不透明”的。换句话说,它们在服务器上编码(有时加密),可以由服务器解码(并可能解密)。它们并非设计为由客户端解码/解密。

回想一下,在探索 ASP.NET Web API 项目的基本结构时,ApplicationOauthProvider 类在 Providers => ApplicationOauthProvider.cs 文件中定义。

当您向 ASP.NET Web API 应用程序的令牌端点发送 POST 请求时(至少,在我们此处配置的方式下),服务器通过调用 ApplicationOauthProvider 类上定义的 GrantResourceOwnersCredentials() 方法来验证您提交的凭据(在此例中为用户名 + 密码)。

来自 ApplicationOAuthProvider 的 GrantResourceOwnersCredentials() 方法
public override async Task GrantResourceOwnerCredentials(
    OAuthGrantResourceOwnerCredentialsContext context)
{
    var userManager = context.OwinContext.GetUserManager<ApplicationUserManager>();
    ApplicationUser user = await userManager.FindAsync(context.UserName, context.Password);
 
    if (user == null)
    {
        context.SetError("invalid_grant", "The user name or password is incorrect.");
        return;
    }
 
    ClaimsIdentity oAuthIdentity = await user.GenerateUserIdentityAsync(userManager,
       OAuthDefaults.AuthenticationType);
    ClaimsIdentity cookiesIdentity = await user.GenerateUserIdentityAsync(userManager,
        CookieAuthenticationDefaults.AuthenticationType);
 
    AuthenticationProperties properties = CreateProperties(user.UserName);
    AuthenticationTicket ticket = new AuthenticationTicket(oAuthIdentity, properties);
    context.Validated(ticket);
    context.Request.Context.Authentication.SignIn(cookiesIdentity);
}

我们可以看到此代码尝试查找与所提交凭据匹配的用户。如果找到有效用户,该方法将调用 `ApplicationUser` 类上定义的 `GenerateUserIdentityAsync()` 方法来获取代表用户以及用户在我们的系统中可能提出的任何声明的 `ClaimsIdentity` 实例。

然后使用 `ClaimsIdentity` 创建一个 `AuthenticationTicket`。

在上面的 `GrantResourceOwnersCredentials()` 代码中,当在 `OAuthGrantResourceOwnerCredentialsContext` 上调用 `Validated()` 时,OWIN 中间件会将 `ClaimsIdentity` 序列化为一个编码和签名的令牌,作为我们请求的结果在 HTTP 响应头中返回。

所有上述内容都是冗长的方式来表达,重要的用户信息,包括用户角色,以及我们决定需要成为令牌一部分的任何其他内容,在客户端收到令牌时都存在于令牌中。

客户端只是无法访问它。

关于不记名令牌和安全的注意事项

如我们之前的文章所述,在生产系统中,使用不记名令牌并通过提交凭据从令牌端点获取令牌应仅通过 SSL/STL 进行。不记名令牌的含义正如其名称所示——任何出示有效不记名令牌的人都将被授予系统访问权限,并拥有该令牌实际“所有者”所拥有的任何权限。

OAuth 不记名令牌代表了一种相当简单的身份验证/授权方案。在构建您的 Web API 时,请考虑所有替代方案,并为您的应用程序做出最佳选择。

如前所述,我们将在后续文章中更深入地探讨声明身份和令牌身份验证/授权。

向 Web API 应用程序添加基于角色的授权

现在我们了解了如何使用不记名令牌进行身份验证,接下来让我们看看如何在应用程序中使用我们新的基于角色的授权功能。

首先,让我们看看 Web API 应用程序中存在的两个主要控制器:`AccountController` 和 `ValuesController`(我们忽略 `HomeController`,因为它在这里对我们的需求没有用)。

我们已经看到 `AccountController` 用 `[Authorize]` 属性进行修饰。这意味着只有经过身份验证的用户才能访问此控制器上定义的动作方法(当然,除非方法本身用 `[AllowAnonymous]` 属性进行修饰)。

让我们看看简单的 `ValuesController`。`ValuesController` 是一个简单的示例,说明如何添加基本的 CRUD 样式功能。`ValuesController` 同样使用 `[Authorize]` 属性进行修饰。同样,只有经过身份验证的用户才能访问 `ValuesController` 上定义的 Action 方法。

正如我们在之前关于在 MVC 项目中实现基于角色的授权的文章中看到的那样,我们可以通过扩展 `[Authorize]` 的使用,在控制器级别或 Action 方法级别修改我们的 Web API 的访问权限。

考虑一下,我们可能希望将访问权限限制为AccountController仅限于 Admin 角色的用户,但允许任何经过身份验证的用户访问 `ValuesController` 及其提供的功能。

在这种情况下,我们需要对 Web API 配置进行一些修改。

在 ApplicationDbInitializer 中添加一个普通用户角色作为默认值

首先,让我们确保我们的应用程序中有两个不同的角色可用——我们已经在配置期间创建的“Admin”角色,以及一个新的“Users”角色。更新 `InitializeDatabaseForEF()` 方法如下:

向 InitializeDatabaseForEF() 方法添加 Users 角色和默认用户
public static void InitializeIdentityForEF(ApplicationDbContext db)
{
    var userManager = HttpContext.Current
        .GetOwinContext().GetUserManager<ApplicationUserManager>();
 
    var roleManager = HttpContext.Current
        .GetOwinContext().Get<ApplicationRoleManager>();
 
    // Initial Admin user:
    const string name = "admin@example.com";
    const string password = "Admin@123456";
 
    // Some initial values for custom properties:
    const string address = "1234 Sesame Street";
    const string city = "Portland";
    const string state = "OR";
    const string postalCode = "97209";
 
    const string roleName = "Admin";
    const string roleDescription = "All access pass";
 
    //Create Role Admin if it does not exist
    var role = roleManager.FindByName(roleName);
    if (role == null)
    {
        role = new ApplicationRole(roleName);
 
        // Set the new custom property:
        role.Description = roleDescription;
        var roleresult = roleManager.Create(role);
    }
 
    // Create Admin User:
    var user = userManager.FindByName(name);
    if (user == null)
    {
        user = new ApplicationUser { UserName = name, Email = name };
 
        // Set the new custom properties:
        user.Address = address;
        user.City = city;
        user.State = state;
        user.PostalCode = postalCode;
 
        var result = userManager.Create(user, password);
        result = userManager.SetLockoutEnabled(user.Id, false);
    }
 
    // Add user admin to Role Admin if not already added
    var rolesForUser = userManager.GetRoles(user.Id);
    if (!rolesForUser.Contains(role.Name))
    {
        userManager.AddToRole(user.Id, role.Name);
    }
 
    // Initial Vanilla User:
    const string vanillaUserName = "vanillaUser@example.com";
    const string vanillaUserPassword = "Vanilla@123456";
 
    // Add a plain vannilla Users Role:
    const string usersRoleName = "Users";
    const string usersRoleDescription = "Plain vanilla User";
 
    //Create Role Users if it does not exist
    var usersRole = roleManager.FindByName(usersRoleName);
    if (usersRole == null)
    {
        usersRole = new ApplicationRole(usersRoleName);
 
        // Set the new custom property:
        usersRole.Description = usersRoleDescription;
        var userRoleresult = roleManager.Create(usersRole);
    }
 
    // Create Vanilla User:
    var vanillaUser = userManager.FindByName(vanillaUserName);
    if (vanillaUser == null)
    {
        vanillaUser = new ApplicationUser 
        { 
            UserName = vanillaUserName, 
            Email = vanillaUserName 
        };
 
        // Set the new custom properties:
        vanillaUser.Address = address;
        vanillaUser.City = city;
        vanillaUser.State = state;
        vanillaUser.PostalCode = postalCode;
 
        var result = userManager.Create(vanillaUser, vanillaUserPassword);
        result = userManager.SetLockoutEnabled(vanillaUser.Id, false);
    }
 
    // Add vanilla user to Role Users if not already added
    var rolesForVanillaUser = userManager.GetRoles(vanillaUser.Id);
    if (!rolesForVanillaUser.Contains(usersRole.Name))
    {
        userManager.AddToRole(vanillaUser.Id, usersRole.Name);
    }
}

上面,我们添加了一个新角色“Users”和另一个初始示例用户。

接下来,让我们修改 `AccountController` 类上的 `[Authorize]` 属性,并添加一个 Role 参数。

AccountController 的修改后的 [Authorize] 属性
[Authorize(Roles= "Admin")]
[RoutePrefix("api/Account")]
public class AccountController : ApiController
{
    // ... All the Code ...
}

现在,我们只需要更改客户端代码,尝试访问两个控制器的一些方法,看看我们的基于角色的授权是如何为我们工作的。

修改客户端代码以尝试控制器访问

这里,我们将简单地设置一些客户端代码,尝试从 `AccountController` 和 `ValuesController` 都检索一些基本数据。我们将以 Admin 角色用户身份执行此操作,然后也以 Users 角色用户身份执行此操作。

将您的控制台应用程序中的代码更改为以下内容:

使用不同角色访问两个控制器的修改客户端代码
class Program
{
    // You will need to substitute your own host Url here:
    static string host = "https://:63074/";
 
    static void Main(string[] args)
    {
        // Use the User Names/Emails and Passwords we set up in IdentityConfig:
        string adminUserName = "admin@example.com";
        string adminUserPassword = "Admin@123456";
 
        string vanillaUserName = "vanillaUser@example.com";
        string vanillaUserPassword = "Vanilla@123456";
 
        // Use the new GetToken method to get a token for each user:
        string adminUserToken = GetToken(adminUserName, adminUserPassword);
        string vaniallaUserToken = GetToken(vanillaUserName, vanillaUserPassword);
 
        // Try to get some data as an Admin:
        Console.WriteLine("Attempting to get User info as Admin User");
        string adminUserInfoResult = GetUserInfo(adminUserToken);
        Console.WriteLine("Admin User Info Result: {0}", adminUserInfoResult);
        Console.WriteLine("");
 
        Console.WriteLine("Attempting to get Values info as Admin User");
        string adminValuesInfoResult = GetValues(adminUserToken);
        Console.WriteLine("Admin Values Info Result: {0}", adminValuesInfoResult);
        Console.WriteLine("");
 
        // Try to get some data as a plain old user:
        Console.WriteLine("Attempting to get User info as Vanilla User");
        string vanillaUserInfoResult = GetUserInfo(vaniallaUserToken);
        Console.WriteLine("Vanilla User Info Result: {0}", vanillaUserInfoResult);
        Console.WriteLine("");
 
        Console.WriteLine("Attempting to get Values info as Vanilla User");
        string vanillaValuesInfoResult = GetValues(vaniallaUserToken);
        Console.WriteLine("Vanilla Values Info Result: {0}", vanillaValuesInfoResult);
        Console.WriteLine("");
 
        Console.Read();
    }
 
 
    static string GetToken(string userName, string password)
    {
        HttpClient client = new HttpClient();
        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);
 
        // Attempt to get a token from the token endpoint of the Web Api host:
        HttpResponseMessage response =
            client.PostAsync(host + "Token", content).Result;
        var result = response.Content.ReadAsStringAsync().Result;
 
        // De-Serialize into a dictionary and return:
        Dictionary<string, string> tokenDictionary =
            JsonConvert.DeserializeObject<Dictionary<string, string>>(result);
        return tokenDictionary["access_token"];
    }
 
 
    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(host + "api/Account/UserInfo").Result;
            return response.Content.ReadAsStringAsync().Result;
        }
    }
 
 
    static string GetValues(string token)
    {
        using (var client = new HttpClient())
        {
            client.DefaultRequestHeaders.Authorization =
            new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token);
 
            var response = client.GetAsync(host + "api/Values").Result;
            return response.Content.ReadAsStringAsync().Result;
        }
    }
}

在上述代码中,我们稍微更改了代码。我们现在有一个 `GetToken()` 方法,它接受用户名/密码作为参数,并且只从对我们 Web API 的 Token 端点的请求中返回 access_token 字符串。

接下来,我们向 Web API 添加了两个不同的调用。一个方法调用 `AccountController` 上的 `GetUserInfo()` 方法,另一个调用 `ValuesController` 上的 `Get()` 方法。

如果我们启动 Web API,并在它启动后运行我们的客户端应用程序,我们应该在控制台中看到以下输出:

访问权限比较的控制台输出
Attempting to get User info as Admin User
Admin User Info Result: {"Email":"admin@example.com","HasRegistered":true,"Login
Provider":null}
Attempting to get Values info as Admin User
Admin Values Info Result: ["value1","value2"]
Attempting to get User info as Vanilla User
Vanilla User Info Result: {"Message":"Authorization has been denied for this req
uest."}
Attempting to get Values info as Vanilla User
Vanilla Values Info Result: ["value1","value2"]

请注意第三次尝试的输出。我们正在尝试以一个普通的 User 身份(在 Users 角色中)调用 `GetUserInfo()`。相应地,我们的 Web API 返回了一个授权错误,因为根据 `AccountController` 类声明上的 `[Authorize(Roles="Admin")]` 属性,`Users` 不被允许访问该方法。

相反,两个用户都能够访问 `ValuesController` 上的 `Get()` 方法,因为此控制器用一个简单的 `[Authorize]` 属性修饰,该属性只需要经过身份验证的用户才能访问。

访问自定义用户属性

我们还为 `ApplicationUserModel` 添加了一些自定义属性。让我们看看是否可以在这里的示例中使用它们。

如果我们仔细查看 `AccountController` 上的 `GetUserInfo()` 方法,我们会发现该方法的实际返回类型是 `UserInfoViewModel`,它位于 Models => AccountViewModels.cs 文件中。现在,在我们粗糙简单的控制台应用程序中,我们不会费力将 GET 请求中的 JSON 反序列化为对象,但我们可以做到。

就我们此处而言,修改 `UserInfoViewModel` 以反映我们想要返回的附加属性,然后更新 `GetUserInfo()` 方法以适应即可。

将我们的自定义用户属性添加到 UserInfoViewModel 类中
public class UserInfoViewModel
{
    public string Email { get; set; }
    public bool HasRegistered { get; set; }
    public string LoginProvider { get; set; }
 
    // Add our custom properties from ApplicationUser:
    public string Address { get; set; }
    public string City { get; set; }
    public string State { get; set; }
    public string PostalCode { get; set; }
}

接下来,更新 `GetUserInfo()` 方法,为我们刚刚添加到 `UserInfoViewModel` 的附加属性提供值。

使用自定义属性更新 AccountController 上的 GetUserInfo() 方法
public UserInfoViewModel GetUserInfo()
{
    ExternalLoginData externalLogin 
        = ExternalLoginData.FromIdentity(User.Identity as ClaimsIdentity);
 
    // We wouldn't normally be likely to do this:
    var user = UserManager.FindByName(User.Identity.Name);
    return new UserInfoViewModel
    {
        Email = User.Identity.GetUserName(),
        HasRegistered = externalLogin == null,
        LoginProvider = externalLogin != null ? externalLogin.LoginProvider : null,
 
        // Pass the custom properties too:
        Address = user.Address,
        City = user.City,
        State = user.State,
        PostalCode = user.PostalCode
    };
}

上面,我们调用了 `UserManager` 来检索我们的用户实例,以便我们可以访问我们添加的新属性。然后,我们在返回之前在 `UserInfoViewModel` 上设置了相应的值。

这个例子有点做作,我们在生产应用程序中很可能不会这样做。但就目前而言,它将用于演示我们改进后的 `ApplicationUser` 确实具有我们添加到模型的自定义属性,并且我们正在按预期从数据库中检索它们。

如果我们最后一次运行控制台客户端应用程序,我们应该会看到以下输出:

带有自定义用户属性的控制台应用程序输出
Attempting to get User info as Admin User
Admin User Info Result: {"Email":"admin@example.com","HasRegistered":true,"Login
Provider":null,"Address":"1234 Sesame Street","City":"Portland","State":"OR",
"PostalCode":"97209"}
Attempting to get Values info as Admin User
Admin Values Info Result: ["value1","value2"]
Attempting to get User info as Vanilla User
Vanilla User Info Result: {"Message":"Authorization has been denied for this 
request."}
Attempting to get Values info as Vanilla User
Vanilla Values Info Result: ["value1","value2"]

我们看到,我们的自定义用户属性随着 JSON 响应一起返回。

基于角色的授权与基于声明的授权

在这篇文章中,我们简要地探讨了在 ASP.NET Web API 项目中实现基于角色的授权,并简要地了解了不记名令牌的工作原理。然而,值得注意的是,除了最简单的授权/访问控制方案外,基于角色的授权("RBA")正迅速被基于声明的授权所取代。

基于声明的身份提供了更大的灵活性,并且更有效地将身份验证和授权机制与您的代码分离。请注意,在此示例项目中,我们需要在 `[Authorize]` 属性中明确指定哪些角色被允许访问哪些控制器和/或方法。

基于声明的身份验证比 RBA 更复杂,但通常更适合 Web API 场景,除非您的需求非常简单。

我们将在后续文章中探讨令牌和基于声明的身份。

其他资源和感兴趣的项目

© . All rights reserved.