接着上文. Net Core 认证系统源码解析, Cookie 认证算是常用的认证模式, 但是目前主流都是前后端分离, 有点鸡肋但是, 不考虑移动端的站点或者纯管理后台网站可以使用这种认证方式. 注意: 基于浏览器且不是前后端分离的架构(页面端具有服务端处理能力). 移动端就不要考虑了, 太麻烦. 支持前后端分离前给移动端提供认证 API 的一般采用 JwtBearer 认证, 可以和 IdentityServer4 的 password 模式结合. 很适用, 但是 id4 的 password 模式各客户端必须绝对信任, 因为要暴露用户名密码. 适合做企业级下所有产品的认证. 不支持除企业外的第三方调用. 当然 id4 提供了其他模式. 这是题外话. 但是场景得介绍清楚. 以免误导大家!
1,Cookie 认证流程
引入核心认证组件之后, 通过扩展的方式引入 Cookie 认证, 微软采用链式编程, 很优雅. Net Core 的一大特点.
注入 Cookie 认证方案, 指定 Cookie 认证参数, 并指定 Cookie 认证处理器, 先不介绍参数, 看看处理器干了什么.
Cookie 的核心认证方法, 第一步如下:
一些必须的防重复执行操作, 没截图, 也不介绍了, 安全工作, 只贴核心代码. 第一步, 就是去读取客户端存在的 cookie 信息.
微软在 Cookie 认证参数中提供了接口, 意味者你可以自定义读取 Cookie 内容的实现, 他会把上下文和 Cookie 的名称传给你, 这样就能定制获取 Cookie 内容的实现. 接着解密 Cookie 内容
微软注入了 Core 的核心加密组件, 大家自行百度, 却采用微软默认的实现. 所以客户端的 cookie 内容一般都以加密内容显示.
接着
拿到 seesionId 的 cliam, 关于 claim 不多说, 自行百度. core 新的身份模型. 必须了解的内容.
cookie 认证参数中你可以配置 SessionStore, 意味者你的 session 可以进行持久化管理, 数据库还是 Redis 还是分布式环境自行选择. 应用场景是 cookie 过长, 客户端无法存储, 那么就可以通过配置这个 SessionStore 来实现. 即分布式会话. 微软也提供了扩展.
接着, cookie 过期检测.
接着
上面的代码意味着 cookie 可以自动刷新. 通过以下两个参数
如果读取到的客户端的 cookie 支持过期刷新, 那么重新写入到客户端.
ok, 如果没有在客户端读取到 cookie 内容, 意味者 cookie 被清除, 或者用户是第一次登陆, 直接返回认证失败, 如果成功, 执行认证 cookie 校验认证上下文的方法
Events 可以在 AuthenticationSchemeOptions 参数中配置
但是 Cookie 认证参数提供了默认实现
意味者你可以在注入 Cookie 认证服务的时候, 自定义验证 cookie 结果的验证实现.
通过 CookieAuthenticationOptions 的 Events 属性进行注入. 验证完毕,
判断上下文中的 ShouldRenew 参数, 这个你可以根据业务需要执行刷新 cookie 的实现, 最后返回认证结果.
整个流程到这里结束.
2, 应用
构建登陆页面和首页, 直接网上找了, 代码如下:
- using Microsoft.AspNetCore.Authentication;
- using Microsoft.AspNetCore.Authentication.Cookies;
- using Microsoft.AspNetCore.Builder;
- using Microsoft.AspNetCore.Hosting;
- using Microsoft.AspNetCore.Http;
- using Microsoft.Extensions.Configuration;
- using Microsoft.Extensions.DependencyInjection;
- using Microsoft.Extensions.Hosting;
- using System;
- using System.Collections.Generic;
- using System.Linq;
- using System.Security.Claims;
- using System.Text.Encodings.web;
- using System.Threading.Tasks;
- namespace Core.Authentication.Test
- {
- public class Startup
- {
- public Startup(IConfiguration configuration)
- {
- Configuration = configuration;
- }
- public IConfiguration Configuration {
- get;
- }
- public void ConfigureServices(IServiceCollection services)
- {
- services.AddControllers();
- // 注入核心认证组件和 cookie 认证组件
- services.AddAuthentication(options =>
- {
- options.DefaultAuthenticateScheme = CookieAuthenticationDefaults.AuthenticationScheme;
- options.DefaultChallengeScheme = CookieAuthenticationDefaults.AuthenticationScheme;
- }).AddCookie();
- }
- public void Configure(IApplicationBuilder App, IWebHostEnvironment env)
- {
- App.UseAuthentication();
- App.UseAuthorize();
- App.AddLoginhtml();
- App.AddUserInfoHtml();
- }
- }
- public static class CustomMiddleware
- {
- /// <summary>
- /// 登陆页面跳过认证组件
- /// </summary>
- /// <param name="app"></param>
- /// <returns></returns>
- public static IApplicationBuilder UseAuthorize(this IApplicationBuilder App)
- {
- return App.Use(async (context, next) =>
- {
- if (context.Request.Path == "/Account/Login")
- {
- await next();
- }
- else
- {
- var user = context.User;
- if (user?.Identity?.IsAuthenticated ?? false)
- {
- await next();
- }
- else
- {
- await context.ChallengeAsync();
- }
- }
- });
- }
- /// <summary>
- /// 注入登陆页面
- /// </summary>
- /// <param name="app"></param>
- /// <returns></returns>
- public static IApplicationBuilder AddLoginHtml(this IApplicationBuilder App)
- {
- return App.Map("/Account/Login", builder => builder.Run(async context =>
- {
- if (context.Request.Method == "GET")
- {
- await context.Response.WriteHtmlAsync(async res =>
- {
- await res.WriteAsync($"<form method=\"post\">");
- await res.WriteAsync($"<input type=\"hidden\"name=\"returnUrl\"value=\"{
- HttpResponseExtensions.HtmlEncode(context.Request.Query["ReturnUrl"])
- }\"/>");
- await res.WriteAsync($"<div class=\"form-group\"><label > 用户名:<input type=\"text\"name=\"userName\"class=\"form-control\"></label></div>");
- await res.WriteAsync($"<div class=\"form-group\"><label > 密码:<input type=\"password\"name=\"password\"class=\"form-control\"></label></div>");
- await res.WriteAsync($"<button type=\"submit\"class=\"btn btn-default\">登录</button>");
- await res.WriteAsync($"</form>");
- });
- }
- else
- {
- var userName = context.Request.Form["userName"];
- var userPassword = context.Request.Form["password"];
- if (!(userName == "admin" && userPassword == "admin"))
- {
- await context.Response.WriteHtmlAsync(async res =>
- {
- await res.WriteAsync($"<h1 > 用户名或密码错误.</h1>");
- await res.WriteAsync("<a class=\"btn btn-default\"href=\"/Account/Login\">返回</a>");
- });
- }
- else
- {
- // 写入 Cookie
- var claimIdentity = new ClaimsIdentity("Cookie");
- claimIdentity.AddClaim(new Claim(ClaimTypes.NameIdentifier,"1"));
- claimIdentity.AddClaim(new Claim(ClaimTypes.Name, userName));
- claimIdentity.AddClaim(new Claim(ClaimTypes.Email, "1@qq.com"));
- var claimsPrincipal = new ClaimsPrincipal(claimIdentity);
- await context.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, claimsPrincipal);
- if (string.IsNullOrEmpty(context.Request.Form["ReturnUrl"])) context.Response.Redirect("/");
- else context.Response.Redirect(context.Request.Form["ReturnUrl"]);
- }
- }
- }));
- }
- /// <summary>
- /// 注入用户信息页面
- /// </summary>
- /// <returns></returns>`
- public static IApplicationBuilder AddUserInfoHtml(this IApplicationBuilder App)
- {
- return App.Map("/profile", builder => builder.Run(async context =>
- {
- await context.Response.WriteHtmlAsync(async res =>
- {
- await res.WriteAsync($"<h1 > 你好, 当前登录用户: {HttpResponseExtensions.HtmlEncode(context.User.Identity.Name)}</h1>");
- await res.WriteAsync("<a class=\"btn btn-default\"href=\"/Account/Logout\">退出</a>");
- await res.WriteAsync($"<h2>AuthenticationType:{context.User.Identity.AuthenticationType}</h2>");
- await res.WriteAsync("<h2>Claims:</h2>");
- await res.WriteTableHeader(new string[] {
- "Claim Type", "Value"
- },
- context.User.Claims.Select(c => new string[] {
- c.Type, c.Value
- }));
- });
- }));
- }
- }
- public static class HttpResponseExtensions
- {
- public static async Task WriteHtmlAsync(this HttpResponse response, Func<HttpResponse, Task> writeContent)
- {
- var Bootstrap = "<link rel=\"stylesheet\"href=\"https://cdn.bootCSS.com/bootstrap/3.3.7/css/bootstrap.min.css\"integrity=\"sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u\"crossorigin=\"anonymous\">";
- response.ContentType = "text/html";
- await response.WriteAsync($"<!DOCTYPE html><html lang=\"zh-CN\"><head><meta charset=\"UTF-8\">{bootstrap}</head><body><div class=\"container\">");
- await writeContent(response);
- await response.WriteAsync("</div></body></html>");
- }
- public static async Task WriteTableHeader(this HttpResponse response, IEnumerable<string> columns, IEnumerable<IEnumerable<string>> data)
- {
- await response.WriteAsync("<table class=\"table table-condensed\">");
- await response.WriteAsync("<tr>");
- foreach (var column in columns)
- {
- await response.WriteAsync($"<th>{HtmlEncode(column)}</th>");
- }
- await response.WriteAsync("</tr>");
- foreach (var row in data)
- {
- await response.WriteAsync("<tr>");
- foreach (var column in row)
- {
- await response.WriteAsync($"<td>{HtmlEncode(column)}</td>");
- }
- await response.WriteAsync("</tr>");
- }
- await response.WriteAsync("</table>");
- }
- public static string HtmlEncode(string content) =>
- string.IsNullOrEmpty(content) ? string.Empty : HtmlEncoder.Default.Encode(content);
- }
- }
ok, 开始分析代码, 第一步:
中间件放行登陆接口, 接着构建页面. 页面构建完毕. 看登陆方法都干了什么
用户校验通过后, 生成 ClaimsPrincipal 身份证集合, 微软关于身份认证的模型都是基于 Claim 的, 所以包括 id4,identity 登陆组件, 等等里面大量使用到了 ClaimsPrincipal
接着
向浏览器端写入 cookie, 刚刚写的完整的流程, 清了下 cookie, 全都没了, 醉了. 吐槽一下博客园的保存机制, 放 Redis 也好的, 清下 cookie 就没了. 花了这个多时间. 不想在重写一遍了. 这个方法, 我就大致介绍下核心点.
这个方案最终会调到, 完成 cookie 的写入
第一步
这个过程, 可能存在重复登陆的情况.
这里 CookieAuthenticationOptions 通过 Cookie 属性, 你可以自定义 Cookie 配置参数, 默认实现如下:
微软通过 Builder 生成器模式实现. 不明白, 请移步我的设计模式板块, 很简单.
接着构建预登陆上下文
这里 CookieAuthenticationOptions 通过配置 Events 属性, 你可以做一些持久化操作. 或者修改参数, 兼容你的业务
接着
_sessionKey 可能存在已登陆的情况, 那就先清除, 接着通过配置 CookieAuthenticationOptions 的 SessionStore 属性, 你可以实现会话持久化, 或者分布式会话. 自行选择.
接着
向浏览器写入 cookie
不多说, 一样. 你也可以进行持久化操作, 或者修改参数
最后
写 http 头, 没啥东西. 并进行日志记录操作.
ok, 登陆的核心流程到这里介绍, 跑下 demo
此时没有 cookie, 输入 admin admin 登陆.
ok, 登陆成功, cookie 写入完毕. 清除 cookie, 跳转到登陆界面. 整个流程结束. 纯属个人理解, 能力有限, 有问题, 请指正, 谢谢.
除远程登陆外, 其余登陆流程 (Cookie,Jwt) 等都大同小异, 所以接下去有时间, 会分析远程登陆的源码, 但是不想浪费太多时间, 下一张会分析微软的
授权组件, 看看他是如何和认证组件协同工作的. 包括如何集成 id4,identity,jwtbear 完成一整套前端分离架构 (且对移动端友好) 的认证中心的构建.
来源: https://www.cnblogs.com/GreenLeaves/p/12100568.html