ASP.NET Core Authentication and Authorization

来源:https://www.cnblogs.com/kklldog/archive/2020/04/02/auth-in-aspnetcore.html
-Advertisement-
Play Games

最近把一個Asp .net core 2.0的項目遷移到Asp .net core 3.1,項目啟動的時候直接報錯: 看意思是缺少了一個authorization的中間件,這個項目在Asp.net core 2.0上是沒問題的。 startup是這樣註冊的: 查了文檔後發現3.0的示例代碼多了一個U ...


最近把一個Asp .net core 2.0的項目遷移到Asp .net core 3.1,項目啟動的時候直接報錯:

InvalidOperationException: Endpoint CoreAuthorization.Controllers.HomeController.Index (CoreAuthorization) contains authorization metadata, but a middleware was not found that supports authorization.
Configure your application startup by adding app.UseAuthorization() inside the call to Configure(..) in the application startup code. The call to app.UseAuthorization() must appear between app.UseRouting() and app.UseEndpoints(...).
Microsoft.AspNetCore.Routing.EndpointMiddleware.ThrowMissingAuthMiddlewareException(Endpoint endpoint)

看意思是缺少了一個authorization的中間件,這個項目在Asp.net core 2.0上是沒問題的。
startup是這樣註冊的:

public class Startup
    {
        public Startup(IConfiguration configuration)
        {
            Configuration = configuration;
        }

        public IConfiguration Configuration { get; }

        // This method gets called by the runtime. Use this method to add services to the container.
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme).AddCookie(options =>
            {
                options.LoginPath = "/account/Login";
            });
            
            services.AddControllersWithViews();
        }

        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }
            else
            {
                app.UseExceptionHandler("/Home/Error");
                // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
                app.UseHsts();
            }
            //app.UseHttpsRedirection();
            app.UseStaticFiles();

            app.UseRouting();

            app.UseAuthentication();

            app.UseEndpoints(endpoints =>
            {
                endpoints.MapControllerRoute(
                    name: "default",
                    pattern: "{controller=Home}/{action=Index}/{id?}");
            });
        }
    }

查了文檔後發現3.0的示例代碼多了一個UseAuthorization,改成這樣就可以了:

 app.UseRouting();
 app.UseAuthentication();
 //use授權中間件
 app.UseAuthorization();

 app.UseEndpoints(endpoints =>
            {
                endpoints.MapControllerRoute(
                    name: "default",
                    pattern: "{controller=Home}/{action=Index}/{id?}");
            });

看來Asp .net Core 3.1的認證跟授權又不太一樣了,只能繼續看文檔學習了。

UseAuthentication and UseAuthorization

先說一下Authentication跟Authorization的區別。這兩個單詞長的十分相似,而且還經常一起出現,很多時候容易搞混了。

  1. Authentication是認證,明確是你誰,確認是不是合法用戶。常用的認證方式有用戶名密碼認證。
  2. Authorization是授權,明確你是否有某個許可權。當用戶需要使用某個功能的時候,系統需要校驗用戶是否需要這個功能的許可權。
    所以這兩個單詞是不同的概念,不同層次的東西。UseAuthorization在asp.net core 2.0中是沒有的。在3.0之後微軟明確的把授權功能提取到了Authorization中間件里,所以我們需要在UseAuthentication之後再次UseAuthorization。否則,當你使用授權功能比如使用[Authorize]屬性的時候系統就會報錯。

Authentication(認證)

認證的方案有很多,最常用的就是用戶名密碼認證,下麵演示下基於用戶名密碼的認證。新建一個MVC項目,添加AccountController:

        [HttpPost]
        public async Task<IActionResult> Login(
            [FromForm]string userName, [FromForm]string password
           )
        {
            //validate username password
            ...
            var claims = new List<Claim>
                {
                  new Claim(ClaimTypes.Name, userName),
                  new Claim(ClaimTypes.Role, "老師")
                };

            var claimsIdentity = new ClaimsIdentity(
                claims, CookieAuthenticationDefaults.AuthenticationScheme);

            await HttpContext.SignInAsync(
                CookieAuthenticationDefaults.AuthenticationScheme,
                new ClaimsPrincipal(claimsIdentity));

            return Redirect("/");
        }
         public async Task<IActionResult> Logoff()
        {
            await HttpContext.SignOutAsync();

            return Redirect("Login");
        }

        public IActionResult AccessDenied()
        {
            return Content("AccessDenied");
        }

