如何用Auth0保证Razor Pages应用程序的安全

186 阅读13分钟

Razor Pages是在ASP.NET Core中创建Web应用程序的编程模型之一。让我们看看如何使用Auth0 ASP.NET Core Authentication SDK添加认证支持。

Razor Pages与ASP.NET Core MVC的对比

当谈到用ASP.NET构建Web应用程序时,你会发现自己不得不在几种编程模型中做出选择。抛开单页应用(SPA),只关注.NET的最新版本,你有两种编程模型来创建传统的Web应用。ASP.NET Core MVC和Razor Pages。你应该为你的网络应用程序选择哪种编程模型?

ASP.NET Core MVC模型比Razor Pages模型更受欢迎,也许是因为ASP.NET Core MVC有更悠久的传统,它始于2009年的ASP.NET MVC。实际上,这并不意味着这种编程模型比Razor Pages模型更好。

这两种编程模型都依赖于相同的模板引擎--Razor。然而,ASP.NET Core MVC提倡模型-视图-控制器(MVC)设计模式,而Razor Pages应用程序提出了一种更轻、更注重页面的方法。因此,当你不得不选择为你的网络应用程序使用哪种编程模式时,你应该仔细评估你的应用程序的行为适合在哪里。正如微软文档中所说,"如果你的ASP.NET MVC应用大量使用视图,你可能要考虑从动作和视图迁移到Razor Pages"。

也就是说,如果你有使用这两种编程模型的经验,你可能会注意到这两种模型之间的边界不是那么整齐。由于这两种模型共享相同的模板引擎,你可能会发现ASP.NET Core MVC应用程序在需要一个简单页面时使用Razor Pages。在另一边,你可能会发现Razor Pages应用程序在有意义的地方使用控制器的功能。

混合这两种模式可以让你取其精华,构建高效的Web应用。

要了解更多关于Razor Pages的信息,请查看官方文档。本文将重点介绍用Auth0保护Razor Pages应用程序的安全。如果你想在ASP.NET Core MVC应用程序中添加认证,请查看这篇文章

示例应用程序

本文不会让你从头开始建立一个Razor Pages应用程序。相反,你将修改一个用C# 10构建的现有样本项目。这意味着你需要在你的机器上安装.NET 6 SDK。要了解更多关于.NET 6所引入的新功能,请查看这篇文章

虽然本文的说明将促使你使用.NET CLI来构建和运行应用程序,但如果你愿意,你也可以使用Visual Studio 2022。

获取并运行示例应用程序

你可以在终端窗口中运行以下命令,在你的机器上下载示例应用程序。

git clone -b starter --single-branch https://github.com/auth0-blog/acme-aspnet-razor.git

一旦你下载了它,移动到 acme-aspnet-razor文件夹,并键入以下命令来启动该应用程序。

dotnet watch

该命令将运行样本应用程序,并等待对代码可能的修改。如果你改变了应用程序的代码,它将被自动重新构建。

请注意,对你的代码的一些特定的改变,即所谓的粗暴的编辑,可能需要重新启动你的应用程序。阅读这个可以了解更多。

几秒钟后,你的应用程序就启动并运行了。将你的浏览器指向 https://localhost:7204.你应该看到以下页面。

ACME website home page

这是虚构的公司ACME公司的主页。

通过点击标题中的目录链接,你可以浏览他们的目录,看起来如下所示。

ACME catalog

实际上,"*现在购买 "*按钮并不工作。这个页面只是一个占位符,用户会期望这个页面受到保护。换句话说,只有经过认证的用户才能访问这个目录页面。这就是你在接下来的几节中要实现的。

向Auth0注册

在做任何修改之前,你必须在Auth0注册该应用程序。当然,你需要一个Auth0账户。如果你还没有一个,你可以免费注册

一旦进入仪表板,移到应用程序部分,并遵循这些步骤。

  1. 单击 "创建应用程序"。
  2. 为你的应用程序提供一个友好的名称(例如,ACME Web App),并选择常规Web应用程序作为应用程序类型。
  3. 最后,点击创建按钮。

这些步骤使Auth0知道你的ASP.NET Core应用程序,并将允许你控制对它的访问。

