八、在 ASP.NET 中使用身份

现在几乎所有的网站都有登录功能。即使它们在匿名浏览时有效,通常也可以选择成为会员或类似的东西。这意味着这些网站有一些身份概念来区分访问者。换句话说,如果你的任务是建立一个网站,那么你很可能也需要处理身份问题。问题是,身份可能很难被正确对待,而错误对待的后果可能不那么有趣。在本章中,我们将深入了解 ASP.NET 5 中标识的基础知识。

本章将介绍以下主题:

  • 理解身份验证概念
  • 理解授权概念
  • ASP.NET 中间件的作用与身份
  • OAuth 和 OpenID 连接基础
  • 与 Azure Active Directory 集成
  • 使用联邦身份

技术要求

本章包括简短的代码片段,以演示所解释的概念。需要以下软件才能使其正常工作:

出于实验室目的,本章中的所有样品都可以免费测试,但区域特定要求可能需要使用信用卡进行验证。

请访问以下链接查看 CiA 视频:https://bit.ly/3qDiqYY

本章代码见https://github.com/PacktPublishing/ASP.NET-Core-5-for-Beginners/tree/master/Chapter%2008

理解认证概念

当我们在日常用语中说“身份”时,我们大多数人都理解自己的意思。在.NET 和一般的编码中,在允许用户进入我们的应用之前,我们需要更加具体。在此上下文中,身份包含多个概念,在确定用户是谁以及允许他们在我们的系统中做什么的过程中,具有不同的操作和机制。

身份之谜的第一部分是身份验证。在文档和文献中,您会经常发现这个缩写为AuthN。身份验证就是回答你是谁的问题。与现实世界类似,这会带来不同程度的信任,这取决于这个问题的答案。

如果你在一个聚会上遇到一个你不认识的人,问他们叫什么名字,你可能会对他们的回答感到高兴,而不用进一步核实。然而,您很可能不喜欢在网站上实现登录功能,用户只需键入用户名即可登录。

现实生活中的一个例子是要求某人提供身份证件——可以是国民身份证、驾驶执照、护照或类似证件。在网站上,最常用的方法是提供用户名和只有你知道的秘密(例如密码)的组合。

在 web 应用中实现这一点的最简单形式是使用基本身份验证,这是 HTTP 规范的部分。这是通过客户端向 HTTP 请求附加一个头,并将凭据编码为 Base64 值来实现的。在控制台应用中,它将如下所示:

static void Main(string[] args){  var username = ''andreas'';  var password = ''password'';  var byteEncoding = System.Text.UTF8Encoding.UTF8.GetBytes(
    $''{username}:{password}'');  var credentials = Convert.ToBase64String(byteEncoding);       
  Console.WriteLine(credentials);
  HttpClient client = new HttpClient();  client.DefaultRequestHeaders.Authorization = new  
    AuthenticationHeaderValue(''Basic'', credentials);  var response = client.GetAsync(''https://localhost:5001'');}

凭证将始终是YW5kcmVhczpwYXNzd29yZA==,没有随机元素,因此以这种方式传输凭证的主要好处是用于编码目的。在继续之前,让我们快速了解一下 Base64 是什么。

Base64 编码

我们大家都熟悉 Base10(通常称为十进制),因为这是我们在做普通算术时使用的——我们使用 0–9 来表示数字。在计算中,Base16 也经常以十六进制的名称使用。因为数字只升到 9,所以我们用字母加起来,所以 A=10,B=11,等等,直到 F=15。Base64 更进一步,使用了A-Za-z0-9以及+/字符,其中=作为特殊的填充字符(以确保字符串始终具有可预测的长度)。

我们将不深入讨论如何转换字符的算法,但正如前面的代码片段中所演示的,它将把一些人类可读的东西转换成一些虽然在技术上仍然可读,但仅通过查看就很难解释的东西。以这种方式编码数据的主要好处是,即使使用不可打印或不可读的字符,也可以传输纯文本和二进制数据而不会损坏。HTTP 协议本身并不能解释所有字符,因此对于带有特殊字符的密码,如果不进行编码传输,则可能无法在服务器端正确解释该密码。

Base64 不是加密的一种形式,因此您不能信任它本身的秘密,它可以被视为纯文本,即使您作为一个人类,无法动态解码它。这也意味着在没有 HTTPS 的情况下使用基本身份验证是一种不安全的身份验证机制。使用 TLS/SSL 来保护传输在这方面有了很大的改进,但它仍然依赖于通过网络发送密码。

在的脑海中,我们可以解码传输另一端的 Base64 字符串,相应的服务器部分如下所示:

public String Get(){  var authHeader = HttpContext.Request.      Headers[''Authorization''];  var base64Creds = AuthenticationHeaderValue.Parse
    (authHeader).Parameter;  var byteEncoded = System.Convert.FromBase64String(base64Creds);  var credentials = System.Text.Encoding.UTF8.GetString(
      byteEncoded);            
  if (credentials == ''andreas:password'')  {    return ''Hello Andreas'';  }  else   {    return ''You didn't pass authentication!'';  }            }

首先运行服务器,然后运行客户端,您将获得一些输出:

dotnet run Base64 encoded: YW5kcmVhczpwYXNzd29yZA==Response: Hello Andreas

由于我们在身份验证代码中对用户名和密码进行了硬编码,所以您可能不会感到惊讶,因为这个实现是一个糟糕的实现。在这一点上,显而易见的选择是将其移动到数据库中并进行查找。这让我们想起了你可能犯下的最严重的身份实现错误之一——将密码直接存储在数据库中。永远不要在数据库中存储密码。时期您应该存储一个不可逆的密码散列,并计算输入的密码是否与数据库中存储的密码匹配。这样,如果攻击者掌握了数据库,他们就无法轻易提取密码。

这就引出了一个问题,在这个上下文中,散列是什么,下面我们来讨论这个问题。

散列是如何工作的

散列函数是一种将一个值转换为另一个值的算法,通常用于优化数据结构中的查找或验证初始值。例如,如果我们要创建一个非常基本的哈希算法,我们可以使用数字替换字符来创建给定字符串的哈希。比如说A=1B=2等等。然后,Password字符串将是16 1 19 19 23 15 4(每个数字代表一个字符;为了可读性而添加空格)。然后我们将这些数字相加,除以字符数–(16 + 1 + 19 + 19 + 23 + 15 + 4) / 8 = 12.125。仅使用整数部分,我们以12结束。

我们将存储值12,而不是存储您的实际密码。当我们输入Password作为密码时,我们可以再次计算散列并将其与存储值进行比较。这也很好,因为它是不可逆的——即使已知算法,也不可能将数字12反向工程为Password,因此数据库的副本不会有助于确定密码。

即使你不是数学天才,你也可能会发现这个算法很弱。使用我们使用的简单替换方案,创建一个字符串相当容易,该字符串也将产生12作为值,因此是有效的。一个好的散列算法应该产生唯一的值,这样两个不同的密码就不可能有相同的散列。幸运的是,Microsoft 已经为.NET 实现了许多哈希算法,因此您不必推出自己的哈希算法。

如果我们要用伪代码来说明这一点(我们不会编译,因为我们没有实现数据库查找),它将如下所示:

var credentials = System.Text.Encoding.UTF8.GetString(byteEncoded);
//Split the credentials into separate parts var username = credentials.Split('':'')[0];var password = credentials.Split('':'')[1];
//Bad          if (db.CheckUser == true && db.CheckPassword == true){  return $''Hello {username}'';}
//Good var myHash = System.Security.Cryptography.SHA256.Create();var hashEncoder = System.Text.UTF8Encoding.UTF8;var byteHashedPassword =  myHash.ComputeHash(hashEncoder.GetBytes(password));          
System.Text.StringBuilder sb = new System.Text.StringBuilder();foreach (Byte b in byteHashedPassword)  sb.Append(b.ToString(''x2''));
var hashedPassword = sb;if (db.CheckUser == true && db.CheckHashedPassword == true){  return $''Hello {username}'';}

到现在为止,你可能会认为在身份验证中会有很多事情发生,而你是正确的。事实上,我们并不推荐使用基本身份验证,但希望它能让您了解什么是身份验证。在解释了身份验证的一个紧密伙伴,即授权之后,我们将展示一些更好的技术。

了解授权概念

身份拼图的第二块是授权,通常将缩短为AuthZ。其中AuthN是关于找出你是谁,AuthZ是关于你被允许做什么。

让我们回到现实世界和事物是如何运作的,让我们暂时考虑一下国际航空旅行。为了简单起见,假设所有国际旅行都需要您出示护照。如果您没有护照,这将等同于未经身份验证(未经身份验证),您将不被允许进入目的国。

如果您有护照,相关当局将通过询问以下问题对其进行审查:

  • 它是由一个真实的国家发行的吗?(不幸的是,“.NET 土地”未得到联合国的承认。)
  • 它看起来是真的,带有水印、生物特征标记等等,还是看起来像你在家里打印的东西?
  • 签发护照的国家是否有良好的护照签发程序?

如果您通过这些检查,您将获得身份验证,但您可能还无法继续进行行李认领。新一轮的问题是:

  • 您是目的地接受游客的国家的公民吗?
  • 您是否来自需要签证的国家?如果是,您是否随身携带签证?
  • 你是被判有罪的罪犯吗?
  • 你是已知的恐怖分子吗?(航空公司在让你登机之前可能应该检查一下,但他们可能错过了。)

细节会因你想进入哪个国家而有所不同,但要点是一样的。当你的身份检查出来时,还有其他机制可以给你盖上批准章。

您可能已经在 web 应用中识别出类似的模式。例如,如果您以John作为用户名登录,则您拥有普通用户的权限,可以进行数据库查找、编辑等操作。然而,如果您以JohnAdmin作为用户名登录,您将获得管理权限,可以访问系统范围的服务器设置等。回顾上一节中的身份验证代码,我们将伪代码扩展为如下内容:

public String Get(){  var authHeader = HttpContext.Request.Headers[''Authorization''];  var base64Creds =    AuthenticationHeaderValue.Parse(authHeader).Parameter;  var byteEncoded = System.Convert.FromBase64String(base64Creds);  var credentials =System.Text.Encoding.UTF8.GetString(byteEncoded);   
  //Split the credentials into separate parts   var username = credentials.Split('':'')[0];  var password = credentials.Split('':'')[1];
  //Password hashing magic omitted   ...  //Authentication code omitted   ...
  var userrole;         
  if (db.CheckRole == ''Admin'')  {    userrole = ''Admin'';  }  if (db.CheckRole == ''User'')  {    userrole = ''User''  }  else   {    return ''You didn't pass authentication!'';  }            
  return $''Hello {userrole}'';          }

尽管这也是我们缺少角色查找的伪代码,但我们可以看到在引入授权时它是如何添加了一个附加层的。这可能是因为你的 web 应用可能不需要区分不同的角色,但我们在这里强调的一点是,我们已经构建了好几页了。

不要从头开始实现您自己的标识解决方案(或基于此示例代码)。

这并不是要抹黑本书读者的知识和能力;这是一个普遍的最佳实践,应该由那些全职工作的人来完成,他们有权与团队一起以批判的眼光审查和测试所有事情。

Microsoft 已在 Visual Studio 中为一个 SQL 支持的 web 应用添加了一个模板,该应用实现了类似的身份设置:

  1. 启动 Visual Studio 并选择新建项目
  2. 选择ASP.NET Core Web 应用模板,点击下一步
  3. 将解决方案命名为Chapter_08_DB_Auth并为本书练习选择合适的位置(如C:\Code\Book\Chapter_08,然后点击创建
  4. Select the Web Application (Model-View-Controller) option and click Change under Authentication. Make sure you select Individual User Accounts and Store user accounts in-app before clicking OK, followed by Create:

    Figure 8.1 – Individual user accounts authentication

    图 8.1–个人用户帐户身份验证

  5. If you take a look at the Data folder, you will see the code that generates a database where the user accounts are stored as shown in Figure 8.2:

    Figure 8.2 – Migrations files in Visual Studio

    图 8.2–Visual Studio 中的迁移文件

  6. Open up 00000000000000_CreateIdentitySchema.cs. It should be 200+ lines of code, and the user object looks like this:

    cs migrationBuilder.CreateTable(  name: ''AspNetUsers'',  columns: table => new   {    Id = table.Column<string>(nullable: false),    UserName = table.Column<string>(maxLength: 256,       nullable: true),    NormalizedUserName = table.Column<string>(maxLength:       256, nullable: true),    Email = table.Column<string>(maxLength: 256,       nullable: true),    NormalizedEmail = table.Column<string>(maxLength:       256, nullable: true),    EmailConfirmed = table.Column<bool>(nullable: false),    PasswordHash = table.Column<string>(nullable: true),    SecurityStamp = table.Column<string>(nullable: true),    ConcurrencyStamp = table.Column<string>(nullable:       true), PhoneNumber = table.Column<string>(nullable:      true), PhoneNumberConfirmed = table.Column<bool>(       nullable: false),    TwoFactorEnabled = table.Column<bool>(nullable:       false), LockoutEnd = table.      Column<DateTimeOffset>(nullable: true),    LockoutEnabled = table.Column<bool>(nullable: false),    AccessFailedCount = table.Column<int>(nullable:       false)  },  constraints: table =>  {    table.PrimaryKey(''PK_AspNetUsers'', x => x.Id);  });

    这些名称应该是不言自明的,但正如您所看到的,除了用户名和散列密码之外,还有更多的含义。

  7. 通过快速查看Startup.cs中的配置,我们可以看到数据库在哪里初始化,需要进行身份验证:

    cs public void ConfigureServices(IServiceCollection services){  services.AddDbContext<ApplicationDbContext>(options =>    options.UseSqlServer(                           Configuration.GetConnectionString(''DefaultConnection'')));  services.AddDefaultIdentity<IdentityUser>(options =>   options.SignIn.RequireConfirmedAccount = true)    .AddEntityFrameworkStores<ApplicationDbContext>();  services.AddControllersWithViews();}

  8. 在尝试运行应用之后,应该有一个用于注册电子邮件地址和定义密码的表单。图 8.3为报名示例:

Figure 8.3 – Registering an individual user account

图 8.3–注册单个用户帐户

如果你仔细查看构建的其他文件,你会注意到实际上有一些代码可以让它全部运行,然后在库中有你看不到的所有东西,巩固了为什么你不愿意自己做这些事情。

像这样的模板在几年前非常流行,因为它们耗费了大量的精力,用户习惯于在他们访问的每个网站上注册。虽然使用它本身并没有什么错——它是安全的,由微软维护——但现在有了其他选择,它变得不那么常见了。

我们将很快恢复常规编程,但前面的代码片段为我们提供了一个切入点,让我们进入一个在技术上与身份无关的主题,但对于理解不同身份片段如何在.NET 应用中发挥作用非常有用。

ASP.NET 中间件的作用与身份

很多技术和产品都是以一个代码名开始的,当微软提出Katana项目时,它肯定对这个名称有着强烈的兴趣。该项目于 2013 年推出,旨在解决.NET 当时的几个缺点。

我们不会拖拽旧的.NET 代码并指出设计中的缺陷,但即使不深入细节,您也可能会联想到替换代码中组件的挑战。比如说,你开始创建一个实用程序来控制家中的一些智能灯泡。在一天的故障排除过程中,您意识到如果捕获一些信息并将其记录下来会更容易。快速而肮脏的方法是在名为log.txt的文件中追加行。这很好地工作,直到您意识到您可以使用一些对非错误条件的洞察,例如在灯光打开和关闭时记录,为自己创建一些统计信息。

当你想在应用之外使用文本文件时,它不容易登录到文本文件中。因此,您意识到在数据库中使用它可能会很好。然后,您必须重写对文件的所有调用,以登录到数据库。你明白了。

如果有一个更通用的log.Info(''Lights out'')方法,而不关心细节,那就太好了。由于日志记录在许多应用中都是一个常见的问题,所以有很多日志记录框架,但每个应用都有一个安装仪式。

这一章是关于身份的,你说这两者之间有什么联系?认证和授权也是应用的常见用例。web 应用中的 URL 路由、缓存以及其他一些东西也是如此。

这些组件的另一个方面是,您最有可能希望在应用初始化期间尽早运行它们——当出现故障时加载日志组件可能太晚了。

这是一个精心设置的说法,微软已经构建了一个称为中间件的抽象。Katana 项目实际上涵盖了四个组件,这将延续到当前的实现中——主机、服务器、中间件和应用。

主机部分可以在Program.cs中找到,对于一个 web 应用,它如下所示:

public class Program {  public static void Main(string[] args)  {    CreateHostBuilder(args).Build().Run();  }  public static IHostBuilder CreateHostBuilder(string[] args) =>    Host.CreateDefaultBuilder(args)      .ConfigureWebHostDefaults(webBuilder =>      {        webBuilder.UseStartup<Startup>();      });}

如果您将其与我们在第 2 章跨平台设置中创建的工人服务进行比较,您会发现相似之处:

public class Program {  public static void Main(string[] args)  {    CreateHostBuilder(args).Build().Run();  }  public static IHostBuilder CreateHostBuilder(string[] args) =>    Host.CreateDefaultBuilder(args)        .UseWindowsService()      .UseSystemd()                    .ConfigureServices((hostContext, services) =>      {        services.AddHostedService<Worker>();      });}

你无法通过更改这些行将任何 web 应用转变为服务,但请注意模式是如何相同的。

我们已经提到并查看了Startup.cs文件,在该文件中可以找到服务器和中间件组件。

运行时使用以下代码调用服务器和服务:

public void ConfigureServices(IServiceCollection services){  …  services.AddControllersWithViews();  …}

正如我们已经看到的,实际的运行时可能会有所不同,这取决于是在 IIS 中托管还是在 Kestrel 中托管(在本文中这并不重要)。

中间件可在文件的下一节中找到:

public void Configure(IApplicationBuilder app, IWebHostEnvironment env){  if (env.IsDevelopment())  {    app.UseDeveloperExceptionPage();  }  else   {    app.UseExceptionHandler(''/Error'');    app.UseHsts();  }  …}

这被称为管道,它构建为一个序列——例如,身份验证先于授权,但并非所有中间件都对加载它的步骤敏感。

一些中间件有一个二进制行为–UseHttpsRedirection正好支持这种行为,如果您不想要它,只需删除它即可。

UseEndpoints允许您添加要收听的特定端点:

app.UseEndpoints(endpoints =>  {    endpoints.MapControllerRoute(      name: ''default'',      pattern: ''{controller=Home}/{action=Index}/{id?}'');  });

中间件和身份的美妙之处在于,您可以将自定义中间件添加到组合中,并且由于使用是标准化的,因此之后进行更改是相当轻松的。我们没有将 basicauth 作为中间件实现,但是 visualstudio 中向导添加的用于使用本地数据库的样板文件实现了。

如果我们将身份实现升级为基于 OAuth,这将变得非常方便,下面将介绍 OAuth。

OAuth 和 OpenID 连接基础

基本身份验证易于实现,如果您需要使用遗留系统,很有可能会遇到它。但不建议使用基本身份验证启动新项目。

身份空间中协议的首字母缩略词并不短缺,.NET Framework 多年来一直依赖于不同的身份验证和授权协议。我们无法深入研究所有这些问题,也无法比较它们的优缺点。

目前用于AuthNAuthZ目的的最流行的一组协议是OAuthOpenID ConnectOIDC),因此我们将研究的部分理论和实际实现。OAuth 是基本协议,OIDC 构建在这个基础之上,因此有一些重叠的细节我们将继续讨论。

回顾基本身份验证,我们已经提到了一个缺点,即密码是通过网络传输的。客户端和服务器端都可以访问实际密码,在许多情况下,这超出了它们的需要。例如,在允许您访问管理设置之前,web 应用肯定会关心您是否具有管理员角色,但只要标识已建立,密码在执行此授权步骤时不会提供任何值。这只是您需要保护的额外数据。

OAuth 将这些部分解耦,以便服务器端不需要知道密码。对于客户机来说,这更像是一种“取决于”如何处理的情况——如果需要密码,您无法避免在某处键入密码。这一切都是从所谓的JSON Web 令牌JWTs开始的,所以让我们先来讨论一下。

JSON web 令牌

对于 OAuth 和 OIDC,我们不依赖于将username:password作为王国的钥匙来传递,而是依赖于传递令牌。这些代币称为 JWTs,发音为jot/jots

JWT 的格式为 JSON,包含三个部分——头、负载和签名。JWT 示例可能如下所示:

{  ''alg'': ''RS256'',  ''kid'': ''4B92FBAE5D98B4D2AB43ACE4198026073012E17F'',  ''x5t'': ''S5L7rl2YtNKrQ6zkGYAmBzAS4X8'',  ''typ'': ''JWT''}.{  ''sub'': ''john.doe@contoso.com'',  ''nbf'': 1596035128,  ''exp'': 1596038728,  ''iss'': ''contoso'',  ''aud'': ''MyWebApp''}.[Signature]

如果您以前没有见过类似的情况,您可能(至少)有两个问题:

  • 这一切意味着什么?
  • 这到底有什么帮助?

此令牌中的信息称为索赔——因此,例如''sub''索赔是主体的缩写,其值为john.doe@contoso.com。此声明通常是用户/用户名(不必是电子邮件格式,但这是常见的)。

其余项权利要求如下。

标题如下

  • ''alg'':用于生成签名的算法
  • ''kid'':密钥标识符
  • ''x5t'':密钥标识符
  • ''typ'':令牌的类型

有效载荷如下

  • ''nbf'':之前没有。令牌生效的时间;通常与发行时间相同。
  • ''exp'':到期时间。令牌在之前的有效时间。通常从发行时算起一小时(但这取决于令牌发行人)。
  • ''iss'':发行人。代币的发行人。
  • ''aud'':观众。代币是给谁的;通常,令牌用于的应用。

这只是一个最小的示例标记–如果愿意,您可以拥有更多声明,并选择这些声明的格式。如果您想要一个值为''bar''''foo''声明,而该声明仅对您的应用有意义,这是可以的。请注意,令牌的大小并不是无限的——在企业环境中,一些开发人员试图包括用户所属的所有组。当用户是 200 多个组的成员时,您会遇到所谓的令牌膨胀,这会导致令牌在通过网络传输时碎片化。在大多数情况下,这些数据包没有正确地重新组装,导致数据包崩溃。

将令牌传递给服务器类似于基本身份验证,因为我们添加了一个授权头,其中令牌是 Base64 编码的(为简洁起见,令牌被缩短):

Authorization: Bearer eyJhbGciOi...PDh4ck7Q

这是很好的,因为您可以发送比传递用户名和密码时更多的信息,同时仍然将凭据保留在数据传输之外。它被称为不记名代币,因为拥有它的任何人都可以使用它。这让我们回到第二个问题——这如何更好?你得到的第一印象是,任何客户都可以自己制作代币,这听起来不是一个好的机制。

OAuth/OIDC 事务中有两个重要操作:

  • 发放代币:这是关于控制谁获得代币,这将受到一个或多个机制的保护。
  • 验证令牌:此是关于检查令牌是否可信以及内容是什么。

这两种方法都主要基于使用证书——在颁发时签名,在验证时验证。(注意,这与基于证书的身份验证不同;这里我们只关注令牌本身。)

让我们来看看这在代码中是如何工作的。

如何生成/发行代币

第 2 章跨平台设置中,我们展示了如何生成证书,在 Windows 和 Linux 上安装证书,以及随后的阅读。在此基础上,我们可以使用相同的证书对令牌进行签名。

要创建一个将生成令牌的应用,请执行以下操作:

  1. 打开命令行并创建一个新目录(Chapter_08_BearerAuthClient
  2. 运行dotnet new console命令。
  3. 运行dotnet add package System.IdentityModel.Tokens.Jwt命令。
  4. We then need to add some code to Program.cs. First, we create the token (based on a generic template):

    cs static void Main(string[] args){  jwt = new GenericToken   {    Audience = ''Chapter_08_BearerAuth'',    IssuedAt = DateTime.UtcNow.ToString(),    iat = DateTimeOffset.UtcNow.ToUnixTimeSeconds().       ToString(),    Expiration = DateTime.UtcNow.AddMinutes(60).ToString(),    exp = DateTimeOffset.UtcNow.AddMinutes(60).       ToUnixTimeSeconds().ToString(),    Issuer = ''Chapter 08'',    Subject = ''john.doe@contoso.com'',  };

    然后,我们设置/检索用于签名的证书:

    cs SigningCredentials = new Lazy<X509SigningCredentials>(() =>  {    X509Store certStore = new X509Store(StoreName.My,     StoreLocation.CurrentUser);    certStore.Open(OpenFlags.ReadOnly);    X509Certificate2Collection certCollection =       certStore.Certificates.Find(                                               X509FindType.FindByThumbprint,      SigningCertThumbprint,      false);     // Get the first cert with the thumbprint     if (certCollection.Count > 0)    {      return new X509SigningCredentials(certCollection[0]);    }    throw new Exception(''Certificate not found'');  });

    最后一个部分是排列声明并创建实际签名的令牌:

    cs IList<System.Security.Claims.Claim> claims = new      List<System.Security.Claims.Claim>();  claims.Add(new System.Security.Claims.Claim(''sub'', jwt.Subject,     System.Security.Claims.ClaimValueTypes.String, jwt.Issuer));   // Create the token   JwtSecurityToken token = new JwtSecurityToken(    jwt.Issuer,    jwt.Audience,    claims,    DateTime.Parse(jwt.IssuedAt),    DateTime.Parse(jwt.Expiration),    SigningCredentials.Value);   // Get the string representation of the signed token and   // print it   JwtSecurityTokenHandler jwtHandler = new     JwtSecurityTokenHandler();   output = jwtHandler.WriteToken(token);  Console.WriteLine($''Token: {output}'');}

    请注意,为了关注重要的部分,这不是完整的代码——请查看本章的 GitHub repo 以了解完整的代码。

  5. Run the dotnet run command.

    您的输出将类似于图 8.4

Figure 8.4 – A JWT

图 8.4-A JWT

这不是供您阅读的,但它是可逆的,因为它只是 Base64 编码的。最重要的一点是,你真正的秘密没有包括在内,所以即使有人能读到,这也不是问题。

如何验证令牌

生成一个令牌很不错,但毫不奇怪,我们需要一个对应的令牌—检查令牌是否良好,并根据此评估允许或拒绝访问。为此,我们还将创建一个服务器端代码示例:

  1. 打开命令行并创建一个新目录(Chapter_08_BearerAuthServer
  2. 运行dotnet new console命令。
  3. 运行dotnet add package System.IdentityModel.Tokens.Jwt命令。
  4. The following code goes into EchoController.cs:

    cs [HttpGet]public String Get(){              var audience = ''Chapter_08_BearerAuth'';  var issuer = ''Chapter 08'';   var authHeader = HttpContext.Request.Headers     [''Authorization''];  var base64Token = AuthenticationHeaderValue.Parse(     authHeader).Parameter;               JwtSecurityTokenHandler handler = new        JwtSecurityTokenHandler();  TokenValidationParameters validationParameters = null;  validationParameters = new TokenValidationParameters   {    ValidIssuer = issuer,    ValidAudience = audience,    ValidateLifetime = true,    ValidateAudience = true,    ValidateIssuer = true,    //Needed to force disabling signature validation                            SignatureValidator = delegate (string token,       TokenValidationParameters parameters)    {      var jwt = new JwtSecurityToken(token);      return jwt;    },                            ValidateIssuerSigningKey = false,                     };   try   {    SecurityToken validatedToken;                    var identity = handler.ValidateToken(base64Token,       validationParameters, out validatedToken);    return ''Token is valid!'';     }  catch (Exception e)  {    return $''Token failed to validate:  {e.Message}'';  }}

    与前面的代码示例一样,为了便于阅读,部分代码被省略了。

  5. 运行dotnet run命令。

  6. 后退到客户端代码,添加以下代码:

    cs HttpClient client = new HttpClient();client.DefaultRequestHeaders.Authorization = new   AuthenticationHeaderValue(''Bearer'', output);var response =   client.GetAsync(''https://localhost:5001/Echo'').Result;           Console.WriteLine(response.Content.ReadAsStringAsync().Result.     ToString());

  7. Run the dotnet run command in this folder while the server part is running.

    您应该看到一个输出,上面写着Token is valid

虽然服务器代码中有一些术语直观地具有某种意义,但可能需要对该过程进行一点解释。

最基本的是,我们为发行者(发行令牌的人)和受众(令牌的预期接收者)配置值。然后,我们配置用于验证令牌的参数;上述受众和发行人以及代币的时间戳。

如果令牌有效,我们将返回一条指示有效的消息,如果失败,我们将返回另一条消息。

在这段代码中,我们禁用了对签名的检查,这似乎违反了直觉。您应该始终验证签名–如果不验证,任何人都可以生成一个令牌,只要他们找到正确的插入值,该令牌将被视为有效。禁用这一难题的重要部分的原因是,如果我们想这样做,代码将变得更加复杂。我们需要先讨论一些额外的主题,然后再回到一种需要较少复杂性才能正确使用的方法。

OAuth 流

能够向 API 发送一个令牌并对其进行验证是非常好的,但是您可能想知道这在应用中实际是如何工作的。我们在这里使用的详细信息中不能有用户类型,即使我们只在服务器上这样做,也不涉及任何凭据。这听起来不像你在现实生活中实际使用的东西。

JWT 是 OAuth 的一个核心部分,但协议的内容不仅仅是令牌。OAuth 由我们所称的“流程”组成,这些流程规定了获取和使用所述令牌的过程中的步骤。这里我们无法涵盖这些流的所有变体,但我们将介绍一些与 ASP.NET Core 用例相关的变体。

我们需要整理几个适用于所有流的术语。

我们有一个称为身份提供者的中央服务,而不是处理代币发行的每个应用。此服务通常验证凭据(密码、证书等),并负责颁发令牌。虽然这在技术上是您可以自己实现的,但强烈建议您在市场上使用一个成熟的解决方案(我们将考虑为此使用Azure AD

当获取令牌时,客户端请求它想要的权限。这些权限称为作用域,并作为声明嵌入到令牌中。

这里描述的流驱动了 Facebook、Google 和 Microsoft 的登录,因此您很可能已经尝试过了,即使您当时没有考虑太多。(这些提供程序支持多个流以支持不同的用例。)

OAuth 客户端凭据授予

最容易理解的流程可能是客户端凭据流程,因为这最接近于使用用户名和密码的。您将在 UI 中为正在使用的身份提供程序注册一个应用,并获得客户端 ID 和客户端机密。当您想要获取令牌时,可以将这些令牌发送给身份提供者,并指示您想要的权限。流程如图 8.5所示:

Figure 8.5 – OAuth Client Credentials flow

图 8.5–OAuth 客户端凭据流

需要注意的一件非常重要的事情是,此流仅适用于受信任的客户端。受信任的客户端通常运行在最终用户无法使用代码和配置的服务器上。这通常是一个服务帐户,或服务器端呈现的 web 应用。客户机 ID 不敏感,但与客户机机密配对后,它可能使任何拥有它的人都能够提取他们不应该拥有的信息。如果您有一个客户端应用(如下载到浏览器的 JavaScript)、一个移动应用或类似的应用,则永远不要使用客户端凭据流。

客户端密码通常太长、太复杂,用户无法记住和输入,因此对于密码,有不同的流程。

OAuth 资源所有者密码凭据

与客户端凭据类似但用于用户凭据的流是资源所有者密码凭据ROPC流。当使用外部身份提供者时,通常会有预定义的登录体验外观,通常在浏览器中呈现为 HTML。即使有一个选项可以根据你自己的喜好来设计它,但使用用户体验的人会说,他们需要以某种方式调整某些元素,以使他们感到高兴,这并不罕见。

此时,您可能会想,如果您可以自己创建所有可视方面并像实现服务器端身份验证体验一样处理身份验证,那将是一件非常棒的事情。此流程中存在这样的选项,但您永远不应该向设计人员承认它的存在。Microsoft 和 identity 社区非常不鼓励使用此流程,因为它本质上不如直接在专门的产品上处理凭据交换以处理身份用例。该应用承担了更多的责任,因为它将知道用户的密码。

我们之所以在这里提到它,是因为意识到它是有用的,即使它没有出现。

OAuth 授权码授予

在本机应用中执行身份验证的推荐方法是一个称为授权代码流的流。当你第一次遇到它时,它可能会显得有点复杂,但它背后有一个逻辑。我们需要用户手动输入他们的凭据,但应用不应该知道他们。同时,我们希望应用在调用 API 时也是一个实体。图如图 8.6所示:

Figure 8.6 – OAuth Authorization Code flow

图 8.6–OAuth 授权代码流

授权端点和令牌端点都位于标识提供程序上。

这个图 Po.T0A.不覆盖低级细节,但在这种情况下可能的攻击向量是,例如,在移动设备上,恶意的 To1 T1 应用可能能够拦截 AUTH 代码并使用它作为其未经批准的目的。建议您实现一个名为代码交换验证密钥PKCE–发音为pixie)的流扩展,以确保只有正确的应用才能使用特定的身份验证代码。

OAuth 隐式授权流

基本上很清楚什么是经典的 web 应用,什么是经典的原生应用,但基于 JavaScript 的单页应用SPA)在哪里合适呢?从某种意义上说,它是一种混合,您可以通过本地执行的浏览器提供代码。这意味着你不能认为它是一个可信的客户端。您将看到许多指南提到为这些目的使用隐式授权流。看起来像图 8.7

Figure 8.7 – OAuth Implicit Grant flow

图 8.7–OAuth 隐式授权流

这里的片段的意思是,当重定向回 SPA 时,令牌将是 URL 的一部分,而不是在 HTTP 响应的主体中返回。这是因为大多数 SPA 不像非 SPA web 应用那样“在页面之间跳转”,需要通过 URL 使用数据。

虽然在某些用例中,隐式授权是合适的,并且在很多地方都在使用,但当前的建议是,使用 PKCE 的身份验证代码更适合大多数 SPA。隐式授权不太安全,因此虽然它在功能上是可以接受的,但它还有其他缺点。

注意,如果您使用库来提供此功能,您应该尝试找出它在幕后使用的两个流中的哪一个。

OpenID 连接

之前的所有流程都集中于获取表示“允许您访问此 API”的令牌。当然,这是一个非常重要的解决方案,但如果您尝试在不接触 API 的情况下登录 web 应用,您通常只想知道“谁登录了”。为此,我们有 OIDC 流程,或者更准确地说,如图 8.8所示,OAuth 顶部有一个单独的协议构建:

Figure 8.8 – OIDC

图 8.8–OIDC

OIDC 协议还包括一些其他功能,使开发人员更容易登录,我们将在代码示例中介绍这些功能。

还有其他 OAuth 流,它可以比我们在这里所展示的更详细,但是它超出了本书的范围,无法涵盖 AuthN 和 AuthZ 的所有细微差别。

如果没有身份提供者,这些流是不好的,因此在下一节中,我们将使用流行的提供者将所有内容放到上下文中。

与 Azure Active Directory 集成

如果您在过去 20 年中登录到公司计算机,那么无论您是否意识到,您都可能使用过 Active Directory。AD 是在 Windows Server 2000 中引入的,扩展了 Windows NT 4.0 中引入的域概念,以提供集中式身份的完整实现。当您登录到 Windows 桌面时,只要您坐在办公室,就可以轻松访问组织中的文件共享和服务器。

有了 AD,您至少需要在本地和配套的基础设施上安装两台服务器。这在当今的云世界是不可行的,但微软在必须提供Azure Active DirectoryAAD作为云身份提供商的基础上,同时打破了物理位置的限制。

AD 基于旧的身份协议,因此 OAuth 流和 OIDC 本机不受支持,但需要使用Active Directory 联合服务ADFS)作为额外服务来支持我们刚才描述的内容。与 Windows Server 许可证相比,这不会带来额外的成本,但建议为此服务配备专用服务器。

相反,AAD 是在考虑较新协议的情况下构建的,因此它不支持没有附加组件的较旧协议。

这意味着,如果您希望将现有的支持广告的本地应用迁移到 AAD,则可能需要对标识堆栈进行一些重写。我们将不讨论这一点,而是直接讨论较新的协议。AAD 是基于开放标准的,您可以很容易地用符合这些标准的其他身份提供程序替换它,因此这也不是微软的锁定。

AAD 的基本形式是免费的。有一些高级安全功能不是免费提供的,并且限制为 50000 个对象,但即使对于许多生产部署,这也应该足够了。根据本章开头列出的技术要求,我们假设您拥有这些样本的 AAD 租户,因此如果您尚未注册,则应立即注册。

使用 AAD 解锁 Azure 门户中的一系列选项。例如,您可以控制我们描述的所有流是可用的,还是只使用一个子集。此外,您可以指定哪些用户具有访问权限,应用可以访问哪些其他数据源,等等。

如果您有一个现有的 web 应用,可以在此基础上添加对 AAD 的支持,但为了简化问题,我们将从头开始创建 Blazor 应用,使用 Visual Studio 中的向导在 Azure 中为我们进行后端配置:

  1. 启动 Visual Studio 2019 并选择创建新项目
  2. 选择Blazor App点击下一步
  3. 将解决方案命名为Chapter_08_AADAuth
  4. 点击认证下的变更
  5. Select Work or School Accounts and select Cloud - Single Organization as shown in Figure 8.9:

    Figure 8.9 – Work or School Accounts

    图 8.9——工作或学校账户

  6. 键入您将使用的 AAD 租户的域名。如果您以前没有登录,系统将提示您登录。

  7. 在点击创建之前,确保您选择了Blazor 服务器应用并且您已经选中了配置 HTTPS

如果您尝试运行应用,首先会遇到的是 Microsoft 提供的登录表单,如图 8.10所示:

Figure 8.10 – AAD sign in

图 8.10–AAD 登录

输入用户名和密码后,下一步是请求权限,如图 8.11 所示:

Figure 8.11 – Consent notification

图 8.11–同意通知

如果您点击接受按钮,应用将打开,在右上角,您将收到您的名字。看起来很简单,但是让我们先看看代码中的内容,然后再添加一些功能。

如果打开Startup.cs,您可能会注意到一些迄今为止尚未看到的代码:

public void ConfigureServices(IServiceCollection services){  services.AddMicrosoftIdentityWebAppAuthentication(Configuration,    ''AzureAd'');  services.AddControllersWithViews()    .AddMicrosoftIdentityUI();  services.AddAuthorization(options =>  {    // By default, all incoming requests will be authorized     // according to the default policy     options.FallbackPolicy = options.DefaultPolicy;  });  services.AddRazorPages();  services.AddServerSideBlazor()    .AddMicrosoftIdentityConsentHandler();}

在上一节中,我们提到了更换身份中间件是多么容易,我们可以在这里看到启动管道如何看到添加了中间件来处理身份和相关 UI。

如果我们看一下appsettings.json,我们可以看到我们的具体配置存储在哪里:

{  ''AzureAd'': {    ''Instance'': ''https://login.microsoftonline.com/'',    ''Domain'': ''contoso.com'',    ''TenantId'': ''tenant-guid'',    ''ClientId'': ''client-guid'',    ''CallbackPath'': ''/signin-oidc''  },

您可能会发现,甚至在看到网页之前就被登录提示击中,这有点不友好。有很多页面在您未登录时提供默认体验,登录时功能解锁。

这由Startup.cs中的几行代码控制:

//Comment out the line below like this //services.AddRazorPages();
//And replace with this services.AddRazorPages(options =>{  options.Conventions.AllowAnonymousToPage(''/_Host'');                });

请注意,这将有效地关闭 Blazor 应用中所有页面的授权,因此您需要在需要的页面上启用它。(关于如何更改默认行为的详细信息在不同的视图引擎(MVC、Razor 页面和 Blazor)之间有所不同。)

您可以将Index.razor的内容替换为以下代码:

@page ''/''
<AuthorizeView>  <Authorized>    Hello, @context.User.Identity.Name!    <table class=''table''>      <thead>        <tr>          <th scope=''col''>Claim Type</th>          <th scope=''col''>Claim Value</th>        </tr>      </thead>      <tbody>        @foreach (var claim in context.User.Claims)        {          <tr>            <td>@claim.Type</td>            <td>@claim.Value</td>          </tr>        }      </tbody>    </table>  </Authorized>  <NotAuthorized>    <p>For full functionality please log in</p>    <a href=''MicrosoftIdentity/Account/SignIn''>Log in</a>  </NotAuthorized></AuthorizeView>

这将打印您令牌中的所有声明,这仅在登录时才有意义,并在您尚未进行身份验证时为登录提供一个链接。这种方法适用于需要一个页面可供登录用户和匿名用户使用的情况。

如果要阻止页面的所有内容,可以通过添加[Authorize]属性(在Counter.razor中):

@page ''/counter''@attribute [Authorize]
<h1>Counter</h1>

未登录的用户只会看到一条消息,表明他们未经授权。

有多种方法可以配置此功能。您可以创建需要显示特定声明的策略,可以创建控制对视图的访问的角色,等等。我们不建议把它变得比必要的更复杂,尤其是在开始的时候。排除故障可能会很麻烦,因此首先要正确掌握基本知识。

理解单租赁与多租赁

在向导中,我们选择了云-单组织,但是如果您选中下拉列表,您可能会注意到云-多组织。我们或许应该解释一下。

这里的一个组织是 AAD 租户。这意味着,如果您的公司结构有多个租户,则这被视为多个组织,即使它可能只是一个合法组织。这是一个纯粹的技术定义。

当您创建单个组织应用时,这意味着只有一个特定 AAD 租户的用户才能登录,并且使用的数据主要是此租户的数据约束。如果您构建的应用只供您和您的同事使用,这是一个很好的选择,因为会有一个逻辑边界,并且您不会最终将数据泄漏到其他组织中。

对于多组织应用,您希望更改配置的原因有两个。假设我们有一家向企业销售电脑用品的网络商店。我们假设我们的大多数客户已经拥有 AAD–我们没有实现我们自己的用户数据库,而是提供客户租户的 AAD 登录。尽管我们有一个销售共享数据库,但我们可以强制执行,例如,只有从contoso.com登录的用户才能访问标有Contoso作为公司名称的订单。

另一个稍微不同的设置是,我们是一家向企业销售软件的 ISV。如果一家公司已经在使用 AAD,单点登录通常是他们的首选。该应用的架构可以让人产生一种为一个组织服务的错觉,但它可以在不同的公司间重用一套通用的用户管理。

多租户应用中的默认设置是允许 AAD 中的所有租户进行身份验证。如果需要,可以通过编辑令牌验证参数来限制这一点,但最重要的是,您还需要了解授权设置。

理解同意和许可

您在运行应用时被要求授予权限,但我们没有真正解释这一部分。基本概念应该很容易理解–如果您使用其他 Microsoft 服务,例如 Office 365,您的 AAD 帐户可能会解锁对大量数据的访问。我们不希望应用能够获取它想要的任何东西,因此作为一种保护措施,该应用必须请求访问,并且必须获得授权。

有两种类型的权限:

  • 委托权限是在用户上下文中有效的权限。例如,如果应用想要读取您的日历,您作为用户必须授予此权限。您的同意仅适用于您–它不允许应用读取其他用户的日历。
  • 应用权限是在更广泛的应用上下文中有效的权限,通常在后端。比如说,应用需要能够列出组织中的所有用户——这不是特定于您的数据。此权限需要由全局管理员授予。这意味着,如果您不是全局管理员,并且没有这些权限应用无法运行,则在组织中具有相应角色的人员同意之前,您不能使用该应用。

如前所述,代码中这些权限的技术术语为范围。默认的 OIDC 流请求offline_accessUser.Read作用域,如果您想读取日历,您可以添加Calendars.Read。这可以在Startup.cs中找到:

public void ConfigureServices(IServiceCollection services){        services.AddAuthentication     (OpenIdConnectDefaults.AuthenticationScheme)  .AddMicrosoftIdentityWebApp(options =>  {    Configuration.Bind(''AzureAD'', options);    options.ResponseType = ''code'';    options.SaveTokens = true;    options.Scope.Add(''offline_access'');    options.Scope.Add(''User.Read'');    options.Scope.Add(''Calendars.Read'');  });…

请注意,虽然不会再次提示您同意不同的登录之间的同一组权限,但如果应用请求的范围超过您最初同意的,则您需要重新同意。

您可能会想,我们如何确定作用域的名称?如果您在 Azure 门户中找到应用注册,您可以动态浏览列表,如图 8.12所示:

Figure 8.12 – Permissions list in the Azure portal

图 8.12–Azure 门户中的权限列表

当然,对于 Microsoft API,它也列在在线文档中,因此您不必猜测权限的名称。

拥有阅读日历的权限是有帮助的,但这并不意味着日历条目开始自己涌入。这需要更多的代码。不过,我们需要先阐述几个概念。

AAD 租户中的每个用户都可以进行身份验证并获取令牌。这是通过 AAD 端点完成的,在我们使用的代码中,这是通过Microsoft.Identity.Web库完成的。这是为了后端使用,例如运行服务器端的 web 应用(我们使用 Blazor 服务器)和受保护的 web API。

为了在客户机上获取令牌,我们使用一个不同的库,称为Microsoft 身份验证库MSAL),它可以在 C#中的本机应用、基于 JavaScript 的 web 应用等上运行。它使用相同的端点,但实现不同的 OAuth 流。在互联网上搜索时,你可能会遇到一个名为ADAL的图书馆,这是一个旧的、不推荐使用的图书馆;你不应该再使用它了。

日历数据取决于是否拥有 Office 365 许可证。这些数据通过 Microsoft Graph 公开,Microsoft Graph 是许多 Microsoft 服务的网关,提供了一个一致的 API 界面。要与 Microsoft Graph 交互,可以在使用上述库之一获取令牌后使用 Microsoft Graph NuGet 包。

说到这里,我们可以回到如何阅读日历条目的问题上来。

客户机已经获得了一个令牌,因此第一种方法可能是认为可以相当容易地利用它。但应用无法直接访问令牌,因为它存储在浏览器会话中,因此您需要通过一些额外的步骤来检索它。幸运的是,微软已经通过Microsoft.Identity.Web库简化了这些步骤。

在幕后,库代表(海外建筑运营管理局)调用名为的 OAuth 流。这里我们并没有描绘流程的全貌,但高级视图是,应用首先允许用户进行身份验证,然后再使用令牌对身份提供商进行第二次调用,并对其本身进行身份验证。这使应用能够在拥有大量后端 API 时构建更复杂的场景。

为了实现这一目标,我们必须做几件事:

  1. 转到 Azure 门户并在 AAD 中找到应用注册。
  2. 进入API 权限刀片,点击添加权限
  3. 选择微软图形,即委托权限权限类型,在列表中定位Calendars.ReadCalendars.ReadWrite
  4. 点击添加权限
  5. 进入证书和机密刀片,点击新客户机密。在点击添加之前,给它一个名称,如MySecret并选择其到期时间。
  6. 立即复制该秘密的副本,因为它在离开页面后将无法检索。
  7. 将新配置添加到appsettings.json

    cs ''AzureAd'': {  …  ''ClientSecret'': ''copied from the portal'',  ''CallbackPath'': ''/signin-oidc''},''Graph'': {  ''BaseUrl'': ''https://graph.microsoft.com/v1.0'',  ''Scopes'': ''user.read calendars.read calendars.    readwrite''},''Logging'': {

  8. 返回到Startup.cs并将我们之前添加的代码更改为:

    cs string[] initialScopes = Configuration.GetValue<string>(''Graph:Scopes'')?.Split(' ');          services.AddAuthentication(OpenIdConnectDefaults.             AuthenticationScheme)                    .AddMicrosoftIdentityWebApp(Configuration.GetSection(''AzureAd''))                       .EnableTokenAcquisitionToCallDownstreamApi (initialScopes)                      .AddInMemoryTokenCaches()                      .AddMicrosoftGraph(Configuration.GetSection(''Graph''));

  9. Since this is a Blazor app, we will add a page called Calendar to show the calendar entries. The first part is adding the following at the top:

    cs @page ''/Calendar''@using Microsoft.Graph @inject Microsoft.Graph.GraphServiceClient GraphClient

    注入的GraphClient负责传递调用 Microsoft Graph 所需的令牌。

  10. 您需要一个代码段来实际调用图形:

    cs @code{  private List<Event> eventList = new List<Event>();   protected override async Task OnInitializedAsync()  {    try     {                        var events = await GraphClient.Me.Events.Request()                   .Select(''subject,body,organizer,start,end,location'')      .GetAsync();       eventList = events.CurrentPage.ToList();    }    catch (Exception ex)            {      var error = ex.Message;    }  }}

  11. Then, you need to print it all out, as shown in the following code block:

    cs <AuthorizeView>  <Authorized>           <table class=''table''>      <thead>        <tr>          <th scope=''col''>Subject</th>          <th scope=''col''>Start</th>          <th scope=''col''>Entry</th>        </tr>      </thead>      <tbody>        @foreach (var entry in eventList)        {          <tr>            <td>@entry.Subject</td>            <td>@entry.Start.DateTime.ToString()</td>            <td>@entry.End.DateTime.ToString()</td>          </tr>        }      </tbody>    </table>  </Authorized>  <NotAuthorized>    <p>For full functionality please log in</p>    <a href=''MicrosoftIdentity/Account/SignIn''>Log in     </a>  </NotAuthorized></AuthorizeView>

    我们将包装在AuthorizeView内,以避免因未登录而产生的任何错误–如果您未登录,您将无法获得任何数据,因此从这个意义上讲跳过它并不危险,但我们喜欢对用户有意义的消息,而不是不起作用的消息。

  12. 运行应用并手动将/Calendar添加到 URL,您应该会看到一个条目列表,如图 8.13所示:

Figure 8.13 – Calendar entries

图 8.13–日历条目

请注意,在调试模式下运行时,您可能需要注销并再次登录才能在使用令牌时正常工作。这可能是由于在两次运行之间(使用内存缓存时)清空令牌缓存时,浏览器存储会话造成的。

我们已经走了很长一段路,但仍有一些事情需要考虑,例如扩展到您当前的 AAD 租户之外。

使用联邦身份

由于您与分配给您的特定 AAD 租户集成,因此很容易将其视为您的身份提供者。不过,微软的运营规模更大,而且在技术层面上,您正在与外部身份提供商联合。

那么,这到底意味着什么?

回到现实世界中我们最初的例子,你可以说护照是联邦身份的一个例子。即使您不是签发护照的实体,您也相信签发机关有一个良好的程序,并且您接受该程序作为身份证明。您可以选择不信任此身份,并构建自己的系统来验证人们是否是他们所说的人,但如果您能够提供相同级别的真实性,则很可能会耗费大量时间和成本。在不同国家订购护照的麻烦程度可能会有所不同,但试想一下,作为一名旅行者,在你旅行到的不同国家获得多张护照是多么不友好。

在过去的几年里,你很可能看到了在你访问过的网站上使用 Facebook 或 Google 登录的选项。您可以单击这些按钮,而不是创建新帐户。只要您接受网站能够读取您的某些身份属性,您就可以继续。当然,这些提供商的信任度可能低于你们国家的联邦实体,但很可能他们已经投入了相当大的努力来确保他们的用户帐户数据库是安全的,不太容易被黑客攻击。对你来说,作为一个用户,他们让你不用再去想另一个要记住的密码了。

护照和谷歌账户都是联邦身份的例子。虽然您的应用可能有一个用于访问和授权目的的用户数据库,但您只有一个对其身份的引用,因为该身份是由您信任的其他人提供的,您可以提供身份验证服务。

在高层次上,您需要在所选身份提供程序的控制窗格中为应用创建一个帐户,在该窗格中,您提供了两个相关属性,并且相应地,您可以像上一节中那样配置元数据,指向身份提供程序。

.NET5 和 ASP.NETCore5 提供了一些库,这些库可以帮助您完成这项工作,而单独完成并不一定很难。然而,在你的应用的生命周期中发生的事情是,你从谷歌和 Facebook 开始,它正在工作。然后,有人要求你添加苹果,让 iOS 用户更容易使用。然后添加一个使用“姓氏”而不是“姓氏”的提供程序,打破数据模型。即使您的回答是您喜欢挑战,也可能是由于您开始添加越来越多的逻辑来处理它,需要新的构建和发布,因此您的登录代码变得臃肿,从而导致了摩擦。

正如你可能猜到的,这导致不可避免的出现了一个 Azure 服务。AAD 的一个版本称为 AAD B2C,旨在处理此类场景。B2C部分代表企业对消费者,但它实际上是关于外部身份的。它的工作方式是,您建立一个嵌套的联盟,您的应用信任 AAD B2C,AAD B2C 反过来信任其他身份提供商。如果您需要添加新的提供商或自定义声明,您可以在 Azure 中这样做,而无需重新编译应用。

AAD B2C 中实际上有两种类型的用户帐户:本地帐户和社交帐户。在本文中,Social 是 federated 的另一个术语,因为它本身不必是社交网络上的帐户。美妙之处在于,有几个预先创建的提供者可以通过一个向导轻松添加,如图 8.14所示:

Figure 8.14 – Identity provider selection

图 8.14–身份提供者选择

如果您的提供程序不在列表中,则可以添加通用 OIDC 提供程序。如果您想要非标准配置,您甚至可以添加一个非 B2C AAD 租户作为身份提供者。

本地帐户不与其他提供商联合,而是 AAD 的专用版本,用于添加具有任何电子邮件地址的个人帐户。一个普通的广告租户通常是一个组织,在这个组织中,用户可以查找其他用户的详细信息,成为组的一部分,等等,这是很正常的。在 B2C 租户中,每个用户都是一个孤岛,无法看到其他用户。如果您还记得我们以数据库的形式创建本地帐户的示例,您可以说这与此相竞争,但它的功能更强大,而且在大多数情况下,比维护自己的数据库更易于使用。

可以通过向导配置不同类型的用户旅程(注册、登录、密码重置),如果愿意,还可以替换样式。

如果您想更深入,还可以选择使用自定义策略,这需要深入 XML 文件以获得类似于编码的体验。它提供了极大的灵活性,可以在流程中调用后端 API,等等。请注意,这可能与用户友好相反,因此只有在向导驱动的策略没有涵盖您的用例时才使用它。

虽然 AAD B2C 具有与常规 AAD 不同的功能集,但用于获取令牌的端点也符合标准,因此调整代码相当容易。

在基本形式中,您实际上可以使用与我们使用常规 AAD 进行身份验证相同的代码,并将appsettings.json更改为指向 B2C 租户,该租户具有在该租户中创建的属性。如果您只定义了一个处理注册和登录的流,那么这将非常有效。如果您还想提供诸如密码重置和配置文件编辑等选项,则该选项将不起作用。

在全面了解 AAD B2C 服务之前,推荐的入门方法是让 Visual Studio 在 Visual Studio 中创建项目时选择使用 B2C 作为提供程序,从而为您生成内容。可在个人用户账户连接到云中已有的用户存储下找到选择,如图 8.15所示:

Figure 8.15 – AAD B2C authentication options

图 8.15–AAD B2C 认证选项

乍一看,AAD B2C 似乎为不明确的好处增加了复杂性,因为这些事情可以直接在代码中实现。需要明确的是,就像许多其他事情一样,有好的用例,也有不太好的用例。最棒的是,如果你想使用 B2C,它只需要对代码进行很少的更改,而且 AAD B2C 中的大部分工作可以“外包”给身份认证专家。

关于身份信息系统的一点注记

无论您是从头开始编写自己的身份实现,还是依赖 AAD,如果用户要输入用户名和密码,您都需要一个 UI。一般来说,有三种不同的方法来实现这一点:

  • 弹出:您可以打开一个单独的较小窗口,供用户输入凭证。一旦它们被验证,弹出窗口就会消失,而你又回到了 web 应用中。从技术角度来看,这种方法没有什么错,但许多用户在浏览器中阻止了弹出窗口,许多人认为它是一个恼人的 UI。
  • 重定向:我们在与 AAD 集成时实现的方法是基于重定向的。您从https://localhost开始,被发送到https://microsoftonline.com,然后再次返回https://localhost。这是一种非常常见的方法。它很容易实现,并且以安全的方式支持我们描述的流。
  • Iframe:最圆滑的方法可能是将登录表单作为 web app 的一部分嵌入,并将用户保持在相同的上下文中。为了实现这一点,您需要在后端使用 cookie 和会话执行一些技巧。当您控制所有内容时,这不是一个问题,但是如果您想要使用联合身份,这将成为一个问题。单租户 AAD 理论上可以支持 Iframe,但在撰写本书时没有这样做。Facebook 和谷歌等提供商不支持该服务,原因是存在安全隐患——例如,创建用于获取密码的登录体验。此外,主要浏览器正在实施更多阻止第三方 cookie 的机制,以确保隐私,因此可能也会在那里被阻止。在尝试实现此 UI 之前,请确保您掌握了所有移动部件。

总结

本章带领我们从基本身份验证到联邦身份验证。它首先解释了身份验证和授权的含义。这里有一些细节,比如了解 Base64 编码和哈希的好处。AuthN 和 AuthZ 的示例实现旨在让您更好地了解正在发生的事情,尽管您可能不会实现或使用所有这些技术。OAuth 的演练和 AAD 的引入将使您能够在 web 应用中实现产品级标识。

并不是每个应用都需要超级安全,但这应该让你的网络应用比将所有访问者视为匿名用户更加个人化。

随着身份的讨论,下一章将深入到另一个热门话题,我们将介绍使用容器的细节。

问题

  1. 身份验证和授权之间有什么区别?
  2. 对于 web 应用中的前端用例,哪种 OAuth 流最常见且最推荐?
  3. 你为什么要使用 AAD B2C?

进一步阅读