Skip to content

Latest commit

 

History

History
1088 lines (869 loc) · 48.8 KB

File metadata and controls

1088 lines (869 loc) · 48.8 KB

三、用户注册及管理

我们在上一章中构建了应用的基础,在那里我们还详细探索了 HTTP 谓词,同时在 ASP 中创建控制器.NET Core Web API。

现在,我们正逐渐转向 API 的一个重要方面,即身份验证。 由于 API 易于访问,身份验证肯定是必需的组件。 限制请求并对其进行限制可以防止恶意攻击。

应用的用户(在我们的例子中是客户)需要一个注册表单/界面,系统可以获取他们的详细信息。 我们将看到如何使用 API 注册用户。

在您注册并获得客户的所有详细信息后,如电子邮件密码,您就可以很容易地识别来自客户的请求。 等等,这很简单,但是我们需要遵循一些原则来验证用户以访问我们的资源。 这就是基本身份验证OAuth 身份验证的作用。

本章将涵盖以下主题:

  • 为什么要进行身份验证和限制请求?
  • 使用 EF Core 引导我们的 REST API
  • 向 REST API 添加基本身份验证
  • 向我们的服务添加 Oauth 2.0 身份验证
  • 定义基于客户机的 API 使用体系结构

为什么要进行身份验证和限制请求?

如果我告诉您某个特定国家的政府公开了一个 Web API,您可以使用它来获取其公民的所有详细信息,那么您首先会问我是否可以从该 API 中提取数据。 这正是我们将要讨论的。

所以,如果你把前面的例子,从 API 返回的数据将公民的敏感数据,如*,地址电话号码,国家和社会安全号码。 政府不应该允许所有人访问这些数据。 通常只允许认证的源。 这意味着当您调用一个 API 时,您需要发送您的身份并请求它允许您操作数据。 如果标识错误或不在允许的源列表中,API 将拒绝该标识。 想象恐怖分子试图访问 API,您肯定会通过检测他们的身份来拒绝访问。*

*现在想象另一个场景,大学有一个 API,它发送特定课程的特定学期的结果。 许多其他网站会通过调用这个大学 API 在他们的网站上显示结果。 黑客使用代码块在循环中调用 API。 如果时间间隔太小,那么如果您收到服务器繁忙/服务器不可达消息,也不要感到惊讶。 这是因为,在短时间内处理大量请求时,服务器会过载并耗尽资源。

这就需要对 API 进行限制,不允许同一源在特定时间间隔内发出更多请求。 例如,如果有任何消费者访问我们的 API,如果消费者在过去 10 秒左右已经请求过该请求,我们将不允许该请求。

首先,在探讨其他概念之前,让我们先设计应用的数据库。

数据库设计

我们肯定会有一个Customers桌。 我们将在该表中存储客户信息,并使用该表的主键作为其他表中的引用,如OrdersCart

客户表可以设计如下。 你可以在这本书中找到名为FlixOneStore.sql的数据库脚本:

CRUD 操作将通过 API 在这些表上执行。 让我们从 API 对该表执行一些操作开始。 更准确地说,我们讨论的是客户注册和登录过程。

用户注册

让我们先将模型放入 API 中,这样我们就可以创建对象并在数据库中保存数据。 我们将使用Entity Framework Core(EF Core)版本 2.0.2。

使用 API 设置 EF

要使用 EF Core,需要安装以下包,可以从工具内部的 NuGet 包管理器下载安装:

此外,我们还需要另一个名为 Microsoft.EntityFrameworkCore.Tools 的包。 这将帮助我们从数据库中创建模型类:

现在,我们到了需要根据数据库表建模类的地方。 下面的 powershell 命令可以在包管理器控制台上执行,以创建Customers表的模型类:

Scaffold-DbContext "Server=.;Database=FlixOneStore;Trusted_Connection=True;" Microsoft.EntityFrameworkCore.SqlServer -OutputDir Models -Tables Customers 

我们在命令中提供了连接字符串,以便它连接到我们的数据库。

下面是我们刚刚讨论过的命令的两个重要部分:

  • -OutputDir Models:定义模型类将放置的文件夹。
  • -Tables Customers:定义将被提取为模型类的表。 我们现在正在处理客户

在执行之后,您将在Models文件夹中看到两个文件Customers.csFixOneStoreContext.csCustomers.cs文件如下所示:

using System;
namespace DemoECommerceApp.Models
{
  public partial class Customers
  {
    public Guid Id { get; set; }
    public string Gender { get; set; }
    public string Firstname { get; set; }
    public string Lastname { get; set; }
    public DateTime Dob { get; set; }
    public string Email { get; set; }
    public Guid? Mainaddressid { get; set; }
    public string Telephone { get; set; }
    public string Fax { get; set; }
    public string Password { get; set; }
    public bool Newsletteropted { get; set; }
  }
}

Configuring DbContext

可以在具有OnConfiguringOnModelCreating方法的同一个文件夹中找到context类,该方法具有Customers的属性。

下面的代码块显示了FlixOneStoreContext类:

