ASP.NET Core Authentication and Authorization


最近把一個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是如何工作的,這樣其實更有意義。

關注我的公眾號一起玩轉技術


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM