认证是一个确定请求访问者真实身份的过程,与认证相关的还有其他两个基本操作——登录和注销。ASP.NET Core利用AuthenticationMiddleware中间件完成针对请求的认证,并提供了用于登录、注销以及“质询”的API,本篇文章利用它们使用最简单的代码实现这些功能。(本文提供的示例演示已经同步到《ASP.NET Core 6框架揭秘-实例演示版》)

一、 认证票据

二、基于Cookie的认证

三、 强制认证

四、登录与注销

一、认证票据

要真正理解认证、登录和注销这三个核心操作的本质,就需要对ASP.NET采用的基于“票据”的认证机制有基本的了解。ASP.NET Core应用的认证实现在AuthenticationMiddleware的中间件中,该中间件在处理分发给它的请求时会按照指定的认证方案(Authentication Scheme)从请求中提取能够验证用户真实身份的信息,我们一般将此信息称为安全令牌(Security Token)。ASP.NET Core应用下的安全令牌被称为认证票据(Authentication Ticket),它采用基于票据的认证方式。该中间件实现的整个认证流程涉及图1所示的三种针对认证票据的操作,即认证票据的“颁发”、“检验”和“撤销”。我们将这三个操作所涉及的三种角色称为票据颁发者(Ticket Issuer)、验证者(Authenticator)和撤销者(Ticket Revoker),在大部分场景下这三种角色由同一个主体来扮演。

图1 基于票据的认证

颁发认证票据的过程就是登录(Sign In)操作。用户试图通过登录来获取认证票据时需要提供可用来证明自身身份的凭证(Credential),最常见的用户凭证类型是“用户名 + 密码”。认证方在确定对方真实身份之后,会颁发一个认证票据,该票据携带着与该用户有关的身份、权限及其他相关的信息。

一旦拥有了由认证方颁发的认证票据,客户端就可以按照双方协商的方式(比如通过Cookie或者报头)在请求中携带该认证票据,并以此票据声明的身份执行目标操作或者访问目标资源。认证票据一般都具有时效性,一旦过期将变得无效。如果希望在过期之前就让认证票据无效,这就是注销(Sign Out)操作。

ASP.NET的认证系统旨在构建一个标准的模型,用来完成针对请求的认证以及与之相关的登录和注销操作。按照惯例,在介绍认证模型的架构设计之前,需要通过一个简单的实例来演示如何在一个ASP.NET应用中实现认证、登录和注销的功能。

二、基于Cookie的认证

我们会采用ASP.NET提供的基于Cookie的认证方案。该认证方案采用Cookie来携带认证票据。为了使读者对基于认证的编程模式有深刻的理解,我们演示的这个应用将从一个空白的ASP.NET应用开始搭建。这个应该会呈现两个页面,认证用户访问主页会呈现一个“欢迎”页面,匿名请求则会重定向到登录页面,我们将这两个页面的呈现实现在如下这个IPageRenderer服务中,PageRenderer类型为该接口的默认实现。

public interface IPageRenderer
{
IResult RenderLoginPage(string? userName = null, string? password = null, string? errorMessage = null);
IResult RenderHomePage(string userName);
} public class PageRenderer : IPageRenderer
{
public IResult RenderHomePage(string userName)
{
var html = @$"
<html>
<head><title>Index</title></head>
<body>
<h3>Welcome {userName}</h3>
<a href='Account/Logout'>Sign Out</a>
</body>
</html>";
return Results.Content(html, "text/html");
} public IResult RenderLoginPage(string? userName, string? password, string? errorMessage)
{
var html = @$"
<html>
<head><title>Login</title></head>
<body>
<form method='post'>
<input type='text' name='username' placeholder='User name' value = '{userName}' />
<input type='password' name='password' placeholder='Password' value = '{password}' />
<input type='submit' value='Sign In' />
</form>
<p style='color:red'>{errorMessage}</p>
</body>
</html>";
return Results.Content(html, "text/html");
}
}

我们采用“用户名+密码”的认证方式,密钥验证实现的如下这个IAccountService接口的Validate方法中。在实现的AccountService类型中,我们预创建了三个密码为“password”的账号(“foo”、“bar”和“baz”)。

