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

ASP.NET Web API:理解 OWIN/Katana 身份验证/授权 第一部分:概念

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.96/5 (13投票s)

2015年2月16日

CPOL

18分钟阅读

viewsIcon

75979

Identity 完全兼容 OWIN 授权模型,并且在这种方式下使用时,它代表了一个非常有用的、开箱即用的具体实现。

 
 
 

Ah-aint-long-for-this-whorl-240最近,我们探讨了 OWIN/Katana 中间件管道的基础知识,然后应用了我们所学的知识,并从零开始构建了一个最小化的、基于 OWIN 的自托管 Web Api。通过这样做,我们成功地避免了System.Web库或 IIS 的沉重负担,最终得到了一个非常轻量级的应用程序。然而,我们讨论过的所有概念,无论托管环境如何,都仍然有效。

但是,如果我们想为这样一个最小化的项目添加一些基本的身份验证呢?

图片由 Chad Miller 提供 | 部分权利保留

再一次,我们将尝试应用我们所学的知识,将一个非常小的身份验证/授权组件引入我们最小化的 Web Api 应用程序中。我们将从实现一个基本的身份验证/授权模型开始,而**不**使用 ASP.NET Identity 框架提供的组件。

Identity 完全兼容 OWIN 授权模型,并且在这种方式下使用时,它代表了一个非常有用的、开箱即用的具体实现。但是,如果我们从简单的概念开始,逐步构建到具体的实现和额外的框架,或许可以更好地理解 OWIN 授权的结构以及应用程序的安全性。

从头开始

在这个系列的文章中,我们将从概念入手,并在此基础上慢慢构建。

  • 第一部分(本文)- 我们将研究用于身份验证的基本 OAuth 资源所有者密码凭据授予(Resource Owner Flow)模型,并组装实现此模型所需的最基本组件。我们不会关注正确哈希密码的加密要求,也不会涉及将用户信息持久化到数据库。我们也不会使用 Identity,而是使用 Microsoft.Owin 库中可用的基本组件来实现安全性。
  • 第二部分 - 我们将模拟一些为用户数据建模所需的基本类,以及一个持久化模型,以了解用户数据和其他元素的存储在基础层面是如何工作的。
  • 第三部分 - 我们将用 Identity 2.0 组件替换我们的模拟对象,以提供加密和安全功能(因为自己编写加密算法不是个好主意)。

与我们之前的文章一样,这里的目标不仅仅是“如何做”,更多的是要理解身份验证,特别是 Identity 2.0,是如何融入一个基于 OWIN 的应用程序结构的。

考虑到这一点,我们将仅使用 OWIN/Katana 授权组件和简化的示例,尽可能地深入探讨。一旦我们看到了基于 OWIN 的 Web Api 应用程序中身份验证和授权的底层结构,然后我们才会引入 Identity 2.0 来提供具体的实现。

示例源代码

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

在 Github 上,Web Api 仓库的分支目前如下所示:

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

应用程序安全很难 - 不要自己造轮子!

实现有效的应用程序安全是一项不平凡的任务。在我们使用的看似简单的框架 API(如 Identity 2.0 或任何其他成员/身份验证库)背后,是业界最顶尖人才数十年的开发成果。

在我们即将看到的示例中,你会看到一些地方我们模拟了一些荒谬的方法来(例如)哈希或验证密码。实际上,**安全地哈希密码是一个复杂但已解决的问题**。你永远不应尝试编写自己的加密或数据保护方案

即使是像我们将要实现的这样一个简单的身份验证机制,也会给项目带来一些复杂性,因为身份验证本身就是复杂的。在像 ASP.NET Identity 这样看似简单的框架 API 背后,隐藏着一些加密和逻辑,除非你真的知道自己在做什么,否则最好不要去动它。

话虽如此,理解各个部分如何组合在一起,以及你可以在哪里深入研究并调整现有的授权/身份验证流程以满足你的应用程序的需求,这是很重要的,也是本系列的主要目标。

OAuth 资源所有者密码凭据授予(Resource Owner Flow)身份验证模型

Web 应用程序中常用的一种身份验证模式是 OAuth 资源所有者密码凭据授予(Resource Owner Flow)模型。事实上,这正是 Visual Studio 中 Web Api 模板项目所使用的模型。我们将要在我们基于 OWIN 的 Web Api 应用程序中“几乎从零开始”实现使用资源所有者密码凭据授予的身份验证。

