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的差別。這兩個單詞長的十分相似,而且還經常一起出現,很多時候容易搞混了。
Authentication是認證,明确是你誰,确認是不是合法使用者。常用的認證方式有使用者名密碼認證。
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>
@Context.User.FindFirst(System.Security.Claims.ClaimTypes.Name)?.Value
角色:
@foreach (var claims in Context.User.Claims.Where(c => c.Type == System.Security.Claims.ClaimTypes.Role))
{
<span> @claims.Value </span>
}
<a href="/Student/index">/Student/index</a>
<a href="/Teacher/index">/Teacher/Index</a>
<a href="/Teacher/Edit">/Student/Edit</a>
<a href="/Account/Logoff">退出</a>
改一下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
{
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();
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方法添加一個姓的聲明:
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;
}
)
);
總結
Authentication跟Authorization是兩個不同的概念。Authentication是指認證,認證使用者的身份;Authorization是授權,判斷是否有某個功能的權限。
Authorization内置了基于角色的授權政策。
可以使用自定義AuthorizationHandler跟Func的方式來實作自定義政策。
吐槽
關于認證跟授權微軟為我們考慮了很多很多,包括identityserver,基本上能想到的都有了,什麼oauth,openid,jwt等等。其實本人是不太喜歡用的。雖然微軟都給你寫好了,考慮很周到,但是學習跟Trouble shooting都是要成本的。其實使用中間件、過濾器再配合redis等元件,很容易自己實作一套授權認證方案,自由度也更高,有問題修起來也更快。自己實作一下也可以更深入的了解某項的技術,比如jwt是如果工作的,oauth是如何工作的,這樣其實更有意義。
Email:[email protected]
作者:Agile.Zhou(kklldog)
出處:
http://www.cnblogs.com/kklldog/