public interface IAccountService
{
bool Validate(string userName, string password);
} public class AccountService: IAccountService
{
private readonly Dictionary<string, string> _accounts = new(StringComparer.OrdinalIgnoreCase)
{
{ "Foo", "password"},
{ "Bar", "password"},
{ "Baz", "password"}
}; public bool Validate(string userName, string password) =>_accounts.TryGetValue(userName, out var pwd) && pwd == password;
}

我们即将创建的这个ASP.NET应用主要处理四种类型的请求。主页需要在登录之后才能访问,所以针对主页的匿名请求会被重定向到登录页面。在登录页面输入正确的用户名和密码之后,应用会自动重定向到主页,该页面会显示当前认证用户名并提供注销的链接。我们按照如下所示的方式注册了四个对应的终结点,其中登录和注销采用的是约定的路径“Account/Login”与“Account/Logout”。

using App;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.Cookies;
using System.Security.Claims;
using System.Security.Principal; var builder = WebApplication.CreateBuilder();
builder.Services
.AddSingleton<IPageRenderer, PageRenderer>()
.AddSingleton<IAccountService, AccountService>()
.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme).AddCookie();
var app = builder.Build();
app.UseAuthentication(); app.Map("/", WelcomeAsync);
app.MapGet("Account/Login", Login);
app.MapPost("Account/Login", SignInAsync);
app.Map("Account/Logout", SignOutAsync);
app.Run(); Task WelcomeAsync () => throw new NotImplementedException();
IResult Login(IPageRenderer renderer) => throw new NotImplementedException();
Task SignInAsync()=> throw new NotImplementedException();
Task SignOutAsync() => throw new NotImplementedException();

上面的演示程序调用UseAuthentication扩展方法注册了AuthenticationMiddleware中间件,它所依赖服务是通过调用AddAuthentication扩展方法进行注册。在调用该方法时,我们还设置了默认采用的认证方案,静态类型CookieAuthenticationDefaults的AuthenticationScheme属性返回的就是Cookie认证方案的默认方案名称。我们在上面定义的两个服务也在这里进行了注册。图2所示就是作为应用的主页在浏览器上呈现的效果。

图2 应用主页

三、 强制认证

演示实例的主页是通过如下所示的WelcomeAsync方法来呈现的,该方法注入了当前HttpContext上下文、代表当前用户的ClaimsPrincipal对象和IPageRenderer对象。我们利用ClaimsPrincipal对象确定用户是否经过人证,认证用户请求将呈现正常的欢迎页面,匿名请求直接调用HttpContext上下文的ChallengeAsync方法进行处理。基于Cookie的认证方案会自动将匿名请求重定向到登录页面,由于我们指定的登录和注销路径是Cookie的认证方案约定的路径,所以调用ChallengeAsync方法时根本不需要指定重定向路径。

Task WelcomeAsync(HttpContext context, ClaimsPrincipal user, IPageRenderer renderer)
{
if (user?.Identity?.IsAuthenticated ?? false)
{
return renderer.RenderHomePage(user.Identity.Name!).ExecuteAsync(context);
} return context.ChallengeAsync();
}

四、登录与注销

针对登录页面所在地址的请求由两种类型,针对GET请求的Login方法会登录页面呈现出来,针对POST请求的SignInAsync方法检验输入的用户名和密码,并在验证成功后实施“登录”。如下面的代码片段所示,SignInAsync方法中注入了当前HttpContext上下文、代表请求的HttpRequest对象和额外两个服务。从请求表单将用户和密码提取出来后,我们利用IAccountService对象进行验证。在验证通过的情况下,我们会根据用户名创建代表当前用户的ClaimsPrincipal对象,并将它作为参数调用HttpContext上下文的SignInAsync扩展方法实施登录, 该方法最终会自动重定向到初始方法的路径,也就是我们的主页。

IResult Login(IPageRenderer renderer) => renderer.RenderLoginPage();