资源所有者密码凭据授予模型假定在一个身份验证场景中有四个主要“角色”:

  • 资源所有者 (The Resource Owner) - 例如,一个用户,或者可能是另一个应用程序。
  • 客户端 (The Client) - 通常是资源所有者用来访问受保护资源的客户端应用程序。在我们的例子中,客户端可能是我们的 Web Api 客户端应用程序。
  • 授权服务器 (The Authorization Server) - 一个接受资源所有者凭据(通常是某种形式的凭据和密码的组合,例如用户名/密码组合)并返回一个编码或加密的访问令牌的服务器。
  • 资源服务器 (The Resource Server) - 资源所在的服务器,它保护资源免受未经授权的访问,除非请求中提供了有效的身份验证/授权凭据。
资源所有者密码凭据授予(Owner Resource Flow)身份验证流程概述

oath-resource-owner-flow

在上述流程中,*资源所有者*向*客户端*提供一组凭据。然后,*客户端*将凭据提交给*授权服务器*,如果凭据能够被*授权服务器*正确验证,一个编码和/或加密的*访问令牌*将被返回给*客户端*。

然后,*客户端*使用*访问令牌*向*资源服务器*发出请求。*资源服务器*已被配置为接受源自*授权服务器*的*访问令牌*,并对这些令牌进行解码/解密,以确认*资源所有者*的身份和授权声明(如果提供的话)。

所有这一切都基于*资源所有者*已在*授权服务器*正确注册的前提。

需要注意的是,OAuth 规范要求任何涉及密码/凭据传输的事务都必须使用 SSL/TSL (HTTPS) 进行。

我们的实现,以及 VS Web Api 项目模板的实现,对此做了一些调整,即将*认证服务器*嵌入到*资源服务器*中:

资源所有者密码凭据授予(Owner Resource Flow)的嵌入式认证服务器变体

oath-embedded-resource-owner-flow

基础知识 - OWIN, Katana, 和身份验证

我们可以构建一个非常精简的示例来演示这些部分是如何组合在一起的,然后再用更高级别的组件和任何额外的数据库问题来使事情变得复杂。

要开始,你可以下载我们在上一篇文章中构建的自托管 web api 的源代码。我们将从那个项目结束的地方继续,并添加一个基本的身份验证组件。

回想一下,我们已经构建了一个相当精简的基于 Owin 的 Web Api,它由一个 OWIN启动类,一个简单的Company模型类,以及一个CompaniesController组成。该应用程序本身是一个基于控制台的应用程序,其标准入口点在Main()方法中程序类。

。在该项目中,我们决定由于是自托管应用程序,我们将把数据存储保持在进程内,并使用本地基于文件的数据存储。我们选择使用 SQL Server Compact Edition,因为它能与 Entity Framework 和 Code-First 数据库生成很好地协同工作。因此,我们还添加了一个ApplicationDbContext。

在做任何更改之前,我们可以回顾一下我们现有的项目组件。

起点 - 自托管 Web Api 项目

首先,我们有我们的 OWIN启动

来自最小化自托管 Web Api 项目的 OWIN Startup 类
// Add the following usings:
using Owin;
using System.Web.Http;
 
namespace MinimalOwinWebApiSelfHost
{
    public class Startup
    {
        // This method is required by Katana:
        public void Configuration(IAppBuilder app)
        {
            var webApiConfiguration = ConfigureWebApi();
 
            // Use the extension method provided by the WebApi.Owin library:
            app.UseWebApi(webApiConfiguration);
        }
 
 
        private HttpConfiguration ConfigureWebApi()
        {
            var config = new HttpConfiguration();
            config.Routes.MapHttpRoute(
                "DefaultApi",
                "api/{controller}/{id}",
                new { id = RouteParameter.Optional });
            return config;
        }
    }
}

然后,我们有一个简单的Company模型,适当地位于我们项目的 *Models* 文件夹中

原始的 Company 模型类
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
 
// Add using:
using System.ComponentModel.DataAnnotations;
 
namespace MinimalOwinWebApiSelfHost.Models
{
    public class Company
    {
        // Add Key Attribute:
        [Key]
        public int Id { get; set; }
        public string Name { get; set; }
    }
}

以及我们原始的CompaniesController类,同样适当地位于我们项目内的 *Controllers* 文件夹中:

