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

使用 OAuth2 客户端凭据保护的 ASP.NET Core WebAPI

starIconstarIconstarIconstarIconstarIcon

5.00/5 (10投票s)

2017 年 6 月 11 日

CPOL

10分钟阅读

viewsIcon

143953

downloadIcon

4028

使用 JWTToken 访问利用 IdentityServer4 / OAuth2 的 .NET Core Web API。

引言

本文的范围是分享一个已保护的 WebAPI 的可能实现,该 WebAPI 能够解码和验证从 OAuth2 授权服务器发出的令牌。该示例展示了如何使用 .NET Core 1.1 创建 Web 服务,如何发布一个可以使用 JWT 令牌访问的端点,以及如何为已保护的端点验证该令牌。

背景

本文与我之前的文章“使用 Identity Server 和 .NET Core 的简单 OAuth2 授权服务器”有关,该文章展示了如何创建符合 OAuth2 授权框架的授权服务器来颁发令牌。

但是,可以使用任何授权服务器来获取令牌并进行验证,而无需对已保护的 Web API 进行重大更改。目前,此扩展不包含在本文中。

逐步创建已保护的 WebAPI 服务

创建空的 WebAPI 项目

对于此 WebAPI,我们将使用 Visual Studio 2017 和 .NET Core 1.1。
我们首先打开 VS2017 并选择文件 -> 新建 -> 新建项目。然后选择一个.NET Core项目,如下所示

给项目起您喜欢的名字。我这里使用的是ProtectedWebAPI。

按“确定”后,在下一个屏幕中,请确保您使用的是.NET Core 1.1。选择 WebAPI 模板,然后再次按“确定”。

为 WebAPI 服务添加一个开放(未受保护)的欢迎页面

这不是必需的步骤。但就我个人而言,当我在调试器中按下“播放”按钮并看到某些东西运行时,我喜欢它,这让我觉得我的代码正在工作。

因此,在此部分,我将向我们的 WebAPI 添加一个“未受保护”的欢迎页面。对于此页面,我们不需要任何令牌,并且当用户通过浏览器访问我们的 WebAPI 服务时,它将成为默认页面。

为此,我需要添加一个控制器、一个视图以及 MVC 库。我将为不熟悉 ASP.NET MVC 框架的人详细介绍必要的步骤。

添加 MVC 控制器

  • Controllers文件夹中,添加一个名为HomeController的新控制器。
  • 添加新控制器后,VS 会询问要向项目添加哪些依赖项。目前您可以选择最小依赖项
  • 在我写这篇文章的时候,添加依赖项后,我需要再次添加控制器。我认为这是 Visual Studio UI 的一个小错误……
  • 现在,添加控制器时,VS 会询问使用哪个脚手架。选择MVC 控制器 - 空
  • 最后,选择HomeController名称。现在创建了一个可以管理Index.cshtml页面的基本控制器。

添加 MVC 视图

  • 再次,在 VS2017 中,右键单击项目并选择添加新文件夹
  • 将其命名为Views不要更改!这是 Asp.net MVC 的默认选项!)。
  • 现在,在此Views文件夹内,添加另一个名为Home的文件夹。
  • 现在右键单击Home文件夹,然后选择添加 -> 新项 -> MVC 视图页(ASP.NET Core)
  • 默认情况下,视图的名称是Index.cshtml,这正是我们想要的。按添加按钮以添加新视图。

编写视图的 HTML 内容

更改Index.cshtml中的代码,如下所示,以创建一个欢迎消息

<html>
<head>
<title>Protected WebAPI. Welcome page.</title>
</head>
<body>
<h1>Protected WebAPI. Welcome page.</h1> 
</body> 
</html>

配置 Startup 类

Startup.cs中设置正确的路由,以便能够浏览网站

public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
        {
            loggerFactory.AddConsole(Configuration.GetSection("Logging"));
            loggerFactory.AddDebug();

            app.UseMvc(routes =>
            {
                routes.MapRoute(
                    name: "default",
                    template: "{controller=Home}/{action=Index}/{id?}");
            });
        }

现在浏览ProtectedWebAPI -> Properties -> LaunchSettings.json文件,并从文件中可见的每个部分中删除包含launchUrl属性的行。

"profiles": {
    "IIS Express": {
      "commandName": "IISExpress",
      "launchBrowser": true,
      "launchUrl": "api/values",  //REMOVE THIS ONE
      "environmentVariables": {
        "ASPNETCORE_ENVIRONMENT": "Development"
      }
    },
    "ProtectedWebAPI": {
      "commandName": "Project",
      "launchBrowser": true,
      "launchUrl": "api/values",  //REMOVE THIS ONE
      "environmentVariables": {
        "ASPNETCORE_ENVIRONMENT": "Development"
      },
      "applicationUrl": "https://:56087"
    }

没有此属性,Visual Studio 将启动主页,遵循默认路径,并将自动指向我们刚刚创建的网页。

启动项目以检查其是否正常工作……

……并享受第一步的成就!

警告:有些人报告在 VS2017 中存在一些问题,与使用 IISExpress 启动 WebAPI 项目有关。如果您是其中之一,您可以通过在“启动项目”按钮旁边的配置文件中,从IISExpress切换到ProtectedWebAPI 来解决此问题,如下图所示

保护 WebAPI 端点

现在我们的项目已经全部设置好,并且我们还有一个很棒的欢迎页面,我们可以创建一个端点,然后对其进行保护,这样如果缺少正确的 OAuth2 令牌,它就不会接受任何请求!

首先,创建一个返回某些值的开放端点

这是一个很容易实现的步骤,VS WebAPI 模板已经为我们创建了一个基本的开放端点!您可以浏览项目到 ProtectedWebAPI -> Controllers -> ValuesController。这是您会找到的代码

namespace ProtectedWebAPI.Controllers
{
    [Route("api/[controller]")]
    public class ValuesController : Controller
    {
        // GET api/values
        [HttpGet]
        public IEnumerable<string> Get()
        {
            return new string[] { "value1", "value2" };
        }

        // GET api/values/5
        [HttpGet("{id}")]
        public string Get(int id)
        {
            return "value";
        }

        // POST api/values
        [HttpPost]
        public void Post([FromBody]string value)
        {
        }

        // PUT api/values/5
        [HttpPut("{id}")]
        public void Put(int id, [FromBody]string value)
        {
        }

        // DELETE api/values/5
        [HttpDelete("{id}")]
        public void Delete(int id)
        {
        }
    }
}
</string>

这段代码提供了一个端点,它

  • 响应地址https://:56087/api/values (好的,端口实际取决于 LaunchSettings.json 中的 applicationUrl 属性)
  • 可以用浏览器浏览并返回字符串列表
  • 可以接受其他请求,使用不同的 HTTP 动词(POST、PUT、DELETE),但这些目前超出了本文的范围。

如果我们尝试浏览地址https://:56087/api/values ,我们将看到(取决于浏览器)我们收到一个名为 values.json 的文件,其中包含 API 调用返回的值

这意味着我们的端点目前正在响应所有匿名请求。它是一个开放的端点。

当然,我们可以更改我们返回给用户请求的值的类型,但这也不是本文的重点。我们的示例端点返回一个字符串列表,但它可以是任何内容。

添加管理令牌验证所需的中间件

使用 Nuget 包管理器(或您喜欢的任何方式),添加以下程序包

  • IdentityServer4.AccessTokenValidation 

到项目中

现在,在Startup.cs文件中,将必要的配置调用添加到Configure()方法中的UseIdentityServerAuthentication(..) 
请记住,始终确保代码app.UseMvc(...) 是 Configure 方法中的最后一行。

public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
        {
            //more code....

            //add this configuration for the middleware needed to validate the tokens
            app.UseIdentityServerAuthentication(new IdentityServerAuthenticationOptions
            {
                Authority = "https://:50151",
                RequireHttpsMetadata = false,
                ApiName = "scope.readaccess"
            });

            //more code....and especially app.UseMvc(...)
        }   

保护此端点,使其不接受 匿名请求

在保护本示例中的端点之前,配置我们的 WebApi 以便在出现问题时显示错误代码会很有用。这将帮助我们更好地理解示例。

Startup.Cs中添加指令app.UseStatusCodePages()

        public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
        {
            //more code...

            //add this line to show the ERROR STATUS CODES
            app.UseStatusCodePages();

            //more code...
        }    

现在,将[Authorize]属性添加到ValuesController.cs类中。
这是激活 ASP.NET MVC 中嵌入的安全功能的关键步骤。

namespace ProtectedWebAPI.Controllers
{
    [Route("api/[controller]")]
    [Authorize] // THIS IS THE KEY ATTRIBUTE TO ACTIVATE SECURITY FEATURES IN OUR WEBAPI
    public class ValuesController : Controller
    {
       //some code that I cut in this snippet
    }
}

如果您想了解更多信息,我发现这篇文章非常有用,它清晰地解释了 .NET Core 中 JWT 身份验证中间件的基础知识。

在我们的已保护 WebAPI 中添加基本的 Scope 验证

在上一篇文章中,当我们创建授权服务器时,我们还创建了两个不同的范围:“scope.readaccess”和“scope.fullaccess”。现在,在本文中,当我们访问已保护的 API 时,我们传递了范围“scope.readaccess”。验证正确的范围超出了本文的范围。有许多技术可以检查令牌,分析范围并适当地在我们保护的 API 中做出反应。

与往常一样,IdentityServer 项目的开发者已经为我们提供了一些很好的提示。在这篇文章中,Dominick Bayer 提供了各种技术。

然而,在本文中,为了完整起见,我提供了一种检查范围的可能方法。代码片段是您将放在ValuesController类中的内容,位于public IEnumerable<string> Get()方法内。该方法将如下所示

 // GET api/values
        [HttpGet]
        public IEnumerable<string> Get()
        {
            //this is a basic code snippet to validate the scope inside the API
            bool userHasRightScope = User.HasClaim("scope", "scope.readaccess");
            if (userHasRightScope == false)
            {
                throw new Exception("Invalid scope");
            }
            return new string[] { "value1", "value2" };
        }

测试已保护的端点

测试非常直接。再次启动您的 WebAPI 项目:欢迎页面出现。
但是现在再次尝试访问链接https://:56087/api/values。之前我们收到了一个包含 API 调用返回的值的 JSON 文件……但现在响应是清晰的

我们的 WebAPI 现在已受到保护。它不能再被匿名访问了。

创建一个可以访问我们 WebApi 的客户端

现在我们有了已保护的 WebAPI,我们如何访问它?在本节中,我们将创建一个客户端,它可以

  • 从 OAuth2 授权服务器请求访问令牌;
  • 使用令牌调用我们的 WebAPI 端点并获得授权;

整理授权服务器凭据

在我们开始编写客户端之前,我们需要知道我们要使用哪个授权服务器。在本文中,我将创建一个 Web API,该 API 已针对我在上一篇文章中构建的授权服务器进行了保护。

因此,我将使用该文章中为该特定客户端配置的凭据参数。这些凭据是

  • URLhttps://:50151/connect/token (授权服务器在我的同一台机器上本地运行,在该特定端口上响应)
  • ClientIdClientIdThatCanOnlyRead
  • ClientSecretsecret1
  • Scopescope.readaccess

请牢记这些参数。您会在客户端代码中找到它们硬编码,现在您知道它们来自哪里。

添加一个控制台项目

作为测试客户端,本教程中我们不需要任何特殊内容。因此,我只是添加了一个控制台客户端,它将请求令牌,然后访问我们的 WebAPI,将结果显示在控制台窗口中。

因此,让我们获取包含 WebAPI 项目的解决方案,并在其旁边添加一个新的文件 -> 添加 -> 新项目 -> 控制台应用(.NET Core)。 选择名称ConsoleTestClient,然后按“确定”添加它。

添加 IdentityModel Nuget 包

为了让我们的客户端能够处理请求并让我们的生活更轻松,请将以下程序包添加到控制台项目中

  • System.IdentityModel.Tokens.Jwt

添加此程序包时,Visual Studio 会拉入一些其他程序包。接受所有内容,因为它们是必需的引用。

请求 JWT Token

现在是时候编写一些代码了。我的控制台客户端只是一个Program.cs 文件。代码的第一部分向本文前面构建的授权服务器发出请求。在此示例中,我假设此服务器正在本地同一台机器上运行!

这是请求令牌的Program.cs代码片段。不要忘记添加 Visual Studio 会建议的using语句。

        static void Main(string[] args)
        {
            //authorization server parameters owned from the client
            //this values are issued from the authorization server to the client through a separate process (registration, etc...)
            Uri authorizationServerTokenIssuerUri = new Uri("https://:50151/connect/token");
            string clientId = "ClientIdThatCanOnlyRead";    
            string clientSecret = "secret1";
            string scope = "scope.readaccess";

            //access token request
            string rawJwtToken = RequestTokenToAuthorizationServer(
                 authorizationServerTokenIssuerUri,
                 clientId, 
                 scope, 
                 clientSecret)
                .GetAwaiter()
                .GetResult();

            //...some more code
        }

重要部分在RequestTokenToAuthorizationServer方法中。如您所见,它包含一个简单的 POST 请求,提交授权服务器分配给我们的客户端凭据。

        private static async Task<string> RequestTokenToAuthorizationServer(Uri uriAuthorizationServer, string clientId, string scope, string clientSecret)
        {
            HttpResponseMessage responseMessage;
            using (HttpClient client = new HttpClient())
            {
                HttpRequestMessage tokenRequest = new HttpRequestMessage(HttpMethod.Post, uriAuthorizationServer);
                HttpContent httpContent = new FormUrlEncodedContent(
                    new[]
                    {
                    new KeyValuePair<string, string>("grant_type", "client_credentials"),
                    new KeyValuePair<string, string>("client_id", clientId),
                    new KeyValuePair<string, string>("scope", scope),
                    new KeyValuePair<string, string>("client_secret", clientSecret)
                    });
                tokenRequest.Content = httpContent;
                responseMessage = await client.SendAsync(tokenRequest);
            }
            return await responseMessage.Content.ReadAsStringAsync();
        }

关于指定的 ClientId、ClientSecret 和 Scope

ClientId、ClientSecret 和 Scope 通常是调用者已知的。您作为 API 的用户,应该知道您的 ClientId、ClientSecret 以及这些凭据关联的范围。如果您尝试获取与这些凭据无关的范围的令牌,您将收到错误,因为这正是授权服务器在颁发令牌之前应该检查的内容,以避免给您错误的授权。

提供 JWT 令牌调用已保护的 WebAPI

现在我们有了令牌,我们可以使用它来执行对 WebAPI 的请求。这是完成工作的简单代码。此方法也在Program.Csmain 文件中调用。

 private static async Task<string> RequestValuesToSecuredWebApi(AuthorizationServerAnswer authorizationServerToken)
        {
            HttpResponseMessage responseMessage;
            using (HttpClient httpClient = new HttpClient())
            {
                httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", authorizationServerToken.access_token);
                HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, "https://:56087/api/values");
                responseMessage = await httpClient.SendAsync(request);
            }

            return await responseMessage.Content.ReadAsStringAsync();
        }