Task SignInAsync(HttpContext context, HttpRequest request, IPageRenderer renderer,IAccountService accountService)
{
var username = request.Form["username"];
if (string.IsNullOrEmpty(username))
{
return renderer.RenderLoginPage(null, null, "Please enter user name.").ExecuteAsync(context);
} var password = request.Form["password"];
if (string.IsNullOrEmpty(password))
{
return renderer.RenderLoginPage(username, null, "Please enter user password.").ExecuteAsync(context);
} if (!accountService.Validate(username, password))
{
return renderer.RenderLoginPage(username, null, "Invalid user name or password.").ExecuteAsync(context);
} var identity = new GenericIdentity(name: username, type: "PASSWORD");
var user = new ClaimsPrincipal(identity);
return context.SignInAsync(user);
}

如果用户名或者密码没有提供或者不匹配,登录页面会以图3所示的形式再次呈现出来,并保留输入的用户名和错误消息。ChallengeAsync方法会将当前路径(主页路径“/”,经过编码后为“%2F”)存储在一个名为ReturnUrl的查询字符串中,SignInAsync方法正是利用它实现对初始路径的重定向的。

图3 登录页面

既然登录可以通过调用当前HttpContext上下文的SignInAsync扩展方法来完成,那么注销操作对应的自然就是SignOutAsync扩展方法。如下面的代码片段所示,SignOutAsync扩展方法正是调用这个方法来注销当前登录状态的。我们在完成注销之后将应用重定向到主页。

async Task SignOutAsync(HttpContext context)
{
await context.SignOutAsync();
context.Response.Redirect("/");
}

ASP.NET Core 6框架揭秘实例演示[39]:使用最简洁的代码实现登录、认证和注销的更多相关文章

  1. ASP.NET Core 6框架揭秘实例演示[07]:文件系统

    ASP.NET Core应用具有很多读取文件的场景,如读取配置文件.静态Web资源文件(如CSS.JavaScript和图片文件等).MVC应用的视图文件,以及直接编译到程序集中的内嵌资源文件.这些文 ...

  2. ASP.NET Core 6框架揭秘实例演示[08]:配置的基本编程模式

    .NET的配置支持多样化的数据源,我们可以采用内存的变量.环境变量.命令行参数.以及各种格式的配置文件作为配置的数据来源.在对配置系统进行系统介绍之前,我们通过几个简单的实例演示一下如何将具有不同来源 ...

  3. ASP.NET Core 6框架揭秘实例演示[09]:配置绑定

    我们倾向于将IConfiguration对象转换成一个具体的对象,以面向对象的方式来使用配置,我们将这个转换过程称为配置绑定.除了将配置树叶子节点配置节的绑定为某种标量对象外,我们还可以直接将一个配置 ...

  4. ASP.NET Core 6框架揭秘实例演示[10]:Options基本编程模式

    依赖注入使我们可以将依赖的功能定义成服务,最终以一种松耦合的形式注入消费该功能的组件或者服务中.除了可以采用依赖注入的形式消费承载某种功能的服务,还可以采用相同的方式消费承载配置数据的Options对 ...

  5. ASP.NET Core 6框架揭秘实例演示[11]:诊断跟踪的几种基本编程方式

    在整个软件开发维护生命周期内,最难的不是如何将软件系统开发出来,而是在系统上线之后及时解决遇到的问题.一个好的程序员能够在系统出现问题之后马上定位错误的根源并找到正确的解决方案,一个更好的程序员能够根 ...

  6. ASP.NET Core 6框架揭秘实例演示[12]:诊断跟踪的进阶用法

    一个好的程序员能够在系统出现问题之后马上定位错误的根源并找到正确的解决方案,一个更好的程序员能够根据当前的运行状态预知未来可能发生的问题,并将问题扼杀在摇篮中.诊断跟踪能够帮助我们有效地纠错和排错&l ...

  7. ASP.NET Core 6框架揭秘实例演示[13]:日志的基本编程模式[上篇]

    <诊断跟踪的几种基本编程方式>介绍了四种常用的诊断日志框架.其实除了微软提供的这些日志框架,还有很多第三方日志框架可供我们选择,比如Log4Net.NLog和Serilog 等.虽然这些框 ...

  8. ASP.NET Core 6框架揭秘实例演示[14]:日志的进阶用法

    为了对各种日志框架进行整合,微软创建了一个用来提供统一的日志编程模式的日志框架.<日志的基本编程模式>以实例演示的方式介绍了日志的基本编程模式,现在我们来补充几种"进阶" ...

  9. ASP.NET Core 6框架揭秘实例演示[15]:针对控制台的日志输出

    针对控制台的ILogger实现类型为ConsoleLogger,对应的ILoggerProvider实现类型为ConsoleLoggerProvider,这两个类型都定义在 NuGet包"M ...

  10. ASP.NET Core 6框架揭秘实例演示[16]:内存缓存与分布式缓存的使用

    .NET提供了两个独立的缓存框架,一个是针对本地内存的缓存,另一个是针对分布式存储的缓存.前者可以在不经过序列化的情况下直接将对象存储在应用程序进程的内存中,后者则需要将对象序列化成字节数组并存储到一 ...

随机推荐

  1. Java高频面试题(2023最新整理)

    Java的特点 Java是一门面向对象的编程语言.面向对象和面向过程的区别参考下一个问题. Java具有平台独立性和移植性. Java有一句口号:Write once, run anywhere,一次 ...

  2. 【Jenkins系列】-Pipeline语法全集

    Jenkins为您提供了两种开发管道代码的方式:脚本式和声明式. 脚本式流水线(也称为"传统"流水线)基于Groovy作为其特定于域的语言. 而声明式流水线提供了简化且更友好的语法 ...

  3. 全网最详细中英文ChatGPT-GPT-4示例文档-快速创意生成从0到1快速入门——官网推荐的48种最佳应用场景(附python/node.js/curl命令源代码,小白也能学)

    目录 Introduce 简介 setting 设置 Prompt 提示 Sample response 回复样本 API request 接口请求 python接口请求示例 node.js接口请求示 ...

  4. Distinctive Image Features from Scale-Invariant Keypoints 论文解读

    Distinctive Image Features from Scale-Invariant Keypoints 论文解读 著名的SIFT local feature提取方法 Scale-space ...

  5. Kubernetes 部署 MySQL 高可用读写分离

    Kubernetes 部署 MySQL 高可用读写分离 简介: 在有状态应用中,MySQL是我们最常见也是最常用的.本文我们将实战部署一个一组多从的MySQL集群. 一.配置准备 configMap ...

  6. it必给装机小软件附源码

    需要的包 启动之后是这个样子的 远吗如下: #authon fengimport zipfile as zfimport osimport win32apiimport win32conimport ...

  7. 微信小程序隐藏页面滚动条

    开发小程序时,经常会碰到页面长度超过屏幕高度,然后下拉时会出现滚动条,对于一些有强迫症的人来说是不可忍受的. 网上看了好多,写的.都评论有起作用或者不起作用的. 我在这分享一个全局隐藏滚动条的方式. ...

  8. 浅谈如何使用 github.com/kardianos/service

    在实际开发过程中,有时候会遇到如何编写Go开机自启服务的需求,在linux中我们可以使用systemd来进行托管,windows下可以通过注册表来实现,mac下可以通过launchd来实现,上面的方式 ...

  9. 【解决方法】windos server 2019 在批量创建DNS的正向与反向记录时,提示报错: >Command failed: ERROR_ACCESS_DENIED 5 0x5

    目录-快速跳转 问题描述 原因分析: 解决方案: 附言: 问题描述 操作环境与场景: 在 VM 内 windos server 2019 在批量创建DNS的正向与反向记录时,提示报错: Command ...

  10. 高精度地形DEM数据下载(NASA数据 12.5米分辨率)

    本文介绍从NASA阿拉斯加卫星设备处网站下载高精度DEM数据,下载的数据精度是12.5米分辨率. 目前国内大部分可以下载的dem数据都是30米或90米分辨率的,对于更高精度的数据要不就是需要付费下载, ...