原始的 Companies 控制器
public class CompaniesController : ApiController
{
    ApplicationDbContext _Db = new ApplicationDbContext();
 
 
    public IEnumerable<Company> Get()
    {
        return _Db.Companies;
    }
 
 
    public async Task<Company> Get(int id)
    {
        var company = 
                await _Db.Companies.FirstOrDefaultAsync(c => c.Id == id);
        if (company == null)
        {
            throw new HttpResponseException(
                System.Net.HttpStatusCode.NotFound);
        }
        return company;
    }
 
 
    public async Task<IHttpActionResult> Post(Company company)
    {
        if (company == null)
        {
            return BadRequest("Argument Null");
        }
        var companyExists = 
                await _Db.Companies.AnyAsync(c => c.Id == company.Id);
 
        if (companyExists)
        {
            return BadRequest("Exists");
        }
 
        _Db.Companies.Add(company);
        await _Db.SaveChangesAsync();
        return Ok();
    }
 
 
    public async Task<IHttpActionResult> Put(Company company)
    {
        if (company == null)
        {
            return BadRequest("Argument Null");
        }
        var existing = 
                await _Db.Companies.FirstOrDefaultAsync(c => c.Id == company.Id);
 
        if (existing == null)
        {
            return NotFound();
        }
 
        existing.Name = company.Name;
        await _Db.SaveChangesAsync();
        return Ok();
    }
 
 
    public async Task<IHttpActionResult> Delete(int id)
    {
        var company = 
                await _Db.Companies.FirstOrDefaultAsync(c => c.Id == id);
        if (company == null)
        {
            return NotFound();
        }
        _Db.Companies.Remove(company);
        await _Db.SaveChangesAsync();
        return Ok();
    }
}

同样在 *Models* 文件夹中是我们的 *ApplicationDbContext.cs* 文件,它实际上包含了ApplicationDbContext本身,以及一个DBInitializer。目前,它派生自DropDatabaseCreateAlways,因此每次应用程序运行时,数据库都会被销毁并重新填充数据。

原始的 ApplicationDbContext 和 DbInitializer
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
 
// Add using:
using System.Data.Entity;
using Microsoft.AspNet.Identity;
using Microsoft.AspNet.Identity.EntityFramework;
 
namespace MinimalOwinWebApiSelfHost.Models
{
    public class ApplicationDbContext : DbContext
    {
        public ApplicationDbContext()
            : base("MyDatabase")
        {
 
        }
 
        static ApplicationDbContext()
        {
            Database.SetInitializer(new ApplicationDbInitializer());
        }
 
        public IDbSet<Company> Companies { get; set; }
    }
 
 
    public class ApplicationDbInitializer 
        : DropCreateDatabaseAlways<ApplicationDbContext>
    {
        protected 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();
        }
    }
}

实际上,自上一篇文章以来,我更改了原始的ApplicationDbContext代码。我添加了一个静态构造函数,在上下文实例化时设置数据库初始化器。这将在我们第一次访问数据库时调用初始化器。

这比以前的解决方案要干净得多,以前我们是在Main()程序

原始 Program.cs 文件(稍作修改)
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
 
// Add reference to:
using Microsoft.Owin.Hosting;
using System.Data.Entity;
using MinimalOwinWebApiSelfHost.Models;
 
namespace MinimalOwinWebApiSelfHost
{
    class Program
    {
        static void Main(string[] args)
        {
            // Specify the URI to use for the local host:
            string baseUri = "https://:8080";
 
            Console.WriteLine("Starting web Server...");
            WebApp.Start<Startup>(baseUri);
            Console.WriteLine("Server running at {0} - press Enter to quit. ", baseUri);
            Console.ReadLine();
        }
    }
}

既然我们知道了上次的进度,让我们来看看如何实现一个非常基本的 OAuth 资源所有者密码凭据授予(Resource Owner Flow)模型的身份验证示例。

Microsoft.AspNet.Identity.OwinNuget 包包含了实现一个基本的资源所有者密码凭据授予(Resource Owner Flow)示例所需的一切,尽管我们暂时还不会直接处理 Identity。

Microsoft.AspNet.Identity.Owin包拉取到我们的项目中。

添加 Microsoft ASP.NET Identity Owin Nuget 包
PM> Install-Package Microsoft.AspNet.Identity.Owin -Pre

现在我们准备开始了…

添加嵌入式授权服务器

