如果你沒接觸過舊版Asp.Net Mvc中的 Authorize 或者 Cookie登陸,那么你一定會疑惑 認證這個名詞,這太正式了,這到底代表這什么?
獲取資源之前得先過兩道關卡Authentication & Authorization
要想了解Identity中用戶登錄之后,后續的訪問時怎樣識別用戶的,那首先我們得了解下認證(Authentication) 和授權(Authorization)的含義
Authentication
Authentication就是認證的意思,還舉之前公園的例子,我們拿到門票之后,去公園,入口門衛A首先要根據門票確認我們是誰?是老王,還是老趙,門票是不是真的,過期沒。這個過程,這個識別來訪者是誰的過程就叫做Authentication(身份認證過程)
那Authorization 又是啥?
這兩個單詞太像了,甚至他們的釋義都很像,以至於我們在不了解他們的時候總是弄混他們,Authorization是授權的意思,上一小節中,門衛A識別出了我們是誰?Ok,我們是老李,那么門衛B能不能讓我們進園呢?不一定,門衛B還要再看,門票過期沒,上一步已經看過門票是否過期了,為什么還要看呢?事情是這樣的:門衛A看到門票過期了,然后在門票副卡上寫上“門票過期”四個大字,再寫上“認證失敗”,但是負責認證的門衛A沒有攔着我們,而是繼續讓我們前進,因為動物園可能因為活動而允許過期門票進入,或者沒有門票也可以,那么接下來授權的人門衛B來看門票,他根據動物園切實的情況看,看看許可范圍,再問問后台這個人是不是動物園的管理員。經過種種校驗,發現,雖然門票過期了,但是今天是開放日,沒門票也行,但是我們是普通游客,卻來到了管理員通道,所以沒讓我們進園——授權失敗
小結
好了,這就是認證和授權(Authentication & Authorization),兩個不同的事,由兩個不同的人(或者組件)來做
- 認證用來確認來者是誰,確認身份(確認之后可能沒有身份)
- 授權用來確認持有此身份的來者能不能訪問當前請求的資源
現在,我們要記住認證與授權中的一個要點
認證只確定用戶是誰即使認證失敗,也不會攔截用戶訪問,攔截用戶訪問發生在授權階段
另外要注意的是 Authentication和Authorization並不屬於Identity的一部分,都不屬於Identity,它和Identity是相互獨立的,然后一起協作。也就是說,即便我們沒有使用Identity ,我們有我們自己的用戶存儲,角色等等和身份權限相關的一切,那么我們可以將我們的成員系統完美的與Asp.Net Core 進行集成,我們可以假設,Identity就是我們寫的,然后將其與Asp.Net Core進行集成
那么為什么要將這個與Identity無關的認證過程Authentication放在這里呢?因為它們是協作的 Authentication和Authorization本事就是要與成員系統協作的,在代碼上,他們解耦並且獨立,但是在事實邏輯上,成員系統和認證授權總是一起使用的,所以一起講容易理解
那么這篇文章只講 Authentication與Authorization中的第一個 —— Authentication,先來了解一下,asp.net core 是怎樣知道我們已經登陸的訪客是誰的
身份認證中間件 Authentication Middleware
中間件(Middleware)講起來又是一個長長的故事,如果你完全沒概念,那么我建議你先簡單學習一下asp.net core 中的中間件,你只要知道它的運行原理即可
在一般的asp.net core web 項目中,我們一般把身份認證中間件放在 靜態文件中間件之后,Mvc中間件之前
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
app.UseStaticFiles();
app.UseAuthentication();
app.UseMvc(routes =>
{
//略...
});
}
這樣做的目的很簡單,僅對需要認證的部分做認證
在http請求到達 mvc中間件之前,也就是進入我們寫的邏輯代碼之前,身份認證就結束了,也就是說,身份認證不能在 controller action中控制
我們用一張圖來簡化發生在身份認證中間件中的認證過程,注意,這里馬上要引入一個新的概念
身份認證 handler
中間件是嵌在中間件管道中的一個一個模塊,http請求有條件的流經他們
另一個相似的東西,有很多 handler 嵌在身份認證中間件上,那么http是流經所有的handler嗎?
Authentication Handler
Authentication Hander 顧名思義,他就是切實處理身份認證的組件,它附加在 authentication middleware 上,在請求到來時, middleware 會在所有附加在它之上的handler中選取一個用來做身份認證
那么當我們使用Identity時,哪些 authentication handler 被附加了呢?當請求到來時,authentication middleware 如何知道要選擇哪個handler呢?
接下來,我們一一解答
Cookie Authentication Handler
Identity只添加了一種類型的 handler ——CookieAuthenticationHandler
在我們的StartUp
類中的ConfigureServices
方法中,我們添加了Identity的Service
services.AddIdentity<ApplicationUser, IdentityRole>()
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddDefaultTokenProviders();
但事情沒這么簡單,在添加Identity的同時,Identity還未我們的項目添加了AuthenticationService
和CookieAuthenticationHandler
public static IdentityBuilder AddIdentity<TUser, TRole>(
{
// Services used by identity
services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = IdentityConstants.ApplicationScheme;
// 略...
})
.AddCookie(IdentityConstants.ApplicationScheme, o =>
{
// 略...
在services.AddAuthentication
的內部添加了AuthenticationService
namespace Microsoft.Extensions.DependencyInjection
{
public static class AuthenticationCoreServiceCollectionExtensions
{
public static IServiceCollection AddAuthenticationCore(this IServiceCollection services)
{
services.TryAddScoped<IAuthenticationService, AuthenticationService>();
services.TryAddScoped<IAuthenticationHandlerProvider, AuthenticationHandlerProvider>();
services.TryAddSingleton<IAuthenticationSchemeProvider, AuthenticationSchemeProvider>();
注意上面代碼的最后三行,后面涉及到他們的獲取,他們就是在此處添加的
namespace Microsoft.Extensions.DependencyInjection
{
public static class CookieExtensions
{
public static AuthenticationBuilder AddCookie(this AuthenticationBuilder builder, string authenticationScheme, string displayName, Action<CookieAuthenticationOptions> configureOptions)
{
builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton<IPostConfigureOptions<CookieAuthenticationOptions>, PostConfigureCookieAuthenticationOptions>());
return builder.AddScheme<CookieAuthenticationOptions, CookieAuthenticationHandler>(authenticationScheme, displayName, configureOptions);
上方的代碼添加了 CookieAuthenticationHandler
在添加Authentication service的同時,制定默認的 authentication scheme(這個概念在之前的文章中提到過,如果你還有印象的話) 是誰(就是下方的cookie authentication handler)
這時候另一個問題浮現了,只添加了一個 cookie authentication handler,為什么還要將他制定成默認值,是否有有點多此一舉呢?
雖然Identity只添加了一種類型的 handler(cookie authentication handler),但是他同時添加了多個
在 authentication 中間件上,區分各個handler的方法是指定不同的 authentication scheme,而不是通過 handler 的類型
其實它添加了這么多:
services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = IdentityConstants.ApplicationScheme;
options.DefaultChallengeScheme = IdentityConstants.ApplicationScheme;
options.DefaultSignInScheme = IdentityConstants.ExternalScheme;
})
.AddCookie(IdentityConstants.ApplicationScheme, 略)
.AddCookie(IdentityConstants.ExternalScheme, 略)
.AddCookie(IdentityConstants.TwoFactorRememberMeScheme, 略)
.AddCookie(IdentityConstants.TwoFactorUserIdScheme,略);
不過我們暫時不用關心這些是什么
目前為止我們已經知道了這樣幾件事:
-
添加Identity時,Identity添加了用於身份認證的服務,以及默認激活的用於認證的handler——
CookieAuthenticationHandler
-
Identity的身份認證基於cookie (上篇文章我們了解到 Identity在登陸時將票據加密寫入了cookie)
-
所以用戶登錄后再次訪問的時候,會通過cookie來識別當前用戶是誰
動手實踐
這一小節中我們先編寫測試代碼,來看看認證過程產生了哪些結果
創建一個名為TestAuthController
的控制器,代碼大致如下:
namespace IdentityInAction.Controllers
{
public class TestAuthController : Controller
{
public IActionResult Index()
{
return Json(new
{
User.Identity.IsAuthenticated,
User.Identity.AuthenticationType,
Claims=User.Claims.Select(c => new { c.Type, c.Value })
// 略...
這些代碼將返回一個json字符串,內容是 authentication的部分結果和用戶的claims信息,這三行核心代碼的意思分別是:
- 用戶是否已經通過身份認證
- 對次請求進行認證的handler的名稱(在上篇文章中我們有提到 authentication scheme 就是 authentication type的另一個名字,記住這件事對我們的理解很有幫助)
- 這個用戶的Claims信息
運行程序,不要進行登陸,如果已經登陸了則退出登陸,退出登陸的鏈接是右上角的LogOut
然后訪問http://localhost:{你的端口}/testauth/index
,得到的結果如下:
{
"isAuthenticated": false,
"authenticationType": null,
"claims": []
}
由於沒有用戶登陸,所以結果里幾乎什么都沒有,然后再嘗試登陸后再次訪問這個地址,結果如下:
{
"isAuthenticated": true,
"authenticationType": "Identity.Application",
"claims": [
{
"type": "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier",
"value": "78a032c7-0d67-4cec-b031-2d15a7bac755"
},
{
"type": "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name",
"value": "abc@abc.com"
},
{
"type": "AspNet.Identity.SecurityStamp",
"value": "babbb46b-6ba0-4b87-875a-92088197dfbf"
}
]
}
"isAuthenticated": true
這代表認證成功
"authenticationType": "Identity.Application"
這是對該請求進行認證的handler的名字,由前文我們知道,我們默認的handler是名為IdentityConstants.ApplicationScheme
的cookie handler,我們看一小段源代碼證實一下:
public class IdentityConstants
{
private static readonly string CookiePrefix = "Identity";
public static readonly string ApplicationScheme = CookiePrefix + ".Application";
正如所料,接下來就是claims了,再上篇文章中提到再登陸過程中加入到Identity的claims有這些:
- UserName |
http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier
- UserId|
http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name
- SecurityStamp(如果支持的話)|
AspNet.Identity.SecurityStamp
- 存儲在數據庫中的額外Claims(如果支持的話)(注:支持,但當前用戶沒有)
這些claims隨着票據一起加密寫到了cookie中,現在他們又隨着cookie一起傳了回來
要注意的是,即便我們退出登陸后沒有身份認證失敗了,但是我們仍然獲得了這個Uri的訪問權限,原因在於認證並不阻止用戶,授權才會阻止用戶,而我們沒又做授權方面的限制
看到這里,我們的認證過程的大體已經清楚了,接下來我們要看下整個認證過程的一點細節,整個過程是自上而下的,看標題為工作的那一列
工作 | 注釋 |
---|---|
獲取IAuthenticationHandlerProvider的實例 | |
獲取默認的AuthenticationScheme | |
使用上一步的scheme獲取IAuthenticationService實例 | |
上一步的service通過第一步的IAuthenticationHandlerProvider獲取handler | handler 是 cookie authentication handler |
handler 調用 AuthenticateAsync,這個方法最終調用了handler的HandleAuthenticateAsync① | 這個方法是事實上執行認證的方法 |
獲取 CookieAuthenticationOptions.Cookie.Name指定的存儲票據的cookie的原始字符串 | 這個Name的默認值是`.AspNetCore.Identity.Application` |
解密cookie字符串獲得AuthenticationTicket的實例 | |
檢查是否使用了session存儲,如果有則驗證是否存在對應的session | |
檢查cookie 是否過期 | |
檢查是否需要刷新cookie | |
創建新的AuthenticationTicket | |
將AuthenticationTicket中的Principal設置到HttpContext.User上,認證結束 | 在動手做一節中,我們使用的User就是在這個時候被賦值的 |
需要注意
① 誰進行的驗證
Identity的實現比較復雜,兜兜轉轉最終的驗證時由 cookie authentication handler 的 HandleAuthenticateAsync
完成的,如果你在看Identity源代碼的話,那么直接跳轉到這里可以節省時間
怎么驗證的
事實上,說的簡單一點,就是在登陸的時候,把票據加密寫到cookie里,驗證的時候
獲取cookie >解密 >還原成票據 >把票據塞到http context中
即使是認證失敗了,也是這4個步驟,最終 負責授權的組件會檢查 http context 中的票據,還會結合其它情況來確定是否允許當前的請求繼續進行下去,而我們的邏輯代碼中也可以查看票據,根據不同的 認證結果 返回不同的數據
最后我們貼上AuthenticationService的AuthenticateAsync源代碼,這寫代碼就是上方表格所對應的代碼,但是我們要注意到,表格中的內容還包含這份代碼所調用的其它代碼的步驟
public virtual async Task<AuthenticateResult> AuthenticateAsync(HttpContext context, string scheme)
{
if (scheme == null)
{
var defaultScheme = await Schemes.GetDefaultAuthenticateSchemeAsync();
scheme = defaultScheme?.Name;
if (scheme == null)
{
throw new InvalidOperationException($"No authenticationScheme was specified, and there was no DefaultAuthenticateScheme found.");
}
}
var handler = await Handlers.GetHandlerAsync(context, scheme);
if (handler == null)
{
throw await CreateMissingHandlerException(scheme);
}
var result = await handler.AuthenticateAsync();
if (result != null && result.Succeeded)
{
var transformed = await Transform.TransformAsync(result.Principal);
return AuthenticateResult.Success(new AuthenticationTicket(transformed, result.Properties, result.Ticket.AuthenticationScheme));
}
return result;
}
全文完