一、ASP.NET Core 简介

不祥之兆是:如果你是一名. NET 开发人员,是时候尽快迁移到 ASP.NET Core 了(当然,如果你还没有的话)。虽然目前还不清楚微软何时会正式结束对现有 ASP.NET 框架的支持,但不会有新的版本,下一个版本的 ASP.NET Core 将只是“ASP。净 5”。幸运的是,对于厌倦学习新技术的开发人员来说,微软在让 Core 看起来和旧框架非常相似方面做得很好。然而,在表象之下,有许多显著的差异。

为了以对我们这些关心安全的人最有用的方式最好地理解它,让我们从深入研究 ASP.NET Core 站点是如何工作和构造的开始。由于 ASP.NET Core 是开源的,我们可以深入到框架的源代码本身,以了解它是如何工作的。如果您是 ASP.NET Core 的新手,这将是一个很好的介绍,让您了解这个框架与它的前辈有什么不同。如果你以前使用过 ASP.NET Core,这是一个深入研究源代码的机会,看看所有的东西是如何组合在一起的。

Note

当我包含微软的源代码时,我几乎总是会删除微软团队的注释,并替换与我试图表达的观点无关的代码,并用我自己的注释替换它们。我会给你一个链接,链接到我正在使用的代码,这样你就可以看到自己的原始代码。

了解服务

ASP.NET Core 不是一个庞大的整体框架,而是运行数百个多少有些关联的服务。为了了解这些服务如何工作以及如何相互交互,让我们先来看看它们是如何在代码中设置的。

服务是如何创建的

当您使用 Visual Studio 附带的模板创建一个全新的网站时,您应该注意到两个文件,Program.cs 和 Startup.cs。

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>();
      });
}

Listing 1-1Default Program.cs in a new website

从安全角度来看,除了在webBuilder.UseStartup<Startup>()中指定的类Startup之外,在清单 1-1 中没有什么可看的。我们一会儿就会破解这段代码。但是首先,有一个概念需要马上理解:ASP.NET Core 大量使用依赖注入。不是直接实例化对象,而是定义服务,然后将服务传递给构造函数中的对象。这种方法有多种优势:

  • 创建单元测试更容易,因为您可以轻松地交换特定于环境的服务(如数据库访问)。

  • 添加新功能更容易,比如添加新的认证方法,而无需重构现有代码。

  • 通过移除现有服务并添加新的(可能更好的)服务来更改现有功能更容易。

要了解如何设置和使用依赖注入,让我们打开 Startup.cs 中的Startup类。

public class Startup
{
  public Startup(IConfiguration configuration)
  {
    Configuration = configuration;
  }

  public IConfiguration Configuration { get; }

  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();
    services.AddRazorPages();
  }

  public void Configure(IApplicationBuilder app,
    IWebHostEnvironment env)
  {
    //Code we’ll talk about later
  }
}

Listing 1-2Default Startup.cs in a new website (comments removed)

清单 1-2 中有两行代码可供调用。首先,在构造函数中,传入了一个类型为IConfiguration的对象。符合IConfiguration接口的对象在代码中的其他地方定义,作为服务添加到框架中,然后依赖注入框架知道在启动类请求时将对象添加到构造函数中。你会在框架和本书中一遍又一遍地看到这种方法。

其次,我们将深入探讨services.AddDefaultIdentity。在我看来,从安全的角度来看,身份和密码管理是 ASP.NET 最需要关注的领域,所以我们将在本书的后面更详细地探讨这一点。现在,我只想以它为例向您展示如何添加服务。好在微软把 ASP.NET Core 代码开源了,我们可以下载源代码,可以在他们的 GitHub 资源库 https://github.com/aspnet/AspNetCore/ 找到,破解打开方法。

public static class IdentityServiceCollectionUIExtensions
{
  public static IdentityBuilder AddDefaultIdentity<TUser> ↲
    (this IServiceCollection services) where TUser : class
      => services.AddDefaultIdentity<TUser>(_ => { });

  public static IdentityBuilder AddDefaultIdentity<TUser> ( ↲
    this IServiceCollection services, ↲
    Action<IdentityOptions> configureOptions) ↲
      where TUser : class
  {
    services.AddAuthentication(o =>
    {
      o.DefaultScheme = IdentityConstants.ApplicationScheme;
      o.DefaultSignInScheme = ↲
                           IdentityConstants.ExternalScheme;
    })
    .AddIdentityCookies(o => { });

    return services.AddIdentityCore<TUser>(o =>
    {
      o.Stores.MaxLengthForKeys = 128;
      configureOptions?.Invoke(o);
    })
      .AddDefaultUI()
      .AddDefaultTokenProviders();
    }
  }
}