资源所有者密码凭据授予(Resource Owner Flow)的关键是授权服务器。在我们的案例中,授权服务器实际上将包含在我们的 Web Api 应用程序中,但其功能与独立托管时相同。

Microsoft.Owin.Security.OAuth库定义了一个默认实现IOAuthAuthorizationServerProvider, OAuthAuthorizationServerProvider,这允许我们为我们的应用程序派生一个自定义实现。如果你以前使用过 Visual Studio Web Api 项目模板,你应该会认出这个。向项目中添加一个新文件夹 *OAuthServerProvider*,然后按如下方式添加一个类:

添加 ApplicationOAuthServerProvider 类
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
 
// Add Usings:
using Microsoft.Owin.Security;
using Microsoft.Owin.Security.OAuth;
using System.Security.Claims;
using MinimalOwinWebApiSelfHost.Models;
 
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)
        {
            // DEMO ONLY: Pretend we are doing some sort of REAL checking here:
            if (context.Password != "password")
            {
                context.SetError(
                    "invalid_grant", "The user name or password is incorrect.");
                context.Rejected();
                return;
            }
 
            // Create or retrieve a ClaimsIdentity to represent the 
            // Authenticated user:
            ClaimsIdentity identity = 
                new ClaimsIdentity(context.Options.AuthenticationType);
            identity.AddClaim(new Claim("user_name", context.UserName));
 
            // Identity info will ultimately be encoded into an Access Token
            // as a result of this call:
            context.Validated(identity);
        }
    }
}

你可以看到我们重写了OAuthAuthorizationServerProvider上的两个可用方法。第一个,ValidateClientAuthentication(),是必需的,尽管在我们的案例中我们没有验证客户端应用程序(虽然如果我们想的话,是可以的)。我们只是调用了Validated()ClientValidationContext上,然后继续。在一个更复杂的场景中,或者需要更强安全性的场景中,我们可能既要验证客户端也要验证资源所有者。

我们的身份验证过程的核心部分发生在GrantResourceOwnerCredentials()方法中。在我们示例的这一部分,我们保持简单。我们编写了一个身份验证过程,基本上是将传入的密码与硬编码的字符串值 "password" 进行比较。如果此检查失败,则设置一个错误,身份验证失败。

当然,在现实中,我们会(并且很快就会)实现一个更复杂的对用户凭据的检查。但就目前而言,这样做可以让我们不偏离整体结构。

如果凭据检查成功,则会创建一个ClaimsIdentity实例来表示用户数据,包括用户应有的任何声明(Claims)。目前,我们所做的只是将用户名作为唯一的声明添加,然后调用Validated()GrantResourceOwnerCredentials上下文。

调用Validated()最终导致 OWIN 中间件将ClaimsIdentity数据编码成一个访问令牌(Access Token)。在Microsoft.Owin实现的上下文中,这是如何发生的,过程复杂,超出了本文的范围。如果你想深入了解,可以下载一份 Telerik 的优秀工具 Just Decompile。简而言之,ClaimsIdentity信息是用私钥(通常但不总是服务器运行所在机器的机器密钥)加密的。一旦加密,访问令牌就会被添加到传出的 HTTP 响应体中。

配置 OWIN 身份验证并添加到中间件管道

现在我们已经有了实际的授权服务器,让我们配置我们的 OWIN Startup 类来验证传入的请求。

我们将添加一个新方法,ConfigureAuth()到我们的启动类中。请检查确保您已将以下using和代码添加到 Startup

向 OWIN Startup 类添加一个 ConfigureAuth() 方法
using System;
 
// Add the following usings:
using Owin;
using System.Web.Http;
using MinimalOwinWebApiSelfHost.Models;
using MinimalOwinWebApiSelfHost.OAuthServerProvider;
using Microsoft.Owin.Security.OAuth;
using Microsoft.Owin;
 
namespace MinimalOwinWebApiSelfHost
{
    public class Startup
    {
        // This method is required by Katana:
        public void Configuration(IAppBuilder app)
        {
            ConfigureAuth(app);
            var webApiConfiguration = ConfigureWebApi();
            app.UseWebApi(webApiConfiguration);
        }
 
 
        private void ConfigureAuth(IAppBuilder app)
        {
            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());
        }
 
 
        private HttpConfiguration ConfigureWebApi()
        {
            var config = new HttpConfiguration();
            config.Routes.MapHttpRoute(
                "DefaultApi",
                "api/{controller}/{id}",
                new { id = RouteParameter.Optional });
            return config;
        }
    }
}