应用程序创建后,移到设置选项卡,注意你的Auth0域和你的客户ID。我们很快就会用到它们。

然后,在同一个表格中,将值分配给 https://localhost:7204/callback到允许的回调URLs字段,并将值 https://localhost:7204/到允许注销的URLs字段。

第一个值告诉Auth0在用户认证后要回调哪个URL。第二个值告诉Auth0,用户在注销后应该被重定向到哪个URL。

点击 "保存更改"按钮来应用它们。

现在,回到示例应用程序项目的根文件夹,打开 appsettings.json配置文件,并将其内容替换为以下内容。

// appsettings.json
{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  },
  "AllowedHosts": "*",
  "Auth0": {
    "Domain": "YOUR_DOMAIN",
    "ClientId": "YOUR_CLIENT_ID"
  }
}

替换占位符 YOUR_DOMAINYOUR_CLIENT_ID替换为从Auth0仪表板上获取的相应值。

添加认证

在这一点上,连接你的应用程序和Auth0的基本设置已经到位。要添加认证,你需要对你的应用程序进行一些修改,并使用这些设置。让我们一步步来。

安装Auth0 SDK

第一步,在终端窗口运行以下命令,安装Auth0 ASP.NET Core Authentication SDK

dotnet add package Auth0.AspNetCore.Authentication

Auth0 ASP.NET Core Authentication SDK可以让你轻松地将基于OpenID Connect的认证整合到你的应用程序中,而不需要处理所有低级别的细节。

如果你想了解更多,这篇博文为你提供了Auth0 ASP.NET Core Authentication SDK的概述

设置认证

现在,让我们修改应用程序代码以支持认证。打开该 Program.cs文件,对其内容做如下修改。

// Program.cs

using Auth0.AspNetCore.Authentication; // 👈 new code

var builder = WebApplication.CreateBuilder(args);

// 👇 new code
builder.Services
    .AddAuth0WebAppAuthentication(options => {
      options.Domain = builder.Configuration["Auth0:Domain"];
      options.ClientId = builder.Configuration["Auth0:ClientId"];
    });
// 👆 new code

// Add services to the container.
builder.Services.AddRazorPages();

var app = builder.Build();

// Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}

app.UseHttpsRedirection();
app.UseStaticFiles();

app.UseRouting();

app.UseAuthentication(); // 👈 new code
app.UseAuthorization();

app.MapRazorPages();

app.Run();

在突出显示的代码之后,你在文件的开头添加了一个对 Auth0.AspNetCore.Authentication命名空间的引用,在文件的开头。然后你调用了 AddAuth0WebAppAuthentication()方法,将Auth0域和客户ID作为参数。最后,你调用了 UseAuthentication()方法来启用认证中间件。请确保在调用 UseAuthentication()之前UseAuthorization().

这些变化为支持通过Auth0认证奠定了基础。

实施登录

为了实现登录,请移动到项目的根文件夹,在终端窗口运行以下命令,添加一个新的Razor页面。

dotnet new page --name Login --namespace acme.Pages --output Pages/Account

这个命令将在Pages 文件夹中创建一个Account 文件夹,并在那里添加两个文件。 Login.cshtmlLogin.cshtml.cs.打开该 Login.cshtml.cs文件并将其内容替换为以下内容。

// Pages/Account/Login.cshtml.cs

using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.AspNetCore.Authentication;
using Auth0.AspNetCore.Authentication;

namespace acme.Pages;

public class LoginModel : PageModel
{
  public async Task OnGet(string returnUrl = "/")
  {
    var authenticationProperties = new LoginAuthenticationPropertiesBuilder()
                .WithRedirectUri(returnUrl)
                .Build();

    await HttpContext.ChallengeAsync(Auth0Constants.AuthenticationScheme, authenticationProperties);
  }
}

这个类定义了登录页面的页面模型。实际上,这个页面根本就没有显示。它只是创建了一套登录所需的认证属性,并通过Auth0触发了认证过程。

为了启用这个登录操作,你需要改变用户界面。因此,打开 _Layout.cshtml文件夹下的 Pages/Shared文件夹下的文件,并更新其内容如下。

@* Pages/Shared/_Layout.cshtml *@

<!DOCTYPE html>
<html lang="en">
  