Listing 1-3Source code for services.AddDefaultIdentity()1

Note

这段代码是 3.1 版本。那个。NET 团队似乎经常重构设置初始服务的代码,所以它很可能会因。净 5。不过,我不认为这种添加服务的方法会改变,所以让我们看看 3.1 版本,即使在 5.x 中细节可能会改变。

清单 1-3 中添加了几个服务,但是从这段代码中看不出来。要查看添加的服务,我们需要更深入地挖掘,所以让我们看一看services.AddIdentityCore()

public static IdentityBuilder AddIdentityCore<TUser>(↲
  this IServiceCollection services, ↲
  Action<IdentityOptions> setupAction)
    where TUser : class
{
  services.AddOptions().AddLogging();

  services.TryAddScoped<IUserValidator<TUser>, ↲
    UserValidator<TUser>>();
  services.TryAddScoped<IPasswordValidator<TUser>, ↲
    PasswordValidator<TUser>>();
  services.TryAddScoped<IPasswordHasher<TUser>, ↲
    PasswordHasher<TUser>>();
  services.TryAddScoped<ILookupNormalizer, ↲
    UpperInvariantLookupNormalizer>();
  services.TryAddScoped<IUserConfirmation<TUser>, ↲
    DefaultUserConfirmation<TUser>>();
  services.TryAddScoped<IdentityErrorDescriber>();

  services.TryAddScoped<IUserClaimsPrincipalFactory<TUser>, ↲
    UserClaimsPrincipalFactory<TUser>>();
  services.TryAddScoped<UserManager<TUser>>();

  if (setupAction != null)
  {
    services.Configure(setupAction);
  }

  return new IdentityBuilder(typeof(TUser), services);
}

Listing 1-4Source for services.AddIdentityCore()2

您可以看到在清单 1-4 中添加了八个不同的服务,它们都是用TryAddScoped方法添加的。

术语“作用域”与服务的生命周期有关——作用域服务的每个请求有一个实例。在大多数情况下,不同生命周期之间的差异是出于性能原因,而不是出于安全原因,但这里仍然值得简要概述不同类型的 3 :

  • 瞬态:每次需要时创建一个实例。

  • 作用域:每个请求创建一个实例。

  • Singleton :一个实例由多个请求共享。

我们将在本书的后面创建服务。不过现在,重要的是要知道 ASP.NET Core 网站的架构是基于这些多少有些关联的服务。大多数实际的框架代码,以及我们可以更改的所有逻辑,都存储在这样或那样的服务中。当我们需要用更安全的东西替换现有的微软服务时,了解这一点将变得有用。

如何使用服务

现在我们已经看到了如何添加服务的示例,让我们通过跟踪用于验证用户密码的服务和方法来看看如何使用它们。ASP.NET 团队已经停止在项目中包含默认登录页面,但至少他们有一个简单的方法将它添加回来。为此,您需要

  1. 右击您的 web 项目。

  2. 将鼠标悬停在“添加”上

  3. 点击“新建脚手架项目”

  4. 在左侧,点击“身份”

  5. 点击“添加”

  6. 选中“覆盖所有文件”

  7. 选择数据上下文类。

  8. 点击“添加”

Note

我肯定有很多人建议你不要出于安全目的这样做。如果微软需要给他们的模板化代码添加一个补丁(就像几年前他们忘记在登录部分的一个方法中添加一个反 CSRF 令牌时所做的那样),那么如果你做了这个改变,你就不会得到它。然而,他们的登录代码有足够多的问题,只有添加这些模板才能解决,没有这些补丁你也只能活下去。

既然您的项目中已经有了默认登录页面的源代码,那么您可以在 Areas/Identity/Pages/Account/log in . cs html . cs 中查看源代码的一个略加简化和重新格式化的版本。

[AllowAnonymous]
public class LoginModel : PageModel
{
  private readonly UserManager<IdentityUser> _userManager;
  private readonly SignInManager<IdentityUser> _signInManager;
  private readonly ILogger<LoginModel> _logger;

  public LoginModel(SignInManager<IdentityUser> signInManager,
            ILogger<LoginModel> logger,
            UserManager<IdentityUser> userManager)
  {
    _userManager = userManager;
    _signInManager = signInManager;
    _logger = logger;
  }

  //Binding object removed here for brevity

  public async Task OnGetAsync(string returnUrl = null)
  {
    //Not important right now
  }

  public async Task<IActionResult> OnPostAsync(
    string returnUrl = null)
  {
    returnUrl = returnUrl ?? Url.Content("~/");

    if (ModelState.IsValid)
    {
      var result = await _signInManager.PasswordSignInAsync(↲
        Input.Email, ↲
        Input.Password, ↲
        Input.RememberMe, ↲
        lockoutOnFailure: false);

      if (result.Succeeded)
      {
        _logger.LogInformation("User logged in.");
        return LocalRedirect(returnUrl);
      }
      if (result.RequiresTwoFactor)
      {
        return RedirectToPage("./LoginWith2fa", new { ↲
          ReturnUrl = returnUrl, ↲
          RememberMe = Input.RememberMe ↲
        });
      }
      if (result.IsLockedOut)
      {
        _logger.LogWarning("User account locked out.");
        return RedirectToPage("./Lockout");
      }
      else
      {
        ModelState.AddModelError(string.Empty, ↲
          "Invalid login attempt.");
        return Page();
      }
    }

    return Page();
  }
}

Listing 1-5Source for default login page code-behind

稍后我们将对此进行更深入的探讨,但是在清单 1-5 中有两行代码非常重要。首先是构造函数。SignInManager是框架中定义的对象,它处理大部分认证。虽然我们没有显式地看到代码,但它是在我们之前调用services.AddDefaultIdentity时作为服务添加的,所以我们可以简单地在LoginModel类的构造函数中请求它,依赖注入框架会提供它。第二,我们可以看到,似乎是SignInManager在进行登录的实际处理。让我们通过深入到SignInManager类的源代码来更深入地研究这个问题,去掉不相关的方法,重新排序相关的方法,这样对你来说更有意义。

public class SignInManager<TUser> where TUser : class
{
  private const string LoginProviderKey = "LoginProvider";
  private const string XsrfKey = "XsrfId";

  public SignInManager(UserManager<TUser> userManager,
    //Other constructor properties
  )
  {
    //Null checks and local variable assignments
  }

  //Properties removed for the sake of brevity

  public UserManager<TUser> UserManager { get; set; }

  public virtual async Task<SignInResult> ↲
    PasswordSignInAsync(string userName, string password,
      bool isPersistent, bool lockoutOnFailure)
  {
    var user = await UserManager.FindByNameAsync(userName);
    if (user == null)
    {
      return SignInResult.Failed;
    }

    return await PasswordSignInAsync(user, password, ↲
      isPersistent, lockoutOnFailure);
  }

  public virtual async Task<SignInResult> ↲
    PasswordSignInAsync(TUser user, string password,
      bool isPersistent, bool lockoutOnFailure)
  {
    if (user == null)
    {
      throw new ArgumentNullException(nameof(user));
    }

    var attempt = await CheckPasswordSignInAsync(user, ↲
      password, lockoutOnFailure);
    return attempt.Succeeded
      ? await SignInOrTwoFactorAsync(user, isPersistent)
      : attempt;
  }

  public virtual async Task<SignInResult> ↲
    CheckPasswordSignInAsync(TUser user, string password, ↲
      bool lockoutOnFailure)
  {
    if (user == null)
    {
      throw new ArgumentNullException(nameof(user));
    }

    var error = await PreSignInCheck(user);
    if (error != null)
    {
      return error;
    }

    if (await UserManager.CheckPasswordAsync(user, password))
    {
      var alwaysLockout = ↲
      AppContext.TryGetSwitch("Microsoft.AspNetCore.Identity.↲
        CheckPasswordSignInAlwaysResetLockoutOnSuccess", ↲
        out var enabled) && enabled;

      if (alwaysLockout || !await IsTfaEnabled(user))
      {
        await ResetLockout(user);
      }

      return SignInResult.Success;
    }
    Logger.LogWarning(2, "User {userId} failed to provide ↲
      the correct password.", await ↲
      UserManager.GetUserIdAsync(user));

    if (UserManager.SupportsUserLockout && lockoutOnFailure)
    {
      await UserManager.AccessFailedAsync(user);
      if (await UserManager.IsLockedOutAsync(user))
      {
        return await LockedOut(user);
      }
    }
    return SignInResult.Failed;
  }
}

Listing 1-6Simplified source for SignInManager4

从安全的角度来看,清单 1-6 中有很多需要改进的地方,因此SignInManager类中包含了很多内容。现在,我们只需注意构造函数取一个UserManager实例,在UserManager.FindByName()的数据库中找到(或找不到)用户后,检查密码的责任就交给了UserManager.CheckPasswordAsync().CheckPasswordSignInAsync方法中的UserManager

接下来,让我们看看UserManager看看它做了什么。