上面的ConfigureAuth()方法中发生了很多事情。

首先,我们初始化一个OAuthAuthorizationServerOptions的实例。作为初始化的一部分,我们设置了令牌端点,并将我们ApplicationOAuthAuthenticationServerProvider类的一个新实例赋给选项对象的提供商属性。

我们为任何发布的令牌设置了过期时间,然后我们明确允许授权服务器允许不安全的 HTTP 连接。关于最后一点的说明——这纯粹是为了演示目的。在实际应用中,你肯定希望**使用安全的 SSL/TLS 协议 (HTTPS) 连接到授权服务器**,因为你正在以明文形式传输用户凭据。

一旦我们的授权服务器选项配置完毕,我们就可以看到通常用于向IAppBuilder添加中间件的标准扩展方法。我们通过UseAuthorizationServer()传入我们的服务器选项,然后我们通过UseOAuthBearerAuthentication()表明我们想要返回 *Bearer Tokens*。在这种情况下,我们传递的是OAuthBearerAuthenticationOptions的默认实现,尽管如果需要,我们可以从中派生并进行自定义。

服务器被添加到选项对象中,该对象指定了其他配置项,然后被传递到中间件管道中。

验证客户端:从授权服务器检索访问令牌

再次说明,在之前的文章中,我们已经构建了一个粗糙但有效的 API 客户端应用程序来测试我们的 API。

在这篇文章中,我们将基本上重写客户端应用程序。

首先,我们将添加一个新的类,apiClient类。这个类将负责将我们的凭证提交给我们的 Web Api,并获取一个Dictionary<string, string>,其中包含反序列化后的响应体,包括访问令牌以及有关身份验证过程的附加信息。

ApiClient 类
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
 
// Add Usings:
using System.Net.Http;
 
// Add for Identity/Token Deserialization:
using Newtonsoft.Json;
 
namespace MinimalOwinWebApiClient
{
    public class apiClientProvider
    {
        string _hostUri;
        public string AccessToken { get; private set; }
 
        public apiClientProvider(string hostUri)
        {
            _hostUri = hostUri;
        }
 
 
        public async Task<Dictionary<string, string>> GetTokenDictionary(
            string userName, string password)
        {
            HttpResponseMessage response;
            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 tokenEndpoint = new Uri(new Uri(_hostUri), "Token");
                response =  await client.PostAsync(tokenEndpoint, content);
            }
 
            var responseContent = await response.Content.ReadAsStringAsync();
            if (!response.IsSuccessStatusCode)
            {
                throw new Exception(string.Format("Error: {0}", responseContent));
            }
 
            return GetTokenDictionary(responseContent);
        }
 
 
        private Dictionary<string, string> GetTokenDictionary(
            string responseContent)
        {
            Dictionary<string, string> tokenDictionary =
                JsonConvert.DeserializeObject<Dictionary<string, string>>(
                responseContent);
            return tokenDictionary;
        }
    }
}

有了这个,我们就可以像这样重新实现客户端的程序类了

客户端 Program 类
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
 
// Add Usings:
using System.Net.Http;
 
namespace MinimalOwinWebApiClient
{
    class Program
    {
        static void Main(string[] args)
        {
            // Wait for the async stuff to run...
            Run().Wait();
 
            // Then Write Done...
            Console.WriteLine("");
            Console.WriteLine("Done! Press the Enter key to Exit...");
            Console.ReadLine();
            return;
        }
 
 
        static async Task Run()
        {
            // Create an http client provider:
            string hostUriString = "https://:8080";
            var provider = new apiClientProvider(hostUriString);
            string _accessToken;
            Dictionary<string, string> _tokenDictionary;
 
            try
            {
                // Pass in the credentials and retrieve a token dictionary:
                _tokenDictionary = await provider.GetTokenDictionary(
                        "john@example.com", "password");
                _accessToken = _tokenDictionary["access_token"];
            }
            catch (AggregateException ex)
            {
                // If it's an aggregate exception, an async error occurred:
                Console.WriteLine(ex.InnerExceptions[0].Message);
                Console.WriteLine("Press the Enter key to Exit...");
                Console.ReadLine();
                return;
            }
            catch (Exception ex)
            {
                // Something else happened:
                Console.WriteLine(ex.Message);
                Console.WriteLine("Press the Enter key to Exit...");
                Console.ReadLine();
                return;
            }
 
            // Write the contents of the dictionary:
            foreach(var kvp in _tokenDictionary)
            {
                Console.WriteLine("{0}: {1}", kvp.Key, kvp.Value);
                Console.WriteLine("");
            }
        }
    }
}

到目前为止,我们已经舍弃了所有向我们 API 中的CompaniesController发出请求的代码,我们现在只关注那些对我们进行身份验证并检索访问令牌的代码。

注意,我们在这里加入了一些非常初步的异常处理。在一个真实的应用中,我们可能需要更多的信息,并且需要整合一个更稳健的机制来处理 HTTP 错误以及其他可能出错的情况。

如果我们运行 Web Api 应用程序,然后运行客户端应用程序,我们应该能从客户端应用程序看到以下输出:

身份验证后客户端应用程序的输出

console-output-client-application-authentication 

我们看到我们已经成功地从我们极其简单的认证服务器获取了一个访问令牌。但是,如果我们传递了无效的凭据呢?

将我们传入的密码从 "password" 改为其他内容,比如说 "assword"(但是妈妈,我只是把字母 "p" 去掉了啊??!!)

无效身份验证后的客户端应用程序

console-output-client-application-invalid-authentication

相应地,我们收到了一个错误,指示我们提供了无效的授权许可。

现在让我们实现客户端的其余部分,并尝试一些对我们 API 本身的调用。

使用经过身份验证的 API 调用实现 API 客户端

现在,我们将添加一个更新版本的CompanyClient类。在这种情况下,我们将所有内容都设为async。此外,我们还更新了类本身以及所有方法,以适应我们在 API 中引入的新的身份验证要求。

经过大量修改的 CompanyClient 类
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
 
// Add Usings:
using System.Net.Http;
using System.Net;
using System.Net.Http.Headers;
 
// Add for Identity/Token Deserialization:
using Newtonsoft.Json;
 
 
namespace MinimalOwinWebApiClient
{
    public class CompanyClient
    {
        string _accessToken;
        Uri _baseRequestUri;
        public CompanyClient(Uri baseUri, string accessToken)
        {
            _accessToken = accessToken;
            _baseRequestUri = new Uri(baseUri, "api/companies/");
        }
 
 
        // Handy helper method to set the access token for each request:
        void SetClientAuthentication(HttpClient client)
        {
            client.DefaultRequestHeaders.Authorization 
                = new AuthenticationHeaderValue("Bearer", _accessToken); 
        }
 
 
        public async Task<IEnumerable<Company>> GetCompaniesAsync()
        {
            HttpResponseMessage response;
            using(var client = new HttpClient())
            {
                SetClientAuthentication(client);
                response = await client.GetAsync(_baseRequestUri);
            }
            return await response.Content.ReadAsAsync<IEnumerable<Company>>();
        }
 
 
        public async Task<Company> GetCompanyAsync(int id)
        {
            HttpResponseMessage response;
            using (var client = new HttpClient())
            {
                SetClientAuthentication(client);
 
                // Combine base address URI and ID to new URI
                // that looks like http://hosturl/api/companies/id
                response = await client.GetAsync(
                    new Uri(_baseRequestUri, id.ToString()));
            }
            var result = await response.Content.ReadAsAsync<Company>();
            return result;
        }
 
 
        public async Task<HttpStatusCode> AddCompanyAsync(Company company)
        {
            HttpResponseMessage response;
            using(var client = new HttpClient())
            {
                SetClientAuthentication(client);
                response = await client.PostAsJsonAsync(
                    _baseRequestUri, company);
            }
            return response.StatusCode;
        }
 
 
        public async Task<HttpStatusCode> UpdateCompanyAsync(Company company)
        {
            HttpResponseMessage response;
            using (var client = new HttpClient())
            {
                SetClientAuthentication(client);
                response = await client.PutAsJsonAsync(
                    _baseRequestUri, company);
            }
            return response.StatusCode;
        }
 
 
        public async Task<HttpStatusCode> DeleteCompanyAsync(int id)
        {
            HttpResponseMessage response;
            using (var client = new HttpClient())
            {
                SetClientAuthentication(client);
 
                // Combine base address URI and ID to new URI
                // that looks like http://hosturl/api/companies/id
                response = await client.DeleteAsync(
                    new Uri(_baseRequestUri, id.ToString()));
            }
            return response.StatusCode;
        }
    }
}

