在安全领域,认证和授权是两个重要的主题。认证是安全体系的第一道屏障,是守护整个应用或者服务的第一道大门。当访问者请求进入的时候,认证体系通过验证对方的提供凭证确定其真实身份。认证体系只有在证实了访问者的真实身份的情况下才会允许其进入。ASP.NET Core提供了多种认证方式,它们的实现都基于相同的认证模型。本篇文章提供了一个极简的实例让读者体验如何在ASP.NET Core应用中实现认证、登录和注销。

本篇文章节选自《ASP.NET Core 3框架揭秘》(下册),针对本书的限时5折优惠截至到今天24时,有兴趣的朋友可以通过加入读者群进行购买。入群方式:扫描右方二维码添加“博文小丸子(broadview002)”,并将本书书号“38462”作为验证信息。源代码从这里下载。

一、认证票据

认证是一个旨在确定请求访问者真实身份的过程,与认证相关的还有其他两个基本操作——登录与注销。要真正理解认证、登录与注销这3个核心操作的本质,就需要对ASP.NET Core采用的基于“票据”的认证机制有基本的了解。ASP.NET Core应用的认证实现在一个名为AuthenticationMiddleware的中间件中,该中间件在处理分发给它的请求时会按照指定的认证方案(Authentication Scheme)从请求中提取能够验证用户真实身份的数据,我们一般将该数据称为安全令牌(Security Token)。ASP.NET Core应用下的安全令牌被称为认证票据(Authentication Ticket),所以ASP.NET Core应用采用基于票据的认证方式。

AuthenticationMiddleware中间件实现的整个认证流程涉及下图所示的3种针对认证票据的操作,即认证票据的颁发、检验和撤销。我们将这3个操作所涉及的3种角色称为票据颁发者(Ticket Issuer)、验证者(Authenticator)和撤销者(Ticket Revoker),在大部分场景下这3种角色由同一个主体来扮演。

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

一旦拥有了由认证方颁发的认证票据,我们就可以按照双方协商的方式(如通过Cookie或者报头)在请求中携带该认证票据,并以此票据声明的身份执行目标操作或者访问目标资源。认证票据一般都具有时效性,一旦过期将变得无效。我们有的时候甚至希望在过期之前就让认证票据无效,以免别人使用它冒用自己的身份与应用进行交互,这就是注销(Sign Out)操作。

ASP.NET Core应用的认证系统旨在构建一个标准的模型来完成针对请求的认证以及与之相关的登录和注销操作。接下来我们就通过一个简单的实例来演示如何在一个ASP.NET Core应用中实现认证、登录和注销的功能。

二、基于Cookie的认证

我们会采用ASP.NET Core提供的基于Cookie的认证方案。顾名思义,该认证方案采用Cookie来携带认证票据。为了使读者对基于认证的编程模式有深刻的理解,我们演示的这个应用将从一个空白的ASP.NET Core应用开始搭建。

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

public class Program
{
public static void Main()
{
Host.CreateDefaultBuilder()
.ConfigureWebHostDefaults(builder => builder
.ConfigureServices(svcs => svcs.AddRouting())
.Configure(app => app
.UseRouting()
.UseEndpoints(endpoints =>{
endpoints.Map(pattern: "/", RenderHomePageAsync);
endpoints.Map("Account/Login", SignInAsync);
endpoints.Map("Account/Logout", SignOutAsync);
})))
.Build()
.Run();
} public static async Task RenderHomePageAsync(HttpContext context)
{
throw new NotImplementedException();
} public static async Task SignInAsync(HttpContext context)
{
throw new NotImplementedException();
} public static async Task SignOutAsync(HttpContext context)
{
throw new NotImplementedException();
}
}

三、应用主页

如下面的代码片段所示,我们调用IApplicationBuilder接口的UseAuthentication扩展方法就是为了注册用来实现认证的AuthenticationMiddleware中间件。该中间件的依赖服务是通过调用IServiceCollection接口的AddAuthentication扩展方法注册的。在注册这些基础服务时,我们还设置了默认采用的认证方案,静态类型CookieAuthenticationDefaults的AuthenticationScheme属性返回的就是Cookie认证方案的默认方案名称。

public class Program
{
public static void Main()
{
Host.CreateDefaultBuilder()
.ConfigureWebHostDefaults(builder => builder
.ConfigureServices(svcs => svcs
.AddRouting()
.AddAuthentication(options => options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme)
.AddCookie())
.Configure(app => app
.UseAuthentication()
.UseRouting()
.UseEndpoints(endpoints =>{
endpoints.Map(pattern: "/", RenderHomePageAsync);
endpoints.Map("Account/Login", SignInAsync);
endpoints.Map("Account/Logout", SignOutAsync);
})))
.Build()
.Run();
}
}

ASP.NET Core提供了一个极具扩展性的认证模型,我们可以利用它支持多种认证方案,针对认证方案的注册是通过AddAuthentication方法返回的一个AuthenticationBuilder对象来实现的。在上面提供的代码片段中,我们调用AuthenticationBuilder对象的AddCookie扩展方法完成了针对Cookie认证方案的注册。

演示实例的主页是通过如下所示的RenderHomePageAsync方法来呈现的。由于我们要求浏览主页必须是经过认证的用户,所以该方法会利用HttpContext上下文的User属性返回的ClaimsPrincipal对象判断当前请求是否经过认证。对于经过认证的请求,我们会响应一个简单的HTML文档,并在其中显示用户名和一个注销链接。

public class Program
{
...
public static async Task RenderHomePageAsync(HttpContext context)
{
if (context?.User?.Identity?.IsAuthenticated == true)
{
await context.Response.WriteAsync(
@"<html>
<head><title>Index</title></head>
<body>" +
$"<h3>Welcome {context.User.Identity.Name}</h3>" +
@"<a href='Account/Logout'>Sign Out</a>
</body>
</html>");
}
else
{
await context.ChallengeAsync();
}
}
}

对于匿名请求,我们希望应用能够自动重定向到登录路径。从如上所示的代码片段可以看出,我们仅仅调用当前HttpContext上下文的ChallengeAsync扩展方法就完成了针对登录路径的重定向。前面提及,注册的登录和注销路径是基于Cookie的认证方案采用的默认路径,所以调用ChallengeAsync方法时根本不需要指定重定向路径。下图所示就是作为应用的主页在浏览器上呈现的效果。


四、登录

登录与注销分别实现在SignInAsync方法和SignOutAsync方法中,我们采用的是针对“用户名 + 密码”的登录方式,所以可以利用静态字段_accounts来存储应用注册的账号。在静态构造函数中,我们添加密码均为“password”的3个账号(Foo、Bar和Baz)。

public class Program
{
private static Dictionary<string, string> _accounts;
static Program()
{
_accounts = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
_accounts.Add("Foo", "password");
_accounts.Add("Bar", "password");
_accounts.Add("Baz", "password");
}
}

如下所示的代码片段是用于处理登录请求的SignInAsync方法的定义,而RenderLoginPageAsync方法用来呈现登录页面。如下面的代码片段所示,对于GET请求,SignInAsync方法会直接调用RenderLoginPageAsync方法来呈现登录界面。对于POST请求,我们会从提交的表单中提取用户名和密码,并对其实施验证。如果提供的用户名与密码一致,我们会根据用户名创建一个代表身份的GenericIdentity对象,并利用它创建一个代表登录用户的ClaimsPrincipal对象,RenderHomePageAsync方法正是利用该对象来检验当前用户是否经过认证的。有了ClaimsPrincipal对象,我们只需要将它作为参数调用HttpContext上下文的SignInAsync扩展方法即可完成登录,该方法最终会自动重定向到初始方法的路径,也就是我们的主页。

public class Program
{
public static async Task SignInAsync(HttpContext context)
{
if (string.Compare(context.Request.Method, "GET") == 0)
{
await RenderLoginPageAsync(context, null, null, null);
}
else
{
var userName = context.Request.Form["username"];
var password = context.Request.Form["password"];
if (_accounts.TryGetValue(userName, out var pwd) && pwd == password)
{
var identity = new GenericIdentity(userName, "Passord");
var principal = new ClaimsPrincipal(identity);
await context.SignInAsync(principal);
}
else
{
await RenderLoginPageAsync(context, userName, password, "Invalid user name or password!");
}
}
} private static Task RenderLoginPageAsync(HttpContext context, string userName, string password, string errorMessage)
{
context.Response.ContentType = "text/html";
return context.Response.WriteAsync(
@"<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>");
}
}

如果用户提供的用户名与密码不匹配,我们还是会调用RenderLoginPageAsync方法来呈现登录页面,该页面会以下图所示的形式保留用户的输入并显示错误消息。图19-3还反映了一个细节,调用HttpContext上下文的ChallengeAsync方法会将当前路径(主页路径“/”,经过编码后为“%2F”)存储在一个名为ReturnUrl的查询字符串中,SignInAsync方法正是利用它实现对初始路径的重定向的。

五、注销

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

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

采用最简单的方式在ASP.NET Core应用中实现认证、登录和注销的更多相关文章

  1. 如何在ASP.NET Core应用中实现与第三方IoC/DI框架的整合?

    我们知道整个ASP.NET Core建立在以ServiceCollection/ServiceProvider为核心的DI框架上,它甚至提供了扩展点使我们可以与第三方DI框架进行整合.对此比较了解的读 ...

  2. [转帖]以Windows服务方式运行ASP.NET Core程序

    以Windows服务方式运行ASP.NET Core程序 原作者blog: https://www.cnblogs.com/guogangj/p/9198031.htmlaspnet的blog 需要持 ...

  3. 在Linux上以服务的方式运行ASP.NET Core站点

    更新:用supervisor是更好的解决方法,详见 Linux下为 dotnet 创建守护进程 要在生成环境下在Linux服务器上跑ASP.NET Core站点,首先要解决的问题是以服务的方式运行AS ...

  4. ASP.NET Core MVC 中设置全局异常处理方式

    在asp.net core mvc中,如果有未处理的异常发生后,会返回http500错误,对于最终用户来说,显然不是特别友好.那如何对于这些未处理的异常显示统一的错误提示页面呢? 在asp.net c ...

  5. ASP.NET Core中的缓存[1]:如何在一个ASP.NET Core应用中使用缓存

    .NET Core针对缓存提供了很好的支持 ,我们不仅可以选择将数据缓存在应用进程自身的内存中,还可以采用分布式的形式将缓存数据存储在一个“中心数据库”中.对于分布式缓存,.NET Core提供了针对 ...

  6. 008.Adding a model to an ASP.NET Core MVC app --【在 asp.net core mvc 中添加一个model (模型)】

    Adding a model to an ASP.NET Core MVC app在 asp.net core mvc 中添加一个model (模型)2017-3-30 8 分钟阅读时长 本文内容1. ...

  7. ASP.NET Core 基于JWT的认证(一)

    ASP.NET Core 基于JWT的认证(一) Json web token (JWT), 是为了在网络应用环境间传递声明而执行的一种基于JSON的开放标准((RFC 7519).该token被设计 ...

  8. 在 ASP.NET Core 项目中使用 AutoMapper 进行实体映射

    一.前言 在实际项目开发过程中,我们使用到的各种 ORM 组件都可以很便捷的将我们获取到的数据绑定到对应的 List<T> 集合中,因为我们最终想要在页面上展示的数据与数据库实体类之间可能 ...

  9. 在 ASP.NET Core 项目中使用 MediatR 实现中介者模式

    一.前言  最近有在看 DDD 的相关资料以及微软的 eShopOnContainers 这个项目中基于 DDD 的架构设计,在 Ordering 这个示例服务中,可以看到各层之间的代码调用与我们之前 ...

随机推荐

  1. 吴裕雄--天生自然 R语言开发学习:基本图形(续三)

    #---------------------------------------------------------------# # R in Action (2nd ed): Chapter 6 ...

  2. mybatis 添加后获得该新增数据自动生成的 id

    // useGeneratedKeys默认值为false,keyProperty的值对应的是User类中的主键字段名 // mybatis 写法如下 <insert id="inser ...

  3. Ubuntu上搭建GPU服务器

    1.安装显卡驱动 2.安装CUDA 3.安装cuDNN 下载: 根据显卡类型以及操作系统,选定CUDA版本和语言设置,下载对应的显卡驱动. 驱动下载地址 安装 $ sudo ./NVIDIA-Linu ...

  4. 第一次安装vs2010无法运行程序,系统找不到exe文件,LINK : fatal error LNK1123: 转换到 COFF 期间失败: 文件无效或损坏

    最近在看数据结构的一些书籍,怎奈代码是c写的,所以安装一个编译器vs2010来测试代码,但是建完文件后编译ok,f5却出现错误:无法启动程序,系统找不到指定文件.上网找了一些解决办法,但是仍然无法解决 ...

  5. Leetcode 412.FizzBuzz

    题目描述 写一个程序,输出从 1 到 n 数字的字符串表示. 1. 如果 n 是3的倍数,输出"Fizz": 2. 如果 n 是5的倍数,输出"Buzz": 3 ...

  6. Spring Boot框架开发的Java项目在CentOS7上的部署

    需求:上级拿来一份Spring Boot框架开发的Java项目代码让我在服务器上运行起来,只说了一句该框架是自带了Tomcat(不用重新安装Tomcat),一份代码下有两个项目(一个管理端项目,一个用 ...

  7. 码海拾遗:简述C++(一)

    C++是Bjarne Stroustrup博士于1982年,在C语言的基础上引入并扩充了面向对象的概念后发明的一种新的程序语言.就与C语言的渊源而言,C++可以说是C语言的超集,它兼容C的一切(可能是 ...

  8. Autotestplat体验中心

    web端 移动端 可戳[阅读原文]进行体验

  9. e代驾狂野裁员 O2O逐渐恢复理智?

    O2O逐渐恢复理智?" title="e代驾狂野裁员 O2O逐渐恢复理智?">     近段时间以来,O2O行业堪称"哀鸿遍野",十分凄惨.巨头 ...

  10. Android(四)-JVM与DVM区别

    JVM与DVM区别 1.由来 Android是基于java的既然java已经有了java虚拟机,为什么android还要弄一个DVM了?最重要的就是版权问题,一开始就是用的 JVM,没过多久就被SUN ...