与通用 HttpRequest 的唯一重要区别是,我们添加了AuthorizationHeader。 此授权头携带令牌,并由 Web API 服务器中配置的授权中间件(在我们的例子中是 IdentityServer4)进行检查。

将所有内容一起运行

我们快到终点了。是时候将所有这些部分一起运行了。为了使示例正常工作,您还需要下载并运行我在此文章中构建的授权服务器

如果您已准备好所有内容,请按顺序运行以下程序。当您熟悉各个部分时,如果您从 VS2017 的单独实例启动每个项目,会更容易

  • 首先启动授权服务器。
  • 然后启动 ProtectedWebAPI 服务器。
  • 最后启动 ConsoleTestClient。查看控制台输出,您会读到来自 WebApi 服务器的响应。

享受所有辛勤工作的成果。

结论

 本文仅作为 OAuth2 框架下客户端凭据身份验证的入门教程。我们涉及了许多不同的方面:.NET Core 中的 HttpRequest/Responses 和 WebAPIs、Identity Server 中间件、JWT 令牌等……还有很多可以完成、配置和探索的事情。

再次,我的目标只是与其他读者分享我对这些概念的理解以及一些可能的实现方法,不一定是最好的方法。一如既往,任何评论、反馈或问题都将非常受欢迎,秉承社区精神,我们可以互相学习。

GITHub 上的代码

一如既往,本文附带代码的可下载版本,但如果由于某种原因您更喜欢 GitHub,这是公共存储库

历史

2017-06-11:第一版。

© . All rights reserved.