修改login.cshtml

@{
    ViewData["Title"] = "Login Page";
}

    <h1>
        Login Page
    </h1>

    <form method="post">
        <p>
            用戶名: <input name="userName" value="administrator" />
        </p>
        <p>
            密碼: <input name="password" value="123" />
        </p>
       
        <p>
            <button>登錄</button>
        </p>
    </form>

從前臺傳入用戶名密碼後進行用戶名密碼校驗(示例代碼省略了密碼校驗)。如果合法,則把用戶的基本信息存到一個claim list里,並且指定cookie-base的認證存儲方案。最後調用SignInAsync把認證信息寫到cookie中。根據cookie的特性,接來下所有的http請求都會攜帶cookie,所以系統可以對接來下用戶發起的所有請求進行認證校驗。Claim有很多翻譯,個人覺得叫“聲明”比較好。一單認證成功,用戶的認證信息里就會攜帶一串Claim,其實就是用戶的一些信息,你可以存任何你覺得跟用戶相關的東西,比如用戶名,角色等,當然是常用的信息,不常用的信息建議在需要的時候查庫。調用HttpContext.SignOutAsync()方法清除用戶登認證信息。
Claims信息我們可以方便的獲取到:

@{
    ViewData["Title"] = "Home Page";
}

    <h2>
        CoreAuthorization
    </h2>

<p>
    @Context.User.FindFirst(System.Security.Claims.ClaimTypes.Name)?.Value
</p>
<p>
    角色:
    @foreach (var claims in Context.User.Claims.Where(c => c.Type == System.Security.Claims.ClaimTypes.Role))
    {
        <span> @claims.Value </span>
    }
</p>
<p>
    <a href="/Student/index">/Student/index</a>
</p>
<p>
    <a href="/Teacher/index">/Teacher/Index</a>
</p>
<p>
    <a href="/Teacher/Edit">/Student/Edit</a>
</p>

<p>
    <a href="/Account/Logoff">退出</a>
</p>

改一下home/Index頁面的html,把這些claim信息展示出來。

以上就是一個基於用戶名密碼以及cookie的認證方案。

Authorization(授權)

有了認證我們還需要授權。剛纔我們實現了用戶名密碼登錄認證,但是系統還是沒有任何管控,用戶可以隨意查庫任意頁面。現實中的系統往往都是某些頁面可以隨意查看,有些頁面則需要認證授權後才可以訪問。

AuthorizeAttribute

當我們希望一個頁面只有認證後才可以訪問,我們可以在相應的Controller或者Action上打上AuthorizeAttribute這個屬性。修改HomeController:

    [Authorize]
    public class HomeController : Controller
    {
        public IActionResult Index()
        {
            return View();
        }

    }

重新啟動網站,如果沒有登錄,訪問home/index的時候網站會跳轉到/account/AccessDenied。如果登錄後則可以正常訪問。AuthorizeAttribute預設授權校驗其實是把認證跟授權合為一體了,只要認證過,就認為有授權,這是也是最最簡單的授權模式。

基於角色的授權策略

顯然上面預設的授權並不能滿足我們開發系統的需要。AuthorizeAttribute還內置了基於Role(角色)的授權策略。
登錄的時候給認證信息加上角色的聲明:

  [HttpPost]
        public async Task<IActionResult> Login(
            [FromForm]string userName, 
            [FromForm]string password
            )
        {
            //validate username password

            var claims = new List<Claim>
                {
                  new Claim(ClaimTypes.Name, userName),
                  new Claim(ClaimTypes.Role, "老師"),
                };

            var claimsIdentity = new ClaimsIdentity(
                claims, CookieAuthenticationDefaults.AuthenticationScheme);

            await HttpContext.SignInAsync(
                CookieAuthenticationDefaults.AuthenticationScheme,
                new ClaimsPrincipal(claimsIdentity));

            return Redirect("/");
        }

新建一個TeacherController:

    [Authorize(Roles = "老師")]
    public class TeacherController : Controller
    {
        public IActionResult Index()
        {
            return Content("Teacher index");
        }
    }

