在安全领域,认证和授权是两个重要的主题。认证是安全体系的第一道屏障,是守护整个应用或者服务的第一道大门。当访问者请求进入的时候,认证体系通过验证对方的提供凭证确定其真实身份。认证体系只有在证实了访问者的真实身份的情况下才会允许其进入。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. (四)mybatis缓存、事务、插件的基本知识

    mybatis缓存.事务.插件的基础 一.缓存 (一)一级缓存与二级缓存 一级缓存 为了获得更好的性能,最重要的就是一级缓存.每个session对象维持一个一级缓存,session对象创建时缓存创建, ...

  2. Qt类声明中Q_OBJECT的作用与报错解决

    2017-06-22 周四 大雨 北京 院里 新建作图类,继承自QCUstomPlot类 因为需要同时作8张图,都要单坐标缩放的功能,因此想干脆新建一个类,继承自QCUstomPlot,把需要的功能都 ...

  3. 通过git shell 在Github上传本地项目

    首先现在github上新建一个库,再进行如下操作,过程不赘述 1.打开git shell 2.cd到项目位置       // cd archives-vue 3.git init 4.Get add ...

  4. rbenv、fish 與 VSCode 設置之路

    在最新的 VSCode 1.3.1 版裡,Integrated Terminal 變得更加好用,但由於上游套件 xterm.js 的緣故,zsh 還是有無法捲動的問題.不過作為一個 Rails 開發者 ...

  5. 神州优车挂牌新三板!专车B2C对决C2C将愈发狂暴?

    近日,全国中小企业股份转让系统公告显示,神州优车已获准在新三板挂牌.神州优车作为神州专车的主营主体,此次挂牌新三板意味着神舟专车成功突围,成为"专车第一股".相比滴滴.Uber中国 ...

  6. 从0到1,本地到远程git程序过程

    从0到1,本地到远程git程序过程 切记一定要在需要提交代码的文件夹下git init,既是你使用了什么 tortoisegit什么工具,或者你在idea环境下已经add了,但是仍然需要你在当前文件夹 ...

  7. Java程序员常用的@Component、@Repository、@Controller、@Service系列【案例demo3】

    Java程序员常用的@Component.@Repository.@Controller.@Service系列[案例demo3]   很多程序员通过在类上使用@Repository.@Componen ...

  8. 《Deep Learning of Graph Matching》论文阅读

    1. 论文概述 论文首次将深度学习同图匹配(Graph matching)结合,设计了end-to-end网络去学习图匹配过程. 1.1 网络学习的目标(输出) 是两个图(Graph)之间的相似度矩阵 ...

  9. harbor自动清理镜像

    harbor定时清理镜像 ​ 分享下最近写harbor仓库镜像自动清理脚本思路,很长时间不写shell脚本,这次的脚本也是匆匆写的,还有很多可优化点,感兴趣的可以参考自己优化下,写的不完善地方也希望指 ...

  10. 学h5前端开发前必知的三大流行趋势

    学h5前端开发前必知的三大流行趋势 随着互联网时代的飞速发展,各种互联网的Web应用程序层出不穷,很多人对于HTML5前端开发的过程充满了好奇,但是却没有了解到前端开发的未来发展趋势.下面,云慧学院专 ...