现在,我们可以更新我们的程序类,以调用CompanyClient来与我们的 API 交互,并将结果输出到控制台。基本上,我们将扩展Run()方法,并异步地执行我们在CompaniesController上定义的每个方法。我们还添加了两个方便的方法用于写入控制台,WriteCompaniesList()和 WriteStatusCodeResult()。 :

更新 Program 类以消费 API 并写入控制台
static async Task Run()
{
    // Create an http client provider:
    string hostUriString = "https://:8080";
    var provider = new apiClientProvider(hostUriString);
    string _accessToken;
    Dictionary<string, string> _tokenDictionary;
 
    try
    {
        // Pass in the credentials and retrieve a token dictionary:
        _tokenDictionary = 
            await provider.GetTokenDictionary("john@example.com", "password");
        _accessToken = _tokenDictionary["access_token"];
 
        // Write the contents of the dictionary:
        foreach (var kvp in _tokenDictionary)
        {
            Console.WriteLine("{0}: {1}", kvp.Key, kvp.Value);
            Console.WriteLine("");
        }
 
        // Create a company client instance:
        var baseUri = new Uri(hostUriString);
        var companyClient = new CompanyClient(baseUri, _accessToken);
 
        // Read initial companies:
        Console.WriteLine("Read all the companies...");
        var companies = await companyClient.GetCompaniesAsync();
        WriteCompaniesList(companies);
 
        int nextId = (from c in companies select c.Id).Max() + 1;
 
        Console.WriteLine("Add a new company...");
        var result = await companyClient.AddCompanyAsync(
            new Company { Name = string.Format("New Company #{0}", nextId) });
        WriteStatusCodeResult(result);
 
        Console.WriteLine("Updated List after Add:");
        companies = await companyClient.GetCompaniesAsync();
        WriteCompaniesList(companies);
 
        Console.WriteLine("Update a company...");
        var updateMe = await companyClient.GetCompanyAsync(nextId);
        updateMe.Name = string.Format("Updated company #{0}", updateMe.Id);
        result = await companyClient.UpdateCompanyAsync(updateMe);
        WriteStatusCodeResult(result);
 
        Console.WriteLine("Updated List after Update:");
        companies = await companyClient.GetCompaniesAsync();
        WriteCompaniesList(companies);
 
        Console.WriteLine("Delete a company...");
        result = await companyClient.DeleteCompanyAsync(nextId - 1);
        WriteStatusCodeResult(result);
 
        Console.WriteLine("Updated List after Delete:");
        companies = await companyClient.GetCompaniesAsync();
        WriteCompaniesList(companies);
    }
    catch (AggregateException ex)
    {
        // If it's an aggregate exception, an async error occurred:
        Console.WriteLine(ex.InnerExceptions[0].Message);
        Console.WriteLine("Press the Enter key to Exit...");
        Console.ReadLine();
        return;
    }
    catch (Exception ex)
    {
        // Something else happened:
        Console.WriteLine(ex.Message);
        Console.WriteLine("Press the Enter key to Exit...");
        Console.ReadLine();
        return;
    }
}
 
 
static void WriteCompaniesList(IEnumerable<Company> companies)
{
    foreach (var company in companies)
    {
        Console.WriteLine("Id: {0} Name: {1}", company.Id, company.Name);
    }
    Console.WriteLine("");
}
 
static void WriteStatusCodeResult(System.Net.HttpStatusCode statusCode)
{
    if (statusCode == System.Net.HttpStatusCode.OK)
    {
        Console.WriteLine("Opreation Succeeded - status code {0}", statusCode);
    }
    else
    {
        Console.WriteLine("Opreation Failed - status code {0}", statusCode);
    }
    Console.WriteLine("");
}

既然我们能够正确地对发送到 Web Api 的请求进行身份验证,那么我们应该能够防止未经授权的访问了,对吧?

没那么快。

使用 [Authorize] 特性保护资源

如果我们现在启动我们的 Web Api 应用程序,打开一个浏览器,并输入路由到GetCompanies()上的CompaniesController方法的 URL,我们会发现我们仍然可以访问该资源,尽管来自浏览器的请求不包含任何身份验证令牌。

未经身份验证从浏览器访问 Companies 资源

access-unprotected-resource-from-browser