給AuthorizeAttribute的屬性設置Roles=老師,表示只有老師角色的用戶才可以訪問。如果某個功能可以給多個角色訪問那麼可以給Roles設置多個角色,使用逗號進行分割。

  [Authorize(Roles = "老師,校長")]
    public class TeacherController : Controller
    {
        public IActionResult Index()
        {
            return Content("Teacher index");
        }

    }

這樣認證的用戶只要具有老師或者校長其中一個角色就可以訪問。

基於策略的授權

上面介紹了內置的基於角色的授權策略。如果現實中需要更複雜的授權方案,我們還可以自定義策略來支持。比如我們下麵定義一個策略:編輯功能只能姓王的老師可以訪問。
定義一個要求:

 public class LastNamRequirement : IAuthorizationRequirement
    {
        public string LastName { get; set; }
    }

IAuthorizationRequirement其實是一個空介面,僅僅用來標記,繼承這個介面就是一個要求。這是空介面,所以要求的定義比較寬鬆,想怎麼定義都可以,一般都是根據具體的需求設置一些屬性。比如上面的需求,本質上是根據老師的姓來決定是否授權通過,所以把姓作為一個屬性暴露出去,以便可以配置不同的姓。
除了要求,我們還需要實現一個AuthorizationHandler:

 public class LastNameHandler : AuthorizationHandler<IAuthorizationRequirement>
    {
        protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, IAuthorizationRequirement requirement)
        {
            var lastNameRequirement = requirement as LastNamRequirement;
            if (lastNameRequirement == null)
            {
                return Task.CompletedTask;
            }

            var isTeacher = context.User.HasClaim((c) =>
            {
                return c.Type == System.Security.Claims.ClaimTypes.Role && c.Value == "老師";
            });
            var isWang = context.User.HasClaim((c) =>
            {
                return c.Type == "LastName" && c.Value == lastNameRequirement.LastName;
            });

            if (isTeacher && isWang)
            {
                context.Succeed(requirement);
            }

            return Task.CompletedTask;
        }
    }

AuthorizationHandler是一個抽象類,繼承它後需要重寫其中的HandleRequirementAsync方法。這裡才是真正判斷是否授權成功的地方。要求(Requirement)跟用戶的聲明(Claim)信息會被傳到這方法里,然後我們根據這些信息進行判斷,如果符合授權就調用context.Succeed方法。這裡註意如果不符合請謹慎調用context.Failed方法,因為策略之間一般是OR的關係,這個策略不通過,可能有其他策略通過
在ConfigureServices方法中添加策略跟註冊AuthorizationHandler到DI容器中:

services.AddSingleton<IAuthorizationHandler, LastNameHandler>();
services.AddAuthorization(options =>
     {
        options.AddPolicy("王老師", policy =>
            policy.AddRequirements(new LastNamRequirement { LastName = "王" })
        );
    });

使用AddSingleton生命周期來註冊LastNameHandler,這個生命周期並不一定要單例,看情況而定。在AddAuthorization中添加一個策略叫"王老師"。這裡有個個人認為比較怪的地方,為什麼AuthorizationHandler不是在AddAuthorization方法中配置?而是僅僅註冊到容器中就可以開始工作了。如果有一個需求,僅僅是需要自己調用一下自定義的AuthorizationHandler,而並不想它真正參與授權。這樣的話就不能使用DI的方式來獲取實例了,因為一註冊進去就會參與授權的校驗了。
在TeacherController下添加一個 Edit Action:

  [Authorize(Policy="王老師")]
public IActionResult Edit()
{
    return Content("Edit success");
}

給AuthorizeAttribute的Policy設置為“王老師”。
修改Login方法添加一個姓的聲明:

  [HttpPost]
        public async Task<IActionResult> Login(
            [FromForm]string userName, 
            [FromForm]string password
            )
        {
            //validate username password

            var claims = new List<Claim>
                {
                  new Claim(ClaimTypes.Name, userName),
                  new Claim(ClaimTypes.Role, "老師"),
                   new Claim("LastName", "王"),
                };

            var claimsIdentity = new ClaimsIdentity(
                claims, CookieAuthenticationDefaults.AuthenticationScheme);

            await HttpContext.SignInAsync(
                CookieAuthenticationDefaults.AuthenticationScheme,
                new ClaimsPrincipal(claimsIdentity));

            return Redirect("/");
        }

運行一下程式,訪問一下/teacher/edit,可以看到訪問成功了。如果修改Login方法,修改LastName的聲明為其他值,則訪問會拒絕。

使用泛型Func方法配置策略

如果你的策略比較簡單,其實還有個更簡單的方法來配置,就是在AddAuthorization方法內直接使用一個Func來配置策略。
使用Func來配置一個女老師的策略:

 options.AddPolicy("女老師", policy =>
    policy.RequireAssertion((context) =>
        {
            var isTeacher = context.User.HasClaim((c) =>
            {
                return c.Type == System.Security.Claims.ClaimTypes.Role && c.Value == "老師";
            });
            var isFemale = context.User.HasClaim((c) =>
            {
                return c.Type == "Sex" && c.Value == "女";
            });

                return isTeacher && isFemale;
        }
    )
);

總結

  1. Authentication跟Authorization是兩個不同的概念。Authentication是指認證,認證用戶的身份;Authorization是授權,判斷是否有某個功能的許可權。
  2. Authorization內置了基於角色的授權策略。
  3. 可以使用自定義AuthorizationHandler跟Func的方式來實現自定義策略。

吐槽

關於認證跟授權微軟為我們考慮了很多很多,包括identityserver,基本上能想到的都有了,什麼oauth,openid,jwt等等。其實本人是不太喜歡用的。雖然微軟都給你寫好了,考慮很周到,但是學習跟Trouble shooting都是要成本的。其實使用中間件、過濾器再配合redis等組件,很容易自己實現一套授權認證方案,自由度也更高,有問題修起來也更快。自己實現一下也可以更深入的瞭解某項的技術,比如jwt是如果工作的,oauth是如何工作的,這樣其實更有意義。


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 作者:網易雲 鏈接:https://www.zhihu.com/question/27696290/answer/381993207 來源:知乎 著作權歸作者所有。商業轉載請聯繫作者獲得授權,非商業轉載請註明出處。 什麼是大數據 近幾年,市場上出現了很多和大數據相關的崗位,不管是數據分析、數據挖掘, ...
  • 大家好,我是小棧君,因為個人和工作的緣故,所以拖更了一點時間,但是關於拖更的內容小棧君會在後續的時間中補回來,還希望大家繼續支持和關註小棧君。當然,在國內疫情稍微減緩的情況下,小棧君在這裡也多說兩句,在非常時刻,我們應當保持警惕,清洗手,多通風,避免人群聚集,希望大家平安健康, 閑話不多說,我們直接 ...
  • 前文總結了NIO的內容,有了NIO的一些基礎之後,我們就可以來看下Netty。Netty是Java領域的高性能網路傳輸框架,RPC的技術核心就是網路傳輸和序列化,所以Netty給予了RPC在網路傳輸領域巨大的支持。 一個簡單的Netty代碼實現 網路傳輸基於的是TCP協議,所以會有服務端和客戶端之分 ...
  • "OpenWrite 博客群發" 第一步:前往 "七牛雲" 官方註冊賬號 第二步:在“個人中心”中完成賬號實名認證 第三步:添加對象存儲 第四步:輸入存儲空間名稱,並將訪問控制設置為“公開” 第五步:在密鑰管理中創建AccessKey和SecretKey 第六步:在openwrite的高級配置中,圖 ...
  • volatile是Java虛擬機提供的 輕量級 的同步機制(“乞丐版”的synchronized) 1. 保證可見性 2. 不保證原子性 3. 禁止指令重排 可見性 指當多個線程訪問同一個變數時,如果其中一個線程修改了這個變數的值,其他線程能夠立即看得到修改的值 驗證可見性demo: 結果: 不保證 ...
  • 1、一張表,裡面有 ID 自增主鍵,當 insert 了 17 條記錄之後,刪除了第 15,16,17 條記錄,再把 Mysql 重啟,再 insert 一條記錄,這條記錄的 ID 是 18 還是 15 ? (1)如果表的類型是 MyISAM,那麼是 18因為 MyISAM 表會把自增主鍵的最大 I ...
  • 1. 構造函數 1.1. 構造函數特征 與類相同名稱,不聲明返回值類型。 不能被static、final、synchronized、abstract、native修飾,不能有return語句。 1.2. 構造函數作用 創建對象,給對象進行初始化。 1.3. 構造函數分類 隱式無參構造器,顯示構造器。 ...
  • 摘要:本文實例講述了php非同步多線程swoole用法。分享給大家供大家參考。具體分析如下:swoole重新定義PHP語言的高性能網路通信框架,提供了PHP語言的非同步多線程服務,下麵的實例就可以證實這一功能。一般來說,Swoole提供了PHP語言的非同步多線程伺服器,非同步TCP/UDP網路客戶端,非同步M ...
一周排行
    -Advertisement-
    Play Games
  • GoF之工廠模式 @目錄GoF之工廠模式每博一文案1. 簡單說明“23種設計模式”1.2 介紹工廠模式的三種形態1.3 簡單工廠模式(靜態工廠模式)1.3.1 簡單工廠模式的優缺點:1.4 工廠方法模式1.4.1 工廠方法模式的優缺點:1.5 抽象工廠模式1.6 抽象工廠模式的優缺點:2. 總結:3 ...
  • 新改進提供的Taurus Rpc 功能,可以簡化微服務間的調用,同時可以不用再手動輸出模塊名稱,或調用路徑,包括負載均衡,這一切,由框架實現並提供了。新的Taurus Rpc 功能,將使得服務間的調用,更加輕鬆、簡約、高效。 ...
  • 本章將和大家分享ES的數據同步方案和ES集群相關知識。廢話不多說,下麵我們直接進入主題。 一、ES數據同步 1、數據同步問題 Elasticsearch中的酒店數據來自於mysql資料庫,因此mysql數據發生改變時,Elasticsearch也必須跟著改變,這個就是Elasticsearch與my ...
  • 引言 在我們之前的文章中介紹過使用Bogus生成模擬測試數據,今天來講解一下功能更加強大自動生成測試數據的工具的庫"AutoFixture"。 什麼是AutoFixture? AutoFixture 是一個針對 .NET 的開源庫,旨在最大程度地減少單元測試中的“安排(Arrange)”階段,以提高 ...
  • 經過前面幾個部分學習,相信學過的同學已經能夠掌握 .NET Emit 這種中間語言,並能使得它來編寫一些應用,以提高程式的性能。隨著 IL 指令篇的結束,本系列也已經接近尾聲,在這接近結束的最後,會提供幾個可供直接使用的示例,以供大伙分析或使用在項目中。 ...
  • 當從不同來源導入Excel數據時,可能存在重覆的記錄。為了確保數據的準確性,通常需要刪除這些重覆的行。手動查找並刪除可能會非常耗費時間,而通過編程腳本則可以實現在短時間內處理大量數據。本文將提供一個使用C# 快速查找並刪除Excel重覆項的免費解決方案。 以下是實現步驟: 1. 首先安裝免費.NET ...
  • C++ 異常處理 C++ 異常處理機制允許程式在運行時處理錯誤或意外情況。它提供了捕獲和處理錯誤的一種結構化方式,使程式更加健壯和可靠。 異常處理的基本概念: 異常: 程式在運行時發生的錯誤或意外情況。 拋出異常: 使用 throw 關鍵字將異常傳遞給調用堆棧。 捕獲異常: 使用 try-catch ...
  • 優秀且經驗豐富的Java開發人員的特征之一是對API的廣泛瞭解,包括JDK和第三方庫。 我花了很多時間來學習API,尤其是在閱讀了Effective Java 3rd Edition之後 ,Joshua Bloch建議在Java 3rd Edition中使用現有的API進行開發,而不是為常見的東西編 ...
  • 框架 · 使用laravel框架,原因:tp的框架路由和orm沒有laravel好用 · 使用強制路由,方便介面多時,分多版本,分文件夾等操作 介面 · 介面開發註意欄位類型,欄位是int,查詢成功失敗都要返回int(對接java等強類型語言方便) · 查詢介面用GET、其他用POST 代碼 · 所 ...
  • 正文 下午找企業的人去鎮上做貸後。 車上聽同事跟那個司機對罵,火星子都快出來了。司機跟那同事更熟一些,連我在內一共就三個人,同事那一手指桑罵槐給我都聽愣了。司機也是老社會人了,馬上聽出來了,為那個無辜的企業經辦人辯護,實際上是為自己辯護。 “這個事情你不能怪企業。”“但他們總不能讓銀行的人全權負責, ...