.Net Core 認證系統之Cookie認證源碼解析

 

接着上文.Net Core 認證系統源碼解析,Cookie認證算是經常使用的認證模式,可是目前主流都是先後端分離,有點雞肋可是,不考慮移動端的站點或者純管理後臺網站可使用這種認證方式.注意:基於瀏覽器且不是先後端分離的架構(頁面端具備服務端處理能力).移動端就不要考慮了,太麻煩.支持先後端分離前給移動端提供認證Api的通常採用JwtBearer認證,能夠和IdentityServer4的password模式結合.很適用,可是id4的password模式各客戶端必須絕對信任,由於要暴露用戶名密碼.適合作企業級下全部產品的認證.不支持除企業外的第三方調用.固然id4提供了其餘模式.這是題外話.可是場景得介紹清楚.以避免誤導你們!css

一、Cookie認證流程html

 引入核心認證組件以後,經過擴展的方式引入Cookie認證,微軟採用鏈式編程,很優雅.Net Core的一大特色.前端

 注入Cookie認證方案,指定Cookie認證參數,並指定Cookie認證處理器,先不介紹參數,看看處理器幹了什麼.redis

Cookie的核心認證方法,第一步以下:數據庫

 

 

 一些必須的防重複執行操做,沒截圖,也不介紹了,安全工做,只貼核心代碼.第一步,就是去讀取客戶端存在的cookie信息.編程

 微軟在Cookie認證參數中提供了接口,意味者你能夠自定義讀取Cookie內容的實現,他會把上下文和Cookie的名稱傳給你,這樣就能定製獲取Cookie內容的實現.接着解密Cookie內容bootstrap

 

 微軟注入了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的實現,最後返回認證結果.

整個流程到這裏結束.

 

二、應用

 構建登錄頁面和首頁,直接網上找了,代碼以下:

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的,因此包括id四、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)等都大同小異,因此接下去有時間,會分析遠程登錄的源碼,可是不想浪費太多時間,下一張會分析微軟的

 

 受權組件,看看他是如何和認證組件協同工做的.包括如何集成id四、identity、jwtbear完成一整套前端分離架構(且對移動端友好)的認證中心的構建.

相關文章
相關標籤/搜索