这是因为我们没有指定由CompaniesController所代表的资源应该受到保护。我们可以通过用一个CompaniesController类本身来轻松解决这个问题。[Authorize]attribute

用 [Authorize] 特性修饰 CompaniesController
[Authorize]
public class CompaniesController : ApiController
{
    // ... Code for Companies Controller ...
}

如果我们现在重新运行 Web Api 应用程序,并刷新我们的浏览器,我们会发现:

未经身份验证从浏览器访问受保护的 Companies 资源

access-protected-resource-from-browser

由于浏览器请求的请求体中没有访问令牌,因此对受保护资源的请求被拒绝了。

使用经过身份验证的客户端请求访问受保护的资源

现在,我们应该能够运行我们的 API 客户端应用程序了(别忘了把密码重置为 "password!")。如果我们现在运行客户端应用程序,我们应该会看到类似以下的控制台输出:

对受保护资源进行身份验证请求后的控制台输出

console-output-client-application-with-authenticated-api-calls

至此,我们已经实现了一个非常基础的示例,通过我们嵌入的授权服务器对用户进行身份验证,从我们的客户端应用程序检索访问令牌,并成功请求访问资源服务器上的受保护资源。

将角色添加为声明

深入探讨基于声明的授权超出了本文的范围。但是,我们可以使用[Authorize]特性来确保只有具有特定角色声明的用户才能访问受保护的资源。

更改[Authorize]特性在CompanyController类上的应用改为以下内容:

在 Company Controller 上的 [Authorize] 特性中添加一个特定的角色
[Authorize(Roles="Admin")]
public class CompaniesController : ApiController
{
    // ... Code for Companies Controller ...
}

如果我们现在运行 Web Api 应用程序,然后再运行 Api 客户端应用程序,我们会发现一个问题:

当需要角色授权时运行 Api 客户端

api-error-unauthorized-with-role-required

鉴于我们已经为访问CompaniesController资源添加了角色限制,这是我们预期的结果。现在让我们看看如何在我们的 Web Api 中根据角色成员资格来授权访问。

向资源所有者身份添加角色声明

在最简单的层面上,我们可以在调用GrantResourceOwnerCredentials():

时,向授予资源所有者的访问令牌添加一个声明。
public override async Task GrantResourceOwnerCredentials(
    OAuthGrantResourceOwnerCredentialsContext context)
{
    // DEMO ONLY: Pretend we are doing some sort of REAL checking here:
    if (context.Password != "password")
    {
        context.SetError(
            "invalid_grant", "The user name or password is incorrect.");
        context.Rejected();
        return;
    }
 
    // Create or retrieve a ClaimsIdentity to represent the 
    // Authenticated user:
    ClaimsIdentity identity = 
        new ClaimsIdentity(context.Options.AuthenticationType);
    identity.AddClaim(new Claim("user_name", context.UserName));
 
    // Add a Role Claim:
    identity.AddClaim(new Claim(ClaimTypes.Role, "Admin"));
   
    // Identity info will ultimatly be encoded into an Access Token
    // as a result of this call:
    context.Validated(identity);
}

在 GrantResourceOwnerCredentials() 中为已验证的用户添加一个角色声明

通过这个简单的改变,我们现在已经向已验证用户的身份中添加了一个声明。这些声明将作为访问令牌的一部分被编码/加密。当令牌被资源服务器(在我们的例子中,就是我们的应用程序)接收时,解码后的令牌将提供已验证用户的身份,以及任何额外的声明,包括用户是“Admin”角色的成员这一事实。

如果我们现在运行这两个应用程序,Api 客户端应用程序的控制台输出将如我们所预期的那样:

api-successful-access-with-role-required

来自具有正确管理员角色声明的已验证用户的客户端控制台输出CompaniesController我们再次成功访问了一个受保护的资源。现在,对

What Next?

到目前为止,我们已经以一种非常基本的方式了解了资源所有者密码凭据授予(Resource Owner Flow)在 OWIN/Katana 管道的上下文中是如何实现的。我们还没有研究我们可能将用户信息存储在哪里,如何将其存入,或者我们的授权框架如何访问这些数据。

在下一篇文章中,我们将探讨持久化授权信息以及如何访问它。

下一篇:ASP.NET Web Api:OWIN/Katana 身份验证/授权第二部分:模型和持久化

其他资源和感兴趣的项目

在学习这些东西时,我参考了一些非常有帮助的文章

 
 
 

© . All rights reserved.