public class UserManager<TUser> : IDisposable where TUser : class
{
  public UserManager(IUserStore<TUser> store,
    IOptions<IdentityOptions> optionsAccessor,
    IPasswordHasher<TUser> passwordHasher,
    //More services that don’t concern us now)
  {
    //Null checks and local variable assignments
  }

  protected internal IUserStore<TUser> Store { get; set; }

  public IPasswordHasher<TUser> PasswordHasher { get; set; }

  public IList<IUserValidator<TUser>> UserValidators { get; }↲
    = new List<IUserValidator<TUser>>();

  public IList<IPasswordValidator<TUser>> PasswordValidators { get; } = new List<IPasswordValidator<TUser>>();

  //More properties removed

  private IUserPasswordStore<TUser> GetPasswordStore()
  {
    var cast = Store as IUserPasswordStore<TUser>;
    if (cast == null)
    {
      throw new NotSupportedException ↲
        (Resources.StoreNotIUserPasswordStore);
    }
    return cast;
  }

  public virtual async Task<TUser> ↲
    FindByNameAsync(string userName)
  {
    ThrowIfDisposed();
    if (userName == null)
    {
      throw new ArgumentNullException(nameof(userName));
    }
    userName = NormalizeKey(userName);

    var user = await Store.FindByNameAsync( ↲
      userName, CancellationToken);

    if (user == null && Options.Stores.ProtectPersonalData)
    {
      var keyRing = ↲
        _services.GetService<ILookupProtectorKeyRing>();
      var protector = ↲
        _services.GetService<ILookupProtector>();
      if (keyRing != null && protector != null)
      {
        foreach (var key in keyRing.GetAllKeyIds())
        {
          var oldKey = protector.Protect(key, userName);
          user = await Store.FindByNameAsync(↲
            oldKey, CancellationToken);
          if (user != null)
          {
            return user;
          }
        }
      }
    }
    return user;
  }

  public virtual async Task<bool> CheckPasswordAsync(↲
    TUser user, string password)
  {
    ThrowIfDisposed();
    var passwordStore = GetPasswordStore();
    if (user == null)
    {
      return false;
    }

    var result = await VerifyPasswordAsync(↲
      passwordStore, user, password);
    if (result == ↲
      PasswordVerificationResult.SuccessRehashNeeded)
    {
      await UpdatePasswordHash(passwordStore, user, ↲
        password, validatePassword: false);
      await UpdateUserAsync(user);
    }

    var success = result != PasswordVerificationResult.Failed;
    if (!success)
    {
      Logger.LogWarning(0, "Invalid password for user ↲
        {userId}.", await GetUserIdAsync(user));
    }
    return success;
  }

  protected virtual async Task<PasswordVerificationResult> ↲
    VerifyPasswordAsync(IUserPasswordStore<TUser> store, ↲
    TUser user, string password)
  {
    var hash = await store.GetPasswordHashAsync(user, ↲
      CancellationToken);
    if (hash == null)
    {
      return PasswordVerificationResult.Failed;
    }
    return PasswordHasher.VerifyHashedPassword(↲
      user, hash, password);
  }

  //Additional methods removed for brevity
}

Listing 1-7Simplified source for UserManager5

现在我们终于看到了清单 1-7 中实际完成重要工作的代码,这里我们需要注意两个服务:IUserStore<TUser>IPasswordHasher<TUser>. IUserStore将用户数据写入数据库,IPasswordHasher包含创建和比较散列的方法。目前我不会深入研究这些,因为IUserStore非常简单,我们将在本书的后面深入研究IPasswordHasher。所以,现在,让我们把这些服务视为理所当然,并继续关注UserManager

UserManager中,我们看到FindByUserName()方法调用IUserStore的同名方法获取用户信息,实际工作在VerifyPasswordAsync()中完成。这是IUserStoreGetPasswordHashAsync()从数据库中提取密码的地方,散列比较在IPasswordHasher服务的VerifyHashedPassword()中完成。我们将在本书的后面讨论VerifyHashedPassword()

在进一步讨论IUserStore之前,值得注意的一点是GetPasswordStore()检查当前的IUserStore是否也继承自IUserPasswordStore。如果是,那么GetPasswordStore()返回电流IUserStore。否则,该方法将引发异常。这很重要,原因有二。第一,如果你想实现一个定制的IUserPasswordStore,你需要扩展IUserStore,而不是添加你自己的服务。这不是特别直观,如果你不注意的话,可能会出错。这一点很重要的第二个原因是,大约有十几个不同的用户存储,我们将在本书中讨论其中的一部分,它们以相同的方式运行。如果你想实现UserManager支持的大部分功能,你要么需要重写UserManager类,要么需要忍受一个巨大的IUserStore实现。在本书中,我将采用后一种方法,以尽可能利用默认UserManager类中的功能。

正如我之前提到的,IUserStore的主要目的是完成在数据库中存储信息的实际工作。SQL Server 的默认实现相当难看和复杂,但是您可能以前编写过数据访问代码,所以不值得在这里深入研究。但是现在你知道了向数据库写入用户信息的服务和从数据库写入用户信息的服务大部分是与其余的逻辑分离的,你现在应该想到只要它在. NET 中受支持,创建你自己的IUserStore实现来写入你想要的任何类型的数据库是可能的。为了这样做,你需要创建一个继承自IUserStore的新类,以及使你的代码工作所必需的任何其他接口,比如IUserPasswordStore,然后编写你的数据访问逻辑。我们将在本书的数据访问部分对此进行更深入的探讨。

红隼和 IIS

以前版本的 ASP.NET 要求这些网站使用 Internet 信息服务(IIS)作为 web 服务器运行。ASP.NET Core 中的新功能是 Kestrel,它是一个轻量级的 web 服务器,与您的代码库一起提供。现在,理论上可以在没有任何网络服务器的情况下托管网站。虽然这可能会吸引一些人,但微软仍然建议您在 Kestrel 之前使用更传统的 web 服务器,因为这些服务器提供了额外的安全层。但是,ASP.NET Core 允许您使用 IIS 以外的 web 服务器,包括 Nginx 和 Apache。这种方法的一个缺点是使用 IIS 并不容易——你需要安装一些软件来让你的核心网站在 IIS 中运行,并确保你创建或生成了一个 web.config 文件。如何做到这一点的指导超出了本书的范围,但是微软已经在网上提供了非常好的指导。 7

在本书中对 Kestrel 本身的讨论很少,很大程度上是因为 Kestrel 不像 ASP.NET Core 本身那样面向服务,因此也不像它那样容易改变。本书中的所有例子都使用 Kestrel 和 IIS 进行了测试,但是大多数建议在您选择的任何 web 服务器上都同样适用。

MVC 与 Razor 页面

有许多信息来源更深入地研究了 ASP.NET Core 的两种创建网页的方法之间的差异。由于本书主要关注安全性,并且由于这两种方法在安全性方面没有显著的不同,因此我将只为不熟悉其中一种或两种方法的人提供足够的概述来理解本书中特定于安全性的解释。对于这些的完整解释,您可以在别处找到大量的资源。

手动音量调节

ASP.NET Core 中的 MVC 类似于 ASP.NET 以前版本中的 MVC。为了告诉框架在哪里找到为任何特定页面执行的代码,您配置了 routes ,它将 URL 的一部分映射到代码组件中,通常在 Startup.cs 中是这样的。

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

Listing 1-8Snippet from Startup.cs showing app.UseEndpoints()

清单 1-8 中的重要代码在pattern定义中。控制器是一个类(通常在类名的末尾带有“Controller ”),而动作是类中的一个方法,如 URL 中定义的那样被调用。所以在这种情况下,如果没有指定,默认调用的类是HomeController,默认调用的方法是Index()。让我们看看 Core 2.0 中的默认登录页面是什么样子的(在页面被转换为使用 Razor 页面之前,即使是在 MVC 站点中),它会通过调用 Account/Login 来命中。

[Authorize]
[Route("[controller]/[action]")]
public class AccountController : Controller
{
  private readonly UserManager<ApplicationUser> _userManager;
  private readonly SignInManager<ApplicationUser> ↲
    _signInManager;
  private readonly IEmailSender _emailSender;
  private readonly ILogger _logger;

  public AccountController(
    UserManager<ApplicationUser> userManager,
    SignInManager<ApplicationUser> signInManager,
    IEmailSender emailSender,
    ILogger<AccountController> logger)
  {
    _userManager = userManager;
    _signInManager = signInManager;
    _emailSender = emailSender;
    _logger = logger;
  }

  [HttpGet]
  [AllowAnonymous]
  public async Task<IActionResult> Login(
    string returnUrl = null)
  {
    await HttpContext.SignOutAsync(
      IdentityConstants.ExternalScheme);

    ViewData["ReturnUrl"] = returnUrl;
    return View();
  }

  [HttpPost]
  [AllowAnonymous]
  [ValidateAntiForgeryToken]
  public async Task<IActionResult> Login(LoginViewModel model,
    string returnUrl = null)
  {
    ViewData["ReturnUrl"] = returnUrl;
    if (ModelState.IsValid)
    {
      var result = await _signInManager.PasswordSignInAsync(
        model.Email, model.Password, model.RememberMe,
        lockoutOnFailure: false);
      if (result.Succeeded)
      {
        _logger.LogInformation("User logged in.");
        return RedirectToLocal(returnUrl);
      }
      if (result.RequiresTwoFactor)
      {
        return RedirectToAction(nameof(LoginWith2fa),
          new { returnUrl, model.RememberMe });
      }
      if (result.IsLockedOut)
      {
        _logger.LogWarning("User account locked out.");
        return RedirectToAction(nameof(Lockout));
      }
      else
      {
        ModelState.AddModelError(string.Empty,
          "Invalid login attempt.");
        return View(model);
      }
    }

    // If we got this far, something failed, redisplay form
    return View(model);
  }
}

Listing 1-9MVC source for default AccountController

你应该注意到清单 1-9 中的类被称为AccountController,两个方法都被称为Login,它们都匹配前面提到的路由模式。在构造函数中,您应该看到使用依赖注入框架添加的服务,包括现在熟悉的SignInManagerUserManager。数据通常作为方法参数传递给每个方法,由框架通过查询字符串或提交的表单数据进行解析。

HTML 存储在视图中。框架知道调用视图,因为本例中控制器类中的方法返回一个View,并且视图是由项目中的名称和文件夹路径选择的。这是帐户/登录的视图。

@using System.Collections.Generic
@using System.Linq
@using Microsoft.AspNetCore.Http
@using Microsoft.AspNetCore.Http.Authentication
@model LoginViewModel
@inject SignInManager<ApplicationUser> SignInManager

@{
  ViewData["Title"] = "Log in";
}

<h2>@ViewData["Title"]</h2>
<div class="row">
  <div class="col-md-4">
    <section>
      <form asp-route-returnurl="@ViewData["ReturnUrl"]" method="post">
        <h4>Use a local account to log in.</h4>
        <hr />
        <div asp-validation-summary="All" class="text-danger"></div>
        <div class="form-group">
          <label asp-for="Email"></label>
          <input asp-for="Email" class="form-control" />
          <span asp-validation-for="Email" class="text-danger"></span>
        </div>
        <div class="form-group">
          <label asp-for="Password"></label>
          <input asp-for="Password" class="form-control" />
          <span asp-validation-for="Password" class="text-danger"></span>
        </div>
        <div class="form-group">
          <div class="checkbox">
            <label asp-for="RememberMe">
              <input asp-for="RememberMe" />
              @Html.DisplayNameFor(m => m.RememberMe)
            </label>
          </div>
        </div>
        <div class="form-group">
          <button type="submit" class="btn btn-default">Log in</button>
        </div>
        <div class="form-group">
          <p>
            <a asp-action="ForgotPassword">Forgot your password?</a>
          </p>
          <p>
            <a asp-action="Register" asp-route-returnurl="@ViewData["ReturnUrl"]">Register as a new user?</a>
          </p>
        </div>
      </form>
    </section>
  </div>
  <!-- Code removed for brevity -->
</div>

@section Scripts {
  @await Html.PartialAsync("_ValidationScriptsPartial")
}

Listing 1-10Code for the Account/Login View

清单 1-10 中所发生的事情的详细分析超出了本书的范围。但是,这里有几项需要强调:

  • 您可以将表单绑定到模型类,并且可以在那里定义一些业务规则(比如某个字段是必需的还是应该遵循特定的格式)。在本书的后面你会看到这样的例子。

  • 您可以通过使用 @ 符号并编写您的 C# 来将数据直接写入页面。这在我们后面讨论防止跨站脚本(XSS)时会很重要。

  • 如果您熟悉 Web 表单,您可能会惊讶地发现form元素被显式使用。如果您对这个元素不熟悉,我建议您浏览一本关于 HTML 的书,以熟悉 web 表单对您隐藏的特定于 Web 的细节。

  • 如果你熟悉老版本的 MVC,你会注意到你指定了input元素而不是@Html.TextBoxFor(…

这里没有显示的是模型类,这里是LoginViewModel,它通常是一个简单的类,具有指定前面提到的业务规则的属性。同样,我们稍后会看到这样的例子。

Razor 页

Razor 页似乎是 ASP.NET Core 的旧 ASP.NET 网络表单的等价物。这两种方法都使用绑定到前端的“代码隐藏”文件。不过,相似之处也仅限于此。与网络表单相比,Razor 页

  • 没有页面生命周期

  • 不存储视图状态

  • 专注于编写 HTML 而不是 web 控件

我希望 Razor 页面对新开发人员来说几乎一样容易掌握和理解,但仍能清晰地呈现 HTML,使使用现代 JavaScript 和 CSS 库变得更容易。您可以在这里看到 Razor 页面与 WebForms 相比更接近 HTML。

@page
@model LoginModel

@{
  ViewData["Title"] = "Log in";
}

<h2>@ViewData["Title"]</h2>
<div class="row">
  <div class="col-md-4">
    <section>
      <form method="post">
        <h4>Use a local account to log in.</h4>
        <hr />
        <div asp-validation-summary="All" class="text-danger"></div>
        <div class="form-group">
          <label asp-for="Input.Email"></label>
          <input asp-for="Input.Email" class="form-control" />
          <span asp-validation-for="Input.Email" class="text-danger"></span>
        </div>
        <div class="form-group">
          <label asp-for="Input.Password"></label>
          <input asp-for="Input.Password" class="form-control" />
          <span asp-validation-for="Input.Password" class="text-danger"></span>
        </div>
        <div class="form-group">
          <div class="checkbox">
            <label asp-for="Input.RememberMe">
              <input asp-for="Input.RememberMe" />
              @Html.DisplayNameFor(m => m.Input.RememberMe)
            </label>
          </div>
        </div>
        <div class="form-group">
          <button type="submit" class="btn btn-default">Log in</button>
        </div>
        <div class="form-group">
          <p>
            <a asp-page="./ForgotPassword">Forgot your password?</a>
          </p>
          <p>
            <a asp-page="./Register" asp-route-returnUrl="@Model.ReturnUrl">Register as a new user</a>
          </p>
        </div>
      </form>
    </section>
  </div>
  <!-- Code removed for brevity -->
</div>

@section Scripts {
  @await  Html.PartialAsync("_ValidationScriptsPartial")
}

Listing 1-11Default login page

清单 1-11 中的代码非常接近 HTML,与 MVC 的例子没有太大区别。正如我们在这里看到的,代码有点不同。

public class LoginModel : PageModel
{
  //Remove properties for brevity

  public LoginModel(
    SignInManager<ApplicationUser> signInManager,
    ILogger<LoginModel> logger)
  {
    _signInManager = signInManager;
    _logger = logger;
  }

  public async Task OnGetAsync(string returnUrl = null)
  {
    if (!string.IsNullOrEmpty(ErrorMessage))
    {
      ModelState.AddModelError(string.Empty, ErrorMessage);
    }

    await HttpContext.SignOutAsync(
      IdentityConstants.ExternalScheme);

    ExternalLogins = (await _signInManager.↲
      GetExternalAuthenticationSchemesAsync()).ToList();

    ReturnUrl = returnUrl;
  }

  public async Task<IActionResult> OnPostAsync(
    string returnUrl = null)
  {
    ReturnUrl = returnUrl;

    if (ModelState.IsValid)
    {
      var result = await _signInManager.PasswordSignInAsync(
        Input.Email, Input.Password, Input.RememberMe,
        lockoutOnFailure: true);
      if (result.Succeeded)
      {
        _logger.LogInformation("User logged in.");
        return LocalRedirect(Url.GetLocalUrl(returnUrl));
      }
      if (result.RequiresTwoFactor)
      {
        return RedirectToPage("./LoginWith2fa",
          new { ReturnUrl = returnUrl,
                RememberMe = Input.RememberMe });
      }
      if (result.IsLockedOut)
      {
        _logger.LogWarning("User account locked out.");
        return RedirectToPage("./Lockout");
      }
      else
      {
        ModelState.AddModelError(string.Empty,
          "Invalid login attempt.");
        return Page();
      }
    }

    // If we got this far, something failed, redisplay form
    return Page();
  }
}

Listing 1-12Source for LoginModel (Razor Page example)

清单 1-12 中的构造函数和SignInManager应该看起来很熟悉。否则,对于大多数有经验的开发人员来说,代码的其余部分应该相对容易理解。对服务器的 GET 和 POST 请求有不同的方法,但除此之外,您应该会看到这里的代码和 MVC 版本中的代码有相似之处。

由于这两种方法非常相似,除非您想以某种方式组织代码,否则没有理由选择其中之一。在本书中,MVC 和 Razor 页面之间的任何显著差异都将被注明,否则大多数示例将使用 MVC 提供。

创建 API

与旧版本的框架相比,ASP.NET Core 的一个主要区别是核心中没有 Web API 的等价物。相反,MVC 和 Web API 被合并到一个单一的项目类型中,简化了项目类型管理,使开发 API 变得更加简单。如何最好地创建 API 的完整解释超出了本书的范围,但是有一个结果值得在这一介绍性章节中指出。模型绑定变得更加明确,这意味着如果您通过 AJAX post 而不是 form post 登录,清单 1-13 中显示的代码将不再适用于新的核心世界。

[HttpPost]
[AllowAnonymous]
public async Task<IActionResult> Login(LoginViewModel model,
  string returnUrl = null)
{
  //Login logic here
}

Listing 1-13Sample MVC method without data source attribute

相反,您需要明确地告诉框架在哪里寻找数据。清单 1-14 展示了一个在 AJAX 请求主体中传递数据的例子。

[HttpPost]
[AllowAnonymous]
public async Task<IActionResult> Login(
  [FromBody]LoginViewModel model, string returnUrl = null)
{
  //Login logic here
}

Listing 1-14Sample MVC method with data source attribute

作为一名开发人员,我发现添加这些属性很烦人,尤其是因为调试由缺失或不正确的属性引起的问题会很困难。不过,作为一名安全专家,我喜欢这些属性,因为它们有助于防止由值隐藏导致的漏洞。我们将在本书的后面讨论价值隐藏。现在,让我们回顾一下。网芯:T3】8T5】

  • FromBody :请求体

  • FromForm :请求体,但表单已编码

  • FromHeader :请求头

  • FromQuery :请求查询字符串

  • FromRoute :请求路线数据

  • FromServices :请求服务作为动作参数

您还可以用一个ApiController属性来修饰您的控制器,这样就不需要显式地告诉框架去哪里找了。

我们将在本书的后面从安全角度对此进行深入探讨。

核心与框架与标准

微软最近宣布,2020 年 12 月之后,将不再有一个”。网芯”和 a”。NET 框架”,会有的”。NET 5.0”。此后,目前的 ASP.NET Core 将成为新的“ASP。NET”,而框架将纯粹是遗产。不过,在那之前,我们需要面对这样一个事实:我们大多数人都必须同时支持核心和框架。针对这些情况,微软在标准库中创建了一组通用特性。您可以创建一个标准类库,它可以在框架或核心项目中被引用。

摘要

ASP.NET Core 从表面上看与 ASP.NET 框架相似,但本质上有许多不同之处。为了解决我们在深入研究框架时会发现的一些安全问题,我们需要理解框架是如何工作的。对我们来说幸运的是,微软已经将 ASP.NET 的代码开源,这意味着我们可以挖掘代码并弄清楚它是如何工作的。更好的是,框架的模块化特性将允许我们替换大多数有问题的组件。

在下一章中,我们将深入探讨一些一般的安全概念,这些概念起初可能不直接适用于编程,但在我们深入编写代码之前了解这些概念是很重要的。

Footnotes [1](#Fn1_source) [T2`https://github.com/aspnet/AspNetCore/blob/release/3.1/src/Identity/UI/src/IdentityServiceCollectionUIExtensions.cs`](https://github.com/aspnet/AspNetCore/blob/release/3.1/src/Identity/UI/src/IdentityServiceCollectionUIExtensions.cs)   [2](#Fn2_source) [T2`https://github.com/aspnet/AspNetCore/blob/release/3.1/src/Identity/Extensions.Core/src/IdentityServiceCollectionExtensions.cs`](https://github.com/aspnet/AspNetCore/blob/release/3.1/src/Identity/Extensions.Core/src/IdentityServiceCollectionExtensions.cs)   [3](#Fn3_source) [T2`https://docs.microsoft.com/en-us/aspnet/core/fundamentals/dependency-injection?view=aspnetcore-3.1`](https://docs.microsoft.com/en-us/aspnet/core/fundamentals/dependency-injection%253Fview%253Daspnetcore-3.1)   [4](#Fn4_source) [T2`https://github.com/aspnet/AspNetCore/blob/release/3.1/src/Identity/Core/src/SignInManager.cs`](https://github.com/aspnet/AspNetCore/blob/release/3.1/src/Identity/Core/src/SignInManager.cs)   [5](#Fn5_source) [T2`https://github.com/aspnet/AspNetCore/blob/release/3.1/src/Identity/Extensions.Core/src/UserManager.cs`](https://github.com/aspnet/AspNetCore/blob/release/3.1/src/Identity/Extensions.Core/src/UserManager.cs)   [6](#Fn6_source) [T2`https://docs.microsoft.com/en-us/aspnet/core/fundamentals/servers/?view=aspnetcore-2.2&tabs=windows`](https://docs.microsoft.com/en%252Dus/aspnet/core/fundamentals/servers/%253Fview%253Daspnetcore%252D2.2%2524tabs%253Dwindows)   [7](#Fn7_source) [T2`https://docs.microsoft.com/en-us/aspnet/core/host-and-deploy/iis/?view=aspnetcore-2.2`](https://docs.microsoft.com/en-us/aspnet/core/host-and-deploy/iis/%253Fview%253Daspnetcore-2.2)   [8](#Fn8_source) [T2`https://docs.microsoft.com/en-us/aspnet/core/web-api/?view=aspnetcore-2.2`](https://docs.microsoft.com/en-us/aspnet/core/web-api/%253Fview%253Daspnetcore-2.2)