using Microsoft.EntityFrameworkCore;
namespace DemoECommerceApp.Models
{
  public partial class FlixOneStoreContext : DbContext
  {
    public virtual DbSet<Customers> Customers { get; set; }
    public FlixOneStoreContext(DbContextOptions<
    FlixOneStoreContext> options)
    : base(options)
    { }
    // Code is commented below, because we are applying
    dependency injection inside startup.
    // protected override void OnConfiguring(
    DbContextOptionsBuilder optionsBuilder)
    // {
    // if (!optionsBuilder.IsConfigured)
    // {
    //#warning To protect potentially sensitive information
    in your connection string, you should move it out of 
    source code. See http://go.microsoft.com/fwlink/?LinkId=723263 
    for guidance on storing connection strings.
    // optionsBuilder.UseSqlServer(@"Server=.;
    Database=FlixOneStore;Trusted_Connection=True;");
    // }
    // }
    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
      modelBuilder.Entity<Customers>(entity =>
      {
        entity.Property(e => e.Id)
        .HasColumnName("id")
        .ValueGeneratedNever();
        entity.Property(e => e.Dob)
        .HasColumnName("dob")
        .HasColumnType("datetime");
        entity.Property(e => e.Email)
        .IsRequired()
        .HasColumnName("email")
        .HasMaxLength(110);
        entity.Property(e => e.Fax)
        .IsRequired()
        .HasColumnName("fax")
        .HasMaxLength(50);
        entity.Property(e => e.Firstname)
        .IsRequired()
        .HasColumnName("firstname")
        .HasMaxLength(50);
        entity.Property(e => e.Gender)
        .IsRequired()
        .HasColumnName("gender")
        .HasColumnType("char(1)");
        entity.Property(e => e.Lastname)
        .IsRequired()
        .HasColumnName("lastname")
        .HasMaxLength(50);
        entity.Property(e => e.Mainaddressid).HasColumnName
        ("mainaddressid");
        entity.Property(e => e.Newsletteropted).HasColumnName
        ("newsletteropted");
        entity.Property(e => e.Password)
        .IsRequired()
        .HasColumnName("password")
        .HasMaxLength(50);
        entity.Property(e => e.Telephone)
        .IsRequired()
        .HasColumnName("telephone")
        .HasMaxLength(50);
      });
    }
  }
}

您是否注意到,我已经注释了OnConfiguring方法,并添加了一个构造函数,以便我们可以从启动时注入依赖项,用连接字符串初始化上下文? 做一下。

因此,在ConfigureServices启动过程中,我们将使用连接字符串将上下文添加到 services 集合中:

public void ConfigureServices(IServiceCollection services)
{
  services.AddSingleton<IProductService, ProductService>();
  services.AddMvc();
  var connection = @"Server=.;Database=FlixOneStore;
  Trusted_Connection=True";
  services.AddDbContext<FlixOneStoreContext>(
  options => options.UseSqlServer(connection));
}

生成控制器

下一步是添加控制器。 为此,请参考以下步骤:

  1. 右键单击Controller文件夹,然后单击 Add,然后单击 Controller。 你会在一个模态中看到创建不同类型控制器的选项:

  1. 选择带有动作的 API Controller,使用实体框架,然后单击 Add 按钮。 下面的截图显示了接下来发生的事情:

  1. 点击添加。瞧! 它完成了所有艰苦的工作,并使用 EF Core 创建了一个功能齐全的控制器,其中包含了所有主要的 HTTP 动词。 下面的代码块是仅使用GET方法的控制器的一个小快照。 我已经删除了其他节省空间的方法:
// Removed usings for brevity.
namespace DemoECommerceApp.Controllers
{
  [Produces("application/json")]
  [Route("api/Customers")]
  public class CustomersController : Controller
  {
    private readonly FlixOneStoreContext _context;
    public CustomersController(FlixOneStoreContext context)
    {
      _context = context;
    }
    // GET: api/Customers
    [HttpGet]
    public IEnumerable<Customers> GetCustomers()
    {
      return _context.Customers;
    }
    // GET: api/Customers/5
    [HttpGet("{id}")]
    public async Task<IActionResult> GetCustomers
    ([FromRoute] Guid id)
    {
      if (!ModelState.IsValid)
      {
        return BadRequest(ModelState);
      }
      var customers = await 
      _context.Customers.SingleOrDefaultAsync(m => m.Id == id);
      if (customers == null)
      {
        return NotFound();
      }
      return Ok(customers);
    }

    // You will also find PUT POST, DELETE methods.
    // These action methods are removed to save space.
  }
}

这里有几点需要注意:

  • 注意这里是如何通过将FlixOneStoreContext注入构造函数来初始化它的。 此外,它将用于所有操作中与数据库相关的操作:
private readonly FlixOneStoreContext _context;
public CustomersController(FlixOneStoreContext context)
{
  _context = context;
}
  • 接下来要关注的是用于从操作返回结果的方法。 查看如何使用BadRequest()NotFound()Ok()NoContent()来返回客户端容易理解的正确 HTTP 响应代码。 当我们调用这些操作来执行真正的任务时,我们将看到它们稍后返回的代码。

从页面调用 API 来注册客户

为了简化工作,我设计了一个简单的 HTML 页面,其中包含客户记录的控件,如下所示。 我们将输入数据并尝试调用 API 来保存记录:

<div class="container">
  <h2>Register for FlixOneStore</h2>
  <div class="form-horizontal">
    <div class="form-group">
      <label class="control-label col-sm-2" for=
      "txtFirstName">First Name:</label>
      <div class="col-sm-3">
        <input type="text" class="form-control" id=
        "txtFirstName" placeholder=
        "Enter first name" name="firstname">
      </div>
    </div>
    <div class="form-group">
      <label class="control-label col-sm-2" for=
      "txtLastName">Last Name:</label>
      <div class="col-sm-3">
        <input type="text" class="form-control" id=
        "txtLastName" placeholder=
        "Enter last name" name="lastname">
      </div>
    </div>
    <div class="form-group">
      <label class="control-label col-sm-2" for="txtEmail">
      Email:</label>
      <div class="col-sm-3">
        <input type="email" class="form-control" id=
        "txtEmail" placeholder=
        "Enter email" name="email">
      </div>
    </div>
    <div class="form-group">
      <label class="control-label col-sm-2" for="gender">
      Gender:</label>
      <div class="col-sm-3">
        <label class="radio-inline"><input type="radio" 
        value="M" name="gender">Male</label>
        <lable class="radio-inline"><input type="radio" 
        value="F" name="gender">Female</lable>
      </div>
    </div>
    <div class="form-group">
      <label class="control-label col-sm-2" for="txtDob">
      Date of Birth:</label>
      <div class="col-sm-3">
        <input type="date" class="form-control" id="txtDob" />
      </div>
    </div>
    <div class="form-group">
      <label class="control-label col-sm-2" for="txtMobile">
      Mobile Number:</label>
      <div class="col-sm-3">
        <input type="text" class="form-control" id="txtMobile"
        placeholder=
        "Enter mobile number" />
      </div>
    </div>
    <div class="form-group">
      <label class="control-label col-sm-2" for="txtFax">Fax:</label>
      <div class="col-sm-3">
        <input type="text" class="form-control" id="txtFax"
        placeholder="Enter fax" />
      </div>
    </div>
    <div class="form-group">
      <label class="control-label col-sm-2" for="txtPassword">
      Password:</label>
      <div class="col-sm-3">
        <input type="password" class="form-control" id=
        "txtPassword" placeholder=
        "Enter password" name="pwd">
      </div>
    </div>
    <div class="form-group">
      <label class="control-label col-sm-2" for="txtConfirmPassword">
      Confirm Password:</label>
      <div class="col-sm-3">
        <input type="password" class="form-control"
        id="txtConfirmPassword" placeholder=
        "Enter password again" name="confirmpwd">
      </div>
    </div>
    <div class="form-group">
      <div class="col-sm-offset-2 col-sm-10">
        <button type="button" class="btn btn-success"
        id="btnRegister">Register</button>
      </div>
    </div>
  </div>
</div>

我已经使用 bootstrap 和jQuery与我的代码。 您可以参考本书附件中的整个代码或参考https://github.com/PacktPublishing/Building-RESTful-Web-services-with-DOTNET-Core

现在是代码的重要部分,在这里我们将调用 API 来存储客户记录。 参考以下代码块:

$(document).ready(function () {
  $('#btnRegister').click(function () {
    // Check password and confirm password.
    var password = $('#txtPassword').val(),
    confirmPassword = $('#txtConfirmPassword').val();
    if (password !== confirmPassword) {
      alert("Password and Confirm Password don't match!");
      return;
    }

    // Make a customer object.
    var customer = {
      "gender": $("input[name='gender']:checked").val(),
      "firstname": $('#txtFirstName').val(),
      "lastname": $('#txtLastName').val(),
      "dob": $('#txtDob').val(),
      "email": $('#txtEmail').val(),
      "telephone": $('#txtMobile').val(),
      "fax": $('#txtFax').val(),
      "password": $('#txtPassword').val(),
      "newsletteropted": false
    };

    $.ajax({
      url: 'http://localhost:57571/api/Customers',
      type: "POST",
      contentType: "application/json",
      data: JSON.stringify(customer),
      dataType: "json",
      success: function (result) {
        alert("A customer record created for: "
        + result.firstname + " " + result.lastname);
      },
      error: function (err) {
        alert(err.responseText);
      }
    });
  });
});

注意http://localhost:57571/api/CustomersURL 和POSTHTTP 方法。 这最终会调用 API 中名为PostCustomersPost方法。 我们在表中肯定会有一些唯一性,在我们的例子中,我将电子邮件作为每个记录的唯一性。 这就是为什么我需要修改一下action方法:

// POST: api/Customers
[HttpPost]
public async Task<IActionResult> PostCustomers([FromBody] Customers customers)
{
  if (!ModelState.IsValid)
  {
    return BadRequest(ModelState);
  }
  // Unique mail id check.
 if (_context.Customers.Any(x => x.Email == customers.Email))
 {
 ModelState.AddModelError("email", "User with mail id already
    exists!");
 return BadRequest(ModelState);
 }
  _context.Customers.Add(customers);
  try
  {
    await _context.SaveChangesAsync();
  }
  catch (DbUpdateException ex)
  {
    if (CustomersExists(customers.Id))
    {
      return new StatusCodeResult(StatusCodes.Status409Conflict);
    }
    else
    {
      throw;
    }
  }
  return CreatedAtAction("GetCustomers", new { id = customers.Id },
  customers);
}

我通过为模型属性电子邮件添加一个错误消息来返回BadRequest()。 我们很快就会看到它是如何显示在浏览器上的!

以下从浏览器捕获的图像显示了成功创建的客户:

一个成功注册的客户看起来类似于前面的图像,它显示了在 ajax 调用的success方法中提示的成功消息。

您可以在完成时对从action方法接收到的数据执行任何您想要的操作,因为它返回整个customer对象。 如果你不相信我,参考下面的截图从源代码窗口的调试器工具:

The response to the POST request with the new Customer created inside the jQuery Ajax success method

那么,这是谁干的? 简单地说,下面的 return 语句在POST方法中完成了所有神奇的事情:

return CreatedAtAction("GetCustomers", new { id = customers.Id }, customers); 

这一行做了几件事:

  • 发送状态码:201 创建为POST动作成功创建资源。
  • 使用资源的实际 URL 设置 Location 头。 如果您记得 RESTful 特征,那么在POST操作之后,服务器应该发送资源的 URL。 这就是它的作用。

让我向您展示开发人员工具的网络选项卡,以证明我的观点。 您也可以使用Postman来分析它。 下面的截图显示了响应的详细信息:

The response received by a POST success request with Status Code and Location Header

Guid实际上是我们在数据库中的列类型中定义的Customer ID,我在Customer模型类构造函数中为其赋值。

现在,如果你复制这个 URL 并在你的浏览器或邮递员中打开它,你会得到客户的详细信息,如下截图所示:

让我们看一个带有已经存在的邮件 ID 的BadRequest()示例。 由于taditdash@gmail.com客户已经存在,发送具有相同电子邮件 ID 的另一个请求应该会向我们发送一个错误消息作为响应。 让我们来看看:

记住,我们添加了一行来检查电子邮件 ID 是否存在,并添加了一个ModelState错误。 现在已经在行动了。

For simplicity of the demo in this book, I am just saving a plain text password. You should not do that in the actual project. Implementing proper encryption for a password is a must.

至此,我将结束注册过程。 但是,在客户端和服务器端都有实现验证的余地。 您可以向Model类属性添加属性,使其更加可靠,这样您就不会从客户端获得错误的数据。 当ModelState验证失败时,发送BadRequest()响应。 可以将所需的电子邮件格式和密码比较属性添加到Model类中。

CORS

如果你在调用 API 动作时看到以下错误,那么你需要启用Cross Origin Resource Sharing(CORS):

Failed to load http://localhost:57571/api/Customers: Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource. Origin 'null' is therefore not allowed access. The response had HTTP status code 404.

要在所有起源中启用 CORS,请遵循前面显示的步骤:

  1. 安装Microsoft.AspNetCore.CorsNuGet 包:

  1. Startup ConfigureServices内部,添加以下代码来实现一个允许所有起源的 CORS 策略:
services.AddCors(options =>
{
  options.AddPolicy("AllowAll",
    builder =>
    {
      builder
      .AllowAnyOrigin()
      .AllowAnyMethod()
      .AllowAnyHeader();
    });
});
  1. Configure方法内部,在app.UseMvc();之前添加以下一行(这很重要):
app.UseCors("AllowAll");

现在,它应该像预期的那样工作。 如果您想更多地了解 CORS,请访问https://docs.microsoft.com/en-us/aspnet/core/security/cors?view=aspnetcore-2.1

向 REST API 添加基本身份验证

现在我们注册了客户,我们可以进入身份验证过程了。 身份验证是验证一个客户是否是我们网站的有效用户。 我们已经有了他们的证件,因为他们使用我们的注册表注册。 当他们试图访问任何资源从我们的网站使用这些凭证,我们将首先核实,然后允许。

所有人都可以注册,不会被认证。 然而,当客户**想读他们的背景资料删除账户,等等,然后我们需要身份验证,数据返回给用户实际受信任的应用的用户。

对于基本认证:

  • 当客户端请求资源时,我们将从客户端获得用户名,这将是电子邮件 ID密码。 这将与 HTTP 头一起发送。 当我们设计客户时,我们会看到这一点。
  • 然后,这些数据将从数据库中进行验证。
  • 如果找到,该操作将被允许,否则将发送一个401 Unauthorized响应。

步骤 1 -添加(authorize)属性

让我们限制返回客户概要细节的操作方法,即CustomersControllerGET方法,命名为GetCustomers([FromRoute] Guid id)

客户试图访问配置文件时,我们将验证以下两件事:

  • 请求来自应用的可信用户。 这意味着,请求来自具有有效的电子邮件密码客户
  • 客户只能访问他们的配置文件。 为了检查这一点,我们将使用 URL 上被请求的客户的ID 验证客户的凭据(存在于请求中)。

让我们开始吧。 记住,我们的目标是实现以下几点:

[Authorize(AuthenticationSchemes = "Basic")]
public async Task<IActionResult> GetCustomers([FromRoute] Guid id)

现在,我们将把注意力集中在这个动作方法上,以理解这个概念。 您可以在这里看到Authorize属性,其中AuthenticationScheme定义为Basic。 这意味着我们必须告诉运行时基本身份验证是什么,以便它在进入操作方法之前先执行该身份验证。

如果身份验证成功,将执行操作方法,否则将向客户端发送 401 Unauthorized 响应。

步骤 2 -设计 BasicAuthenticationOptions 和 BasicAuthenticationHandler

首先,我们需要一个类来派生Microsoft.AspNetCore.Authentication中的AuthenticationSchemeOptions类,如下代码块所示:

using Microsoft.AspNetCore.Authentication;
namespace DemoECommerceApp.Security.Authentication
{
  public class BasicAuthenticationOptions : AuthenticationSchemeOptions {}
}

为简单起见,它被留空,但可以用不同的属性加载它。 我们就不深入讨论了。

接下来,我们需要一个用于基本身份验证的处理程序,其中我们将有我们的实际逻辑:

public class BasicAuthenticationHandler : AuthenticationHandler<BasicAuthenticationOptions>

我们可以使用一个额外的DbContext参数作为它的类构造函数,因为我们将验证数据库中的客户详细信息:

public BasicAuthenticationHandler(IOptionsMonitor<BasicAuthenticationOptions> options, ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock, FlixOneStoreContext context)
: base(options, logger, encoder, clock)
{
  _context = context;
}

AuthenticationHandler<T>是一个抽象类,具有与身份验证相关的属性和方法。 目前,我们将覆盖两个方法HandleAuthenticateAsyncHandleChallengeAsyncHandleAuthenticateAsync将具有验证客户的实际逻辑,HandleChallengeAsync用于处理 401 个挑战问题,这意味着无论何时您决定客户无效,都可以用这种方法编写代码来处理这种情况。

我们假设将在 HTTP 头中接收电子邮件和密码,该头名为Authorization,由分隔符冒号(:)分隔。 下面是从 header 中提取数据并验证其是否正确的代码:

protected override Task<AuthenticateResult> HandleAuthenticateAsync()
{
  // 1\. Verify if AuthorizationHeaderName present in the header.
 // AuthorizationHeaderName is a string with value "Authorization".
  if (!Request.Headers.ContainsKey(AuthorizationHeaderName))
  {
    // Authorization header not found.
    return Task.FromResult(AuthenticateResult.NoResult());
  }
  // 2\. Verify if header is valid. 
  if (!AuthenticationHeaderValue.TryParse(Request.Headers
  [AuthorizationHeaderName], out AuthenticationHeaderValue 
  headerValue))
  {
    // Authorization header is not valid.
    return Task.FromResult(AuthenticateResult.NoResult());
  }
  // 3\. Verify is scheme name is Basic. BasicSchemeName is a string
 // with value 'Basic'. 
  if (!BasicSchemeName.Equals(headerValue.Scheme, StringComparison.
  OrdinalIgnoreCase))
  {
    // Authorization header is not Basic.
    return Task.FromResult(AuthenticateResult.NoResult());
  }
  // 4\. Fetch email and password from header.
 // If length is not 2, then authentication fails.
  byte[] headerValueBytes = Convert.FromBase64String(headerValue.
  Parameter);
  string emailPassword = Encoding.UTF8.GetString(headerValueBytes);
  string[] parts = emailPassword.Split(':');
  if (parts.Length != 2)
  {
    return Task.FromResult(AuthenticateResult.Fail("Invalid Basic
    Authentication Header"));
  }
  string email = parts[0];
  string password = parts[1];
  // 5\. Validate if email and password are correct.
  var customer = _context.Customers.SingleOrDefault(x => 
  x.Email == email && x.Password == password);
  if (customer == null)
  {
    return Task.FromResult(AuthenticateResult.Fail("Invalid email 
    and password."));
  }
  // 6\. Create claims with email and id.
  var claims = new[]
  { 
    new Claim(ClaimTypes.Name, email),
    new Claim(ClaimTypes.NameIdentifier, customer.Id.ToString())
  };
  // 7\. ClaimsIdentity creation with claims.
  var identity = new ClaimsIdentity(claims, Scheme.Name);
  var principal = new ClaimsPrincipal(identity);
  var ticket = new AuthenticationTicket(principal, Scheme.Name);
  return Task.FromResult(AuthenticateResult.Success(ticket));
}

代码非常容易理解。 我在代码块上添加了步骤。 基本上,我们试图验证头,然后处理它,看看它们是否正确。 如果正确,则创建ClaimsIdentity对象,该对象可以在应用中进一步使用。 我们将在下一节中进行。 在每个步骤中,如果验证失败,我们将发送AuthenticateResult.NoResult()AuthenticateResult.Fail()

让我们将这个身份验证附加到 action 方法上,使用如下方法:

[HttpGet("{id}")]
[Authorize(AuthenticationSchemes = "Basic")]
public async Task<IActionResult> GetCustomers([FromRoute] Guid id)
{
  if (!ModelState.IsValid)
  {
    return BadRequest(ModelState);
  }
  var ident = User.Identity as ClaimsIdentity;
 var currentLoggeedInUserId = ident.Claims.FirstOrDefault
  (c => c.Type == ClaimTypes.NameIdentifier)?.Value;
 if (currentLoggeedInUserId != id.ToString())
 {
 // Not Authorized
 return BadRequest("You are not authorized!");
 }
  var customers = await _context.Customers.SingleOrDefaultAsync
  (m => m.Id == id);
  if (customers == null)
  {
    return NotFound();
  }
  return Ok(customers);
}

步骤 3 -在启动时注册基本认证

看起来一切都设置好了,但是,我们在启动时漏掉了注册这个基本身份验证的步骤。 否则,如何调用BasicAuthenticationHandler处理程序? 看一看:

services.AddAuthentication("Basic")
.AddScheme<BasicAuthenticationOptions, BasicAuthenticationHandler>("Basic", null);
services.AddTransient<IAuthenticationHandler, BasicAuthenticationHandler>();

要测试 API,您可以设计一个 HTML 页面,通过使用Id从 API 获取详细信息来显示用户的概要文件。 你可以使用jQuery Ajax调用 API 并对接收到的结果进行操作:

$.ajax({
  url: 'http://localhost:57571/api/Customers/
  910D4C2F-B394-4578-8D9C-7CA3FD3266E2',
  type: "GET",
  contentType: "application/json",
  dataType: "json",
  headers: { 'Authorization': 'Basic ' + btoa
  (email + ':' + password)},
  success: function (result) {
    // Work with result. Code removed for brevity.
  },
  error: function (err) 
  {
    if (err.status == 401) 
    {
      alert("Either wrong email and password or you are 
      not authorized to access the data!")
    }
  }
});

注意头文件部分,其中提到了Authorization头文件,其中emailpassword用冒号(:)分隔,并传递给了btoa方法,该方法负责 Base64 加密。 在你得到结果后,你可以做n许多事情。 下面的截图显示了它在页面上使用一些 bootstrap 设计:

现在,有一个重要的代码块应该包含在前面的处理程序代码中。 这是另一个应该被覆盖的HandleChallengeAsync方法。 这种方法的目的是处理身份验证失败的情况。

我们将只发送一个名为WWW-Authenticate的响应头,其值可以用realm设置。 先看看代码,然后我将解释:

protected override async Task HandleChallengeAsync(AuthenticationProperties properties)
{
  Response.Headers["WWW-Authenticate"] = $"Basic 
  realm=\"http://localhost:57571\", charset=\"UTF-8\"";
  await base.HandleChallengeAsync(properties);
}

If a client tries to access a restricted resource or a resource that requires authentication, it's the server's responsibility to inform the client about the authentication type and related information. The WWW-Authenticate HTTP response header is set by the server that defines the authentication method that should be used to gain access to the restricted resource requested.

因此,很明显,WWW-Authenticate头与 401 Unauthorized 响应一起发送。 该字符串包含三个内容:身份验证类型领域字符。 域是身份验证将在其中有效的域或区域。

在我们的案例中,方案是Basic,Realm 是http://localhost:57571,Charset 是UTF-8。 因此,如果客户机提供的基本身份验证参数为UsernamePassword,那么这些参数在localhost:57571域中是有效的。

这就是它的意义。 因此,只需删除分配代码的头文件或将其注释出来以进行测试。 以下是 Chrome 开发工具的网络选项卡截图:

下面是一条警告消息的截图,我们在 Ajax 调用的错误方法中提供了它。 当 API 动作在没有任何凭据的情况下被调用时,就会发生这种情况:

向我们的服务添加 OAuth 2.0 身份验证

OAuth 是一个开放标准,api 使用它来控制客户端(如网站、桌面应用,甚至其他 api)对资源的访问。 然而,实现 OAuth 的 API 可以在不与第三方应用共享密码的情况下提供用户信息。

你一定见过一些网站,他们允许使用不同的服务登录,如 Facebook, Twitter,或谷歌,说的东西,如(针对 Facebook)登录 Facebook。 这意味着 Facebook 有一个 OAuth 服务器,它会通过你之前提供给 Facebook 的某个身份验证你的应用,并给你一个有一定有效性的访问令牌。 使用该令牌,您可以读取所需用户的配置文件。

以下是一些基本的 OAuth2.0 术语:

  • **资源:**我们已经在前面的章节中定义了这个。 资源是我们需要保护的东西。 这可能是任何与我们系统有关的信息。
  • **资源服务器:**该服务器将保护资源,主要是我们为访问电子商务数据库而设计的 API。
  • **资源所有者:**授予我们访问特定资源的人。 大多数用户都是所有者,正如你所看到的,当你点击登录 Facebook 时,它会征求你的登录和同意。
  • **客户端:**需要我们访问资源的应用。 在我们的例子中,当jQuery代码在我们设计的 HTML 页面上执行时,是浏览器试图访问资源。
  • **访问令牌:**这实际上是这个体系结构的支柱。 我们将要设计的 OAuth 服务器应该提供一个使用用户凭据的令牌,以便随后访问我们的资源,因为我们知道 OAuth 标准告诉我们不要向客户端提供密码。
  • **不记名令牌:**这是一种特殊类型的访问令牌,允许任何人轻松地使用令牌,这意味着,为了使用令牌访问资源,客户端不需要加密密钥或其他秘密密钥。 由于这比其他类型的令牌安全性差,无记名令牌只能在 HTTPs 上使用,并且应该在很短的时间内过期。
  • **授权服务器:**向客户端提供访问令牌的服务器。

让我们开始将 OAuth 添加到 Web API 中。 我们将使用 IdentityServer4,它是一个免费的、开源的 OpenID 连接和 OAuth 2.0 框架。 净的核心。 项目可以在这里找到:https://github.com/IdentityServer

IdentityServer(http://identityserver.io/)基于 OWIN/Katana,但据我们所知,它以 NuGet 包的形式发布和可用。 为了启动 IdentityServer,需要安装以下两个 NuGet 包:

The authorization server in a production scenario is ideally isolated from the main web API. But for this book, we will directly put that in the same Web API project for simplicity. We are not using the default ASP.NET Core Identity. We will be using our own set of tables. For instance, we will use our customer table details for verification.

步骤 1 -设计 Config 类

Config类包含授权服务器的重要细节,例如资源客户端用户。 在生成令牌时使用这些细节。 让我们设计:

public class Config
{
  public static IEnumerable<ApiResource> GetApiResources()
  {
    return new List<ApiResource>
    {
      new ApiResource
      (
        "FlixOneStore.ReadAccess",
        "FlixOneStore API", 
         new List<string> 
         {
           JwtClaimTypes.Id,
           JwtClaimTypes.Email,
           JwtClaimTypes.Name,
           JwtClaimTypes.GivenName,
           JwtClaimTypes.FamilyName
        }
      ),

      new ApiResource("FlixOneStore.FullAccess", "FlixOneStore API")
    }; 
  }
}

ApiResource用于声明 API 的不同作用域和声明。 对于简单的情况,一个 API 可能有一个简单的资源结构,在这个结构中它可以访问所有的客户机。 然而,在典型的场景中,客户机可以被限制访问 API 的不同部分。 在声明客户机时,我们将使用这些资源来配置它们的范围和访问权限。 ReadAccessFullAccess是两种不同的资源类型,客户机可以使用它们分别提供读访问和完全访问。

基本上,我们现在正在设计的方法将在Startup上调用。 这里,GetApiResources实际上是使用不同的设置创建两种类型的资源。 第一个是我们现在要处理的。 我们把它命名为FlixOneStore.ReadAccess。 您可以看到包含IdName等字符的字符串列表,这些是将用令牌生成并传递给客户的客户的详细信息。

让我们添加一个客户端的细节,我们将从那里使用授权服务器:

public static IEnumerable<Client> GetClients()
{
  return new[]
  {
    new Client
    {
      Enabled = true,
      ClientName = "HTML Page Client",
      ClientId = "htmlClient",
      AllowedGrantTypes = GrantTypes.ResourceOwnerPassword,

      ClientSecrets =
      {
        new Secret("secretpassword".Sha256())
      },

      AllowedScopes = { "FlixOneStore.ReadAccess" }
    }
  };
}

您可以根据需要添加许多客户机。 在该方法中,可以根据 OAuth 标准设置客户端 id客户端 secret、授权类型。 注意,密码设置为secretpassword。 你可以在这里设置任何字符串; 它可以是一个Guid。 这里,GrantType.ResourceOwnerPassword定义了验证传入请求以生成令牌的方式。

它对授权服务器说:“Hey 在请求体中查找usernamepassword。” 还有其他类型的奖助金。 您可以通过官方文档链接了解更多信息。

你现在可能有问题了! 我们将用usernamepassword做什么? 当然,我们会验证它们,但用什么呢? 答案是Customers表中的EmailPassword字段。 我们没有做任何与连接授权服务器Customers*表相关的事情。 这就是我们接下来要做的。 但在此之前,让我们在Startup中注册这些设置。

Just to make sure we are on the same page, we landed at the point where we are trying to generate a token from the Authorization Server in order to access our API.

步骤 2 -启动时注册 Config

对于注册,以下是我们在ConfigureServices方法里面要做的:

services.AddIdentityServer()
        .AddInMemoryApiResources(Config.GetApiResources())
        .AddInMemoryClients(Config.GetClients())
        .AddProfileService<ProfileService>()
        .AddDeveloperSigningCredential();

我们在这里通过调用我们设计的方法来加载所有的配置设置,例如资源客户端AddDeveloperSigningCredential在启动时添加一个临时密钥,仅在开发环境中,当我们没有任何证书来申请授权时使用。 您将为实际使用添加适当的证书细节。

在这里标记ProfileService。 这就是我在前一节中讨论的内容,它将用于根据数据库验证用户凭据。 我们一会儿会讲到。 首先,让我们测试 API,假设授权服务器已经设置好ProfileService

现在来看看 API,我们需要在 API 的开头添加AuthenticationScheme来声明我们将使用什么身份验证。 为此,添加以下代码:

services.AddAuthentication(options =>
{
  options.DefaultAuthenticateScheme = 
  JwtBearerDefaults.AuthenticationScheme;
  options.DefaultChallengeScheme = 
  JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(o =>
{
  o.Authority = "http://localhost:57571";
  o.Audience = "FlixOneStore.ReadAccess";
  o.RequireHttpsMetadata = false;
});

JwtBearerDefaults.AuthenticationScheme实际上是一个具有持钵值的字符串常量。 承载认证又称为令牌认证。 这意味着我们的客户端需要发送一个令牌来访问 API 的资源。 为了获得令牌,他们需要调用我们的授权服务器,该服务器在/connect/token可用。

注意,我们将Audience设置为FlixOneStore.ReadAccess,这是我们为客户端在内部配置中指定的。 简单地说,我们正在设置承载类型的身份验证。

步骤 3 -添加[Authorize]属性

下一步是将[Authorize]属性添加到 API 控制器操作中。 让我们用GetCustomers(id)方法来测试一下:

// GET: api/Customers/5
[HttpGet("{id}")]
[Authorize]
public async Task<IActionResult> GetCustomers([FromRoute] Guid id)

Postman调用它会产生以下输出:

所以,我们的请求不再被批准了。 我们收到一个回复,说我们需要发送一个令牌来访问资源。 那我们去拿代币吧。

步骤 4 -获取令牌

为了获得令牌,我们需要调用位于/connect/token的授权服务器。

以下是来自Postman的屏幕,其中在http://localhost:57571/connect/tokenURL 上执行一个POST请求,请求体包含了验证客户机所需的所有参数。 以下是我们在步骤 1中的GetClients()方法中注册的细节:

哦! 这是一个糟糕的请求。 这是因为我们给客户传递了错误的秘密密码。 如果您还记得,我们将其设置为secretpassword,但将其作为secret传递。 这就是为什么它被拒绝了。

这里需要注意一些重要的事情。 要获取令牌:

  • 我们需要向/connect/tokenURL 发送一个POST请求。 因为我们在同一个应用中实现了服务器,所以这里的域与 API 相同。
  • 我们需要将Content-Type标题设置为application/x-www-form-urlencoded(这实际上是在屏幕截图的不同标签中)。
  • 在请求的主体中,我们根据标准添加了 OAuth 所需的所有参数,它们与我们在配置类中拥有的完全匹配。

当我们按照要求正确发送一切时,我们将收到一个令牌,如下截图所示:

我们根据 OAuth 规范收到了不记名令牌响应。 它们是access_tokenexpires_intoken_typeexpires_in参数默认设置为 3600,单位为秒,即 1 小时。 1 小时后,此令牌将不再工作。 因此,在这个令牌到期之前,让我们用它快速调用我们的 API,看看它是否工作。

第 5 步-使用访问令牌调用 API

看看下面的图片,它显示了使用我们刚刚收到的令牌对 API 的调用:

Calling the API endpoint with the token in the authorization header

瞧! 这工作。 我只是复制了我得到的令牌,并以Bearer [Access Token]格式添加到授权头中,并发送了请求。 现在一切都很完美。

步骤 6 -添加 ProfileService 类

在我们讨论这一切的时候,我遗漏了我现在想解释的一部分。 当我们获取访问令牌时,如果你看到请求体,它是这样的:

grant_type=password&scope=FlixOneStore.ReadAccess&client_id=htmlClient&client_secret=secretpassword&username=taditdash@gmail.com&password=12345

关注usernamepassword参数。 他们在这里是有原因的。 在生成令牌时,会对它们进行验证,是的,我们直接使用数据库进行验证。 让我们来看看。

IdentityServer4为此提供了两个接口,分别为IProfileServiceIResourceOwnerPasswordValidator

下面是实现该接口的ResourceOwnerPasswordValidator类。 记住,我们在客户端的配置中设置了AllowedGrantTypes = GrantTypes.ResourceOwnerPassword。 这就是为什么我们要这样做来验证用户的凭证:

public class ResourceOwnerPasswordValidator : IResourceOwnerPasswordValidator
{
  private readonly FlixOneStoreContext _context;
  public ResourceOwnerPasswordValidator(FlixOneStoreContext context)
  {
    _context = context;
  }
  public async Task ValidateAsync(ResourceOwnerPassword
  ValidationContext context)
  {
    try
    {
      var customer = await _context.Customers.SingleOrDefaultAsync
      (m => m.Email == context.UserName);
      if (customer != null)
      {
        if (customer.Password == context.Password)
        {
          context.Result = new GrantValidationResult(
 subject: customer.Id.ToString(),
 authenticationMethod: "database",
 claims: GetUserClaims(customer));
          return;
        }
        context.Result = new GrantValidationResult
        (TokenRequestErrors.InvalidGrant,
        "Incorrect password");
        return;
      }
      context.Result = new GrantValidationResult
      (TokenRequestErrors.InvalidGrant,
      "User does not exist.");
      return;
    }
    catch (Exception ex)
    {
      context.Result = new GrantValidationResult
      (TokenRequestErrors.InvalidGrant,
      "Invalid username or password");
    }
  }
  public static Claim[] GetUserClaims(Customers customer)
  {
    return new Claim[]
    {
      new Claim(JwtClaimTypes.Id, customer.Id.ToString() ?? ""),
      new Claim(JwtClaimTypes.Name, (
      !string.IsNullOrEmpty(customer.Firstname) &&  
      !string.IsNullOrEmpty(customer.Lastname))
      ? (customer.Firstname + " " + customer.Lastname)
      : String.Empty),
      new Claim(JwtClaimTypes.GivenName, customer.Firstname ?? 
      string.Empty),
      new Claim(JwtClaimTypes.FamilyName, customer.Lastname ??       
      string.Empty),
      new Claim(JwtClaimTypes.Email, customer.Email ?? string.Empty)
    };
  }
}

在前面的代码中标记粗体行。 ValidateAsync是向我们提供请求细节的方法,然后用数据库值对其进行验证。 如果匹配,则创建一个带有subjectauthenticationMethodclaimsGrantValidationResult对象。

GetUserClaims帮助我们建立所有的索赔。 我们马上就会看到这些主张的实际用途。

We added a number of claims inside config with a list of ApiResources, such as Id, Name, Email, GivenName, and FamilyName. That means the server can return these details about the customer.

让我们跳到ProfileService:

public class ProfileService : IProfileService
{
  private readonly FlixOneStoreContext _context;
  public ProfileService(FlixOneStoreContext context)
  {
    _context = context;
  }
  public async Task GetProfileDataAsync(ProfileDataRequestContext 
  profileContext)
  {
    if (!string.IsNullOrEmpty(profileContext.Subject.Identity.Name))
    {
      var customer = await _context.Customers
      .SingleOrDefaultAsync(m => m.Email == 
      profileContext.Subject.Identity.Name);
      if (customer != null)
      {
        var claims = ResourceOwnerPasswordValidator.
        GetUserClaims(customer);
 profileContext.IssuedClaims = claims.Where(x => 
        profileContext.RequestedClaimTypes.Contains(x.Type)).ToList();
      }
    }
    else
    {
      var customerId = profileContext.Subject.Claims.FirstOrDefault
      (x => x.Type == "sub");
      if (!string.IsNullOrEmpty(customerId.Value))
      {
        var customer = await _context.Customers
        .SingleOrDefaultAsync(u => u.Id == 
        Guid.Parse(customerId.Value));
        if (customer != null)
        {
          var claims = 
          ResourceOwnerPasswordValidator.GetUserClaims(customer);
 profileContext.IssuedClaims = claims.Where(x => 
          profileContext.RequestedClaimTypes.Contains(x.Type)).
          ToList();
        }
      }
    } 
  }
}

一个ProfileDataRequestContext对象中填充了我们在ApiResource中添加的所有声明。 请参考下面的屏幕截图,这是调试时请求的索赔列表:

这意味着我们需要从我们所做的客户记录中填充所有这些细节,并将其添加到IssuedClaims

等一下! 我们为什么要这么做? 因为我们的配置告诉我们提供这个信息。 但是我们需要填写所有需要的信息吗? 不。 不一定。 我们可以根据自己的意愿提出多少索赔。

现在的大问题! 我们在哪里找到这些信息? 我们知道,在所有这些授权设置之后,我们得到了一个加密的令牌字符串。 你猜吗? 是的,所有这些信息实际上都驻留在令牌本身中。 不要相信我,相信下面的截图。 由于该令牌是一个 JWT 令牌,您可以使用https://jwt.io/来解码它,并查看里面是什么:

基于客户机的 API-consumption 架构

我们已经讨论了RESTful 服务、Web api 以及如何注册、认证和授权用户。 此外,我们确实稍微关注了服务的消费方面。 服务不仅设计为在Postman上测试,而且实际上是为不同类型的应用(桌面、web、移动、智能手表和物联网应用)消费。

虽然大多数现代应用都是基于基于 mvc 的架构,但在这些应用的控制器中仍然需要使用 web 服务。 基本上,我需要找到一种方法来毫无麻烦地从控制器调用服务。

要做到这一点,我不能调用Postman或任何其他第三方工具。 我需要的是能够与RESTful Web API交互的客户端或组件。 我只需要告诉客户,我需要*客户通过id*细节或一些标识符和其他由客户照顾,从调用 API,传递价值,并得到响应。 响应最终返回到控制器,然后我可以对其进行操作。

我们将在第 10 章、*构建 Web 客户端(消费 Web 服务)*中探讨如何用简单、快速、简单的步骤构建 REST 客户端。

总结

注册是一个非常常见,但非常重要的部分应用。 我们通过 API 为客户办理注册。 在此之前,我们学习了如何使用 EF Core 引导 API 控制器操作和建模类。 当我们做这些的时候,我们降落在 CORS 上,也学会了如何处理它。

渐渐地,我们转到身份验证部分,在那里我们详细讨论了基本身份验证。 它是一种通过客户(他们是我们 API 的用户)凭据(usernamepassword)验证客户的机制,这些凭据随请求一起传递进来。

无记名基于令牌的身份验证是我们研究的下一个主题,我们使用IdentityServer4实现了 OAuth 范例。 在这种情况下,客户端不能像在 basic 情况下那样通过usernamepassword直接访问资源。 它首先需要的是一个令牌,它是由一个授权服务器根据客户机的请求生成的,客户机详细信息如客户机 id客户机秘密。 然后,可以将令牌发送给 API,用于后续的受限资源访问请求。

在下一章,我们将这些知识来构建其他组件的 API,如购物车*、,订单项,付款***