<!-- ...existing code -->
  
  <div class="navbar-collapse collapse d-sm-inline-flex justify-content-between">
    // 👇 new code
    @if (User.Identity.IsAuthenticated)
    {
    // 👆 new code
      <ul class="navbar-nav flex-grow-1">
        <li class="nav-item">
          <a class="nav-link text-dark display-6" asp-area=""
             asp-page="/Catalog">Catalog</a>
        </li>
      </ul>
    // 👇 new code
    } else {
      <ul class="navbar-nav ms-auto">
        <li class="nav-item">
          <a class="nav-link text-dark display-6" asp-area=""
            asp-page="Account/Login">Login</a>
        </li>
      </ul>
    }
    // 👆 new code
  </div>
  
<!-- ...existing code -->

你修改页面的标记,在 User.Identity.IsAuthenticated属性的检查。正如你可能已经猜到的那样,这个属性让你知道当前用户是否经过认证。如果用户没有通过认证,导航栏的右侧将显示一个登录链接。

测试登录

一切都准备好了。在浏览器中刷新页面,你应该看到登录链接,如下图所示。

ACME home page with login

当你点击该链接时,你将被重定向到Auth0通用登录页面,如下截图所示。

Log in with Auth0

用户第一次认证时,会被提示有一个类似以下的页面。

Consent screen

这个页面被称为同意屏幕,它通知用户,样本应用程序将访问他们的用户资料数据。接受后,你会被重定向到样本应用程序,应该看到导航栏左侧出现了目录链接。

ACME home page after login

祝贺你!你在你的ASP.NET应用程序中添加了认证。你在你的ASP.NET Core Razor Pages应用程序中添加了认证!

保护私人页面

尽管你必须通过认证才能看到导航栏中的目录链接,但目录视图本身并没有受到保护。你可以通过访问 https://localhost:7204/Catalog地址来验证。

你需要限制只有通过认证的用户才能访问目录。

保护目录页

为此目的,编辑 Program.cs文件,如下图所示。

// Program.cs

using Auth0.AspNetCore.Authentication;

var builder = WebApplication.CreateBuilder(args);

builder.Services
    .AddAuth0WebAppAuthentication(options => {
      options.Domain = builder.Configuration["Auth0:Domain"];
      options.ClientId = builder.Configuration["Auth0:ClientId"];
    });

// Add services to the container.
// 👇 changed code
builder.Services.AddRazorPages(options =>
{
    options.Conventions.AuthorizePage("/Catalog");
});
// 👆 changed code

var app = builder.Build();

//...existing code...

你使用了Razor Pages授权约定来保护目录页面不被非法访问。这种方法允许你在你的 Razor Pages 应用程序中集中访问控制。请查看官方文档以了解更多关于Razor Pages授权约定的信息。

实现注销

使你的应用程序可用的另一个缺失的东西是注销。事实上,目前,当你登录到应用程序时,你会保持登录状态,直到你的会话过期。你可能想让你的用户明确地注销应用程序。为此,通过运行以下命令在Account 文件夹中添加一个注销页面。

dotnet new page --name Logout --namespace acme.Pages --output Pages/Account

然后,打开该 Logout.cshtml.cs文件并将其内容替换为以下内容。

// Pages/Account/Logout.cshtml.cs

using Microsoft.AspNetCore.Mvc.RazorPages;
using Auth0.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.Cookies;

namespace acme.Pages
{
    public class LogoutModel : PageModel
    {
        public async Task OnGet()
        {
    var authenticationProperties = new LogoutAuthenticationPropertiesBuilder()
        .WithRedirectUri("/")
        .Build();

    await HttpContext.SignOutAsync(Auth0Constants.AuthenticationScheme, authenticationProperties);
    await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
        }
    }
}

和登录页面一样,注销页面也不显示任何东西。它只是创建一组所需的属性,并触发注销过程,销毁Auth0会话和本地会话。

下一步是使注销链接对用户可用。因此,更新以下文件的内容 _Layout.cshtml文件夹下的 Pages/Shared文件夹下的文件内容如下。

@* Pages/Shared/_Layout.cshtml *@

<!DOCTYPE html>
<html lang="en">
  
<!-- ...existing code -->
  
  <div class="navbar-collapse collapse d-sm-inline-flex justify-content-between">
    @if (User.Identity.IsAuthenticated)
    {
      <ul class="navbar-nav flex-grow-1">
        <li class="nav-item">
          <a class="nav-link text-dark display-6" asp-area="" 
             asp-page="/Catalog">Catalog</a>
        </li>
      </ul>
      // 👇 new code
      <ul class="navbar-nav ms-auto">
        <li class="nav-item">
          <a class="nav-link text-dark display-6" asp-area="" 
             asp-page="/Account/Logout">Logout</a>
         </li>
       </ul>
      // 👆 new code
    } else {
      <ul class="navbar-nav ms-auto">
        <li class="nav-item">
          <a class="nav-link text-dark display-6" asp-area=""
             asp-page="/Account/Login">Login</a>
        </li>
      </ul>
      }
  </div>
  
<!-- ...existing code -->

你只是在用户被认证时,在导航栏的右侧添加了注销链接。该链接指向Account 文件夹中的注销页面。

作为最后一步,打开 Program.cs文件,并将注销页面配置为受保护的页面,如下图所示。

// Program.cs

//...existing code...

// Add services to the container.
builder.Services.AddRazorPages(options =>
{
  options.Conventions.AuthorizePage("/Catalog");
  // 👇 new code
  options.Conventions.AuthorizePage("/Account/Logout");
  // 👆 new code
});

//...existing code...

测试应用程序

现在是测试这个新版本的应用程序的时候了。首先,从你的浏览器中删除所有的cookies或使用隐身窗口。这是需要的,因为到目前为止你还没有机会注销。

现在,在你再次登录后,主页应该看起来如下。

ACME homepage with logout

当你点击注销链接时,你将与Auth0断开连接,看到通常的主页,只有登录链接。

通过对一些最臭名昭著的威胁的实践探索来学习网络安全。

下载免费电子书

Security for Web Developers

添加个人资料页

让我们试着更进一步,添加一个显示用户的一些数据的页面。

指定你的作用域

如前所述,Auth0 ASP.NET Core Authentication SDK使用OpenID Connect(OIDC)来验证你的用户。OIDC为你的应用程序提供了一个ID令牌,其中包含了一些关于用户的基本数据。从技术角度来看,这些用户数据是可用的,因为SDK默认会请求openidprofile 范围。你没有看到这一点,因为SDK负责整个认证过程并管理ID令牌。所以,默认情况下,你有用户的名字,可能还有他们的照片。如果你还想让用户的电子邮件出现在他们的资料中,你必须明确指定范围。

打开该 Program.cs文件,并应用以下修改。

// Program.cs

// ...existing code...

var builder = WebApplication.CreateBuilder(args);

builder.Services
    .AddAuth0WebAppAuthentication(options => {
      options.Domain = builder.Configuration["Auth0:Domain"];
      options.ClientId = builder.Configuration["Auth0:ClientId"];
      options.Scope = "openid profile email"; // 👈 new code
    });

// ...existing code...

你给Scope 选项分配了一个字符串,包含前面提到的默认作用域(openidprofile )和email 作用域。

这将导致电子邮件被添加到用户资料中。

创建个人资料页面

现在,让我们创建一个新的页面,以显示用户的个人资料数据。从项目的根文件夹中,运行以下命令。

dotnet new page --name Profile --namespace acme.Pages --output Pages/Account

像往常一样,它在文件夹中创建了两个新的文件 Pages/Account文件夹中。 Profile.cshtmlProfile.cshtml.cs.

打开这个 Profile.cshtml.cs文件并将其内容替换为以下内容。

// Pages/Account/Profile.cshtml.cs

using Microsoft.AspNetCore.Mvc.RazorPages;
using System.Security.Claims;

namespace acme.Pages;

public class ProfileModel : PageModel
{
  public string UserName { get; set; }
  public string UserEmailAddress { get; set; }
  public string UserProfileImage { get; set; }
  public void OnGet()
  {
    UserName = User.Identity.Name;
    UserEmailAddress = User.FindFirst(c => c.Type == ClaimTypes.Email)?.Value;
    UserProfileImage = User.FindFirst(c => c.Type == "picture")?.Value;
  }
}

这段代码简单地定义了ProfileModel 类,包含了用户资料的三个元素:姓名、电子邮件地址和用户的照片。

现在,定义相关的视图,将同一文件夹中的 Profile.cshtml文件的内容。

@* Pages/Account/Profile.cshtml *@

@page
@model acme.Pages.ProfileModel
@{
    ViewData["Title"] = "User Profile";
}

<div class="row">
    <div class="col-md-12">
        <div class="row">
            <h2>@ViewData["Title"].</h2>

            <div class="col-md-2">
                <img src="@Model.UserProfileImage"
                     alt="" class="img-rounded img-responsive" />
            </div>
            <div class="col-md-4">
                <h3>@Model.UserName</h3>
                <p>
                    <i class="glyphicon glyphicon-envelope"></i> @Model.UserEmailAddress
                </p>
            </div>
        </div>
    </div>
</div>

这个标记显示了用户模型的三个属性。

保护个人资料页

在继续测试这个新功能之前,让我们确保只有授权用户可以访问用户资料页面。因此,打开 Program.cs文件,并将简介页配置为受保护的页面,如下所示。

// Program.cs

//...existing code...

// Add services to the container.
builder.Services.AddRazorPages(options =>
{
  options.Conventions.AuthorizePage("/Catalog");
  options.Conventions.AuthorizePage("/Account/Logout");
  // 👇 new code
  options.Conventions.AuthorizePage("/Account/Profile");
  // 👆 new code
});

//...existing code...

你刚刚把个人资料页添加到需要授权的页面中。

链接个人资料页

最后,让我们使用户可以使用个人资料页。打开 _Layout.cshtml文件夹下的 Pages/Shared文件夹下的文件,更新其内容如下。

@* Pages/Shared/_Layout.cshtml *@

<!DOCTYPE html>
<html lang="en">
  
<!-- ...existing code -->
  
  <div class="navbar-collapse collapse d-sm-inline-flex justify-content-between">
    @if (User.Identity.IsAuthenticated)
    {
      <ul class="navbar-nav flex-grow-1">
        <li class="nav-item">
          <a class="nav-link text-dark display-6" asp-area="" 
             asp-page="/Catalog">Catalog</a>
        </li>
      </ul>
      <ul class="navbar-nav ms-auto">
        // 👇 new code
        <li class="nav-item">
          <a class="nav-link text-dark display-6" 
             asp-page="/Account/Profile">Hello @User.Identity.Name!</a>
        </li>
        // 👆 new code
        <li class="nav-item">
          <a class="nav-link text-dark display-6" asp-area="" 
             asp-page="/Account/Logout">Logout</a>
         </li>
       </ul>
    } else {
      <ul class="navbar-nav ms-auto">
        <li class="nav-item">
          <a class="nav-link text-dark display-6" asp-area=""
             asp-page="/Account/Login">Login</a>
        </li>
      </ul>
      }
  </div>
  
<!-- ...existing code -->

你把链接添加到个人资料视图中的注销链接旁边。该链接将显示一个带有用户名的欢迎信息。而且,只有在用户通过认证后才会显示。

测试用户资料

回到你的浏览器,再次进行认证。作为第一个区别,你会注意到你又看到了同意屏幕。为什么?你之前没有接受吗?

实际上,这一次你的应用程序正在要求你提供一个新的信息:电子邮件。如果你将这个屏幕与之前的屏幕进行比较,你会注意到这个微妙的区别。

Consent screen with new the email scope

在你接受该屏幕后,你的新主页将看起来如下。

ACME homepage with profile link

你可以看到注销链接旁边的欢迎词。如果你点击欢迎信息,用户资料将显示如下图所示。

User profile

概要

祝贺你!你完成了这个基本的旅程!你学会了如何通过Auth0在Razor Pages应用程序中集成认证。你已经看到了Auth0 ASP.NET Core Authentication SDK是如何在引擎盖下为你处理OpenID Connect的,从而避免你处理技术细节。你还学会了如何保护你的应用程序的私人页面以及如何实现注销。最后,你能够获得用户的数据来创建一个用户资料页面。

你可以在这个GitHub资源库中找到Razor Pages项目的完整代码。