跌倒了,再爬起來:ASP.NET 5 Identity

112326321091363.jpg

“跌倒了”指的是這一篇博文:愛與恨的抉擇:ASP.NET 5+EntityFramework 7

如果想了解 ASP.NET Identity 的“歷史”及“原理”,強烈建議讀一下這篇博文:MVC5 - ASP.NET Identity登錄原理 - Claims-based認證和OWIN,如果你有時間,也可以讀下 Jesse Liu 的 Membership 三部曲:

其實說來慚愧,我自己對 ASP.NET Identity 的理解及運用,僅限在使用 AuthorizeAttribute、FormsAuthentication.SetAuthCookie 等一些操作,背後的原理及其發展歷程並不是很瞭解,所以我當時在 ASP.NET 5 中進行身份驗證操作,纔會讓自己有種“無助”的感覺,週末的時候,閱讀了 Jesse Liu 的這幾篇博文,然後又找了一些相關資料,自己似乎懂得了一些,但好像又沒有完全理解,既然說不出來,那就用“筆”記下來。

ASP.NET Identity GitHub 地址:https://github.com/aspnet/Identity

ASP.NET 5 中,關於身份驗證的變化其實不大,還是 MVC5 的那一套,只不過配置有的變化罷了,使用 VS2015 創建 MVC 項目的時候,點擊“Change Authentication”會出現下面四個選項:

111603100469274.png

如果創建的是 ASP.NET 5 項目,Authentication 默認是不可更改:

111604585005608.png

使用 VS2015 分別創建 MVC5 及 ASP.NET 5 的示例項目,你會發現 MVC5 中關於身份驗證的代碼及配置非常複雜,而在 ASP.NET 5 中則相對來說簡化下,首先,在 Startup.cs 文件中的 ConfigureServices 方法中,有如下配置:

public void ConfigureServices(IServiceCollection services)
{
    // Add EF services to the services container.
    services.AddEntityFramework(Configuration)
        .AddSqlServer()
        .AddDbContext<ApplicationDbContext>();

    // Add Identity services to the services container.
    services.AddDefaultIdentity<ApplicationDbContext, ApplicationUser, IdentityRole>(Configuration);
    services.AddIdentityEntityFramework<ApplicationDbContext, ApplicationUser, IdentityRole>(Configuration);
    services.AddIdentity<ApplicationUser, IdentityRole>(Configuration);

    // Add MVC services to the services container.
    services.AddMvc();
}

上面代碼中,AddDefaultIdentity 和 AddIdentityEntityFramework 其實是一個意思(“捆綁銷售”),所在程序集:Microsoft.AspNet.Identity.EntityFramework,AddEntityFramework 和 AddIdentityEntityFramework 使用的是同一個 DbContext,當然也可以進行對身份驗證上下文進行分開管理,比如我們有可能多個應用程序共享一個身份驗證的上下文。ConfigureServices 方法的解釋爲:This method gets called by the runtime,表示這個方法在應用程序運行的時候註冊使用的服務,有點類似於組件化的應用,比如 ASP.NET 5 只是一個基礎 Web 站點,你可以在這個應用中添加你想要的組件或模塊,比如你想使用 WebAPI,你只需要在 project.json 中添加 Microsoft.AspNet.Mvc.WebApiCompatShim 程序包,然後在 ConfigureServices 方法中進行服務註冊就行了:services.AddWebApiConventions();。

AddDefaultIdentity 註冊的三個基礎類型:

  • IdentityDbContext< IdentityUser >:ApplicationDbContext 繼承實現。

  • IdentityUser:ApplicationUser 繼承實現。

  • IdentityRole

註冊完成之後,就是配置使用了,在 Startup.cs 的 Configure 方法中進行配置使用:app.UseIdentity();,表示應用程序啓用身份驗證,如果把這段代碼註釋掉的話,你會發現整個應用程序的身份驗證就失效了,Configure 方法解釋是:Configure is called after ConfigureServices is called,在上面 AddDefaultIdentity 註冊中,其實包含了很多內容,關於身份驗證基本上就這三個類型,ASP.NET Identity 直接的操作通過註冊的這三個類型進行以來注入,比如後面會遇到的 UserManager 和 SignInManager,但查看這部分的源代碼,在 Microsoft.AspNet.Identity.EntityFramework 中並沒有加入進來。

下面我們來根據 ASP.NET Identity 的源碼,來看一個身份驗證的流程,ASP.NET 5 中的身份驗證和之前一樣,只需要在需要驗證的 Action 上面添加 Authorize 就行了,在上面 Startup.cs 中的身份驗證配置很簡單,啓用的話只需要 app.UseIdentity(); 就可以了,而在之前的 MVC 程序的 Web.config 中需要配置一大堆東西,在 IdentityServiceCollectionExtensions 源碼中,包含了一大堆默認配置,比如 ApplicationCookieAuthenticationType 註冊:

services.Configure<CookieAuthenticationOptions>(options =>
{
    options.AuthenticationType = IdentityOptions.ApplicationCookieAuthenticationType;
    options.LoginPath = new PathString("/Account/Login");
    options.Notifications = new CookieAuthenticationNotifications
    {
        OnValidateIdentity = SecurityStampValidator.ValidateIdentityAsync
    };
}, IdentityOptions.ApplicationCookieAuthenticationType);

我們也可以在 Configure 中進行自定義配置,配置方法:app.UseCookieAuthentication。當訪問 Action 的身份驗證失效後,跳轉到“/Account/Login”進行登錄,查看 AccountController 中的示例代碼,你會發現有下面的東西:

public class AccountController : Controller
{
    public AccountController(UserManager<ApplicationUser> userManager, SignInManager<ApplicationUser> signInManager)
    {
        UserManager = userManager;
        SignInManager = signInManager;
    }

    public UserManager<ApplicationUser> UserManager { get; private set; }
    public SignInManager<ApplicationUser> SignInManager { get; private set; }
}

查看整個的應用程序的代碼,發現我們並沒有註冊 UserManager、SignInManager 類型的依賴注入,那是怎麼注入的呢?其實注入的類型不是 UserManager 和 SignInManager,而是 IdentityUser,在 ConfigureServices 中我們添加過這樣的代碼:services.AddDefaultIdentity<ApplicationDbContext, ApplicationUser, IdentityRole>(Configuration);,這是最重要的,之後所有身份驗證操作所用到的基類型都是從這裏來的,在 IdentityServiceCollectionExtensions 中的 AddIdentity 操作中,我們發現了下面這樣的代碼:

services.TryAdd(describe.Scoped<UserManager<TUser>, UserManager<TUser>>());
services.TryAdd(describe.Scoped<SignInManager<TUser>, SignInManager<TUser>>());
services.TryAdd(describe.Scoped<RoleManager<TRole>, RoleManager<TRole>>());

Scoped 所在程序集:Microsoft.Framework.DependencyInjection,DependencyInjection 爲 ASP.NET 5 自帶的依賴注入,如果你仔細查看其相關類型的源碼,發現都是通過這個東西進行 IoC 管理的,下面我們看一個 Login 操作:

[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Login(LoginViewModel model, string returnUrl = null)
{
    if (ModelState.IsValid)
    {
        var signInStatus = await SignInManager.PasswordSignInAsync(model.UserName, model.Password, model.RememberMe, shouldLockout: false);
        switch (signInStatus)
        {
            case SignInStatus.Success:
                return RedirectToLocal(returnUrl);
            case SignInStatus.Failure:
            default:
                ModelState.AddModelError("", "Invalid username or password.");
                return View(model);
        }
    }

    // If we got this far, something failed, redisplay form
    return View(model);
}

最主要的操作是,通過 ASP.NET Identity 的 SignInManager.PasswordSignInAsync 操作,進行驗證身份密碼,返回 SignInStatus 類型的驗證結果:

public enum SignInStatus
{
    Success = 0,
    LockedOut = 1,
    RequiresVerification = 2,
    Failure = 3
}

我們來看一下 SignInManager.PasswordSignInAsync 中究竟幹了什麼事:

public virtual async Task<SignInResult> PasswordSignInAsync(TUser user, string password, 
    bool isPersistent, bool shouldLockout, CancellationToken cancellationToken = default(CancellationToken))
{
    if (user == null)
    {
        throw new ArgumentNullException(nameof(user));
    }
    var error = await PreSignInCheck(user, cancellationToken);
    if (error != null)
    {
        return error;
    }
    if (await IsLockedOut(user, cancellationToken))
    {
        return SignInResult.LockedOut;
    }
    if (await UserManager.CheckPasswordAsync(user, password, cancellationToken))
    {
        await ResetLockout(user, cancellationToken);
        return await SignInOrTwoFactorAsync(user, isPersistent, cancellationToken);
    }
    if (UserManager.SupportsUserLockout && shouldLockout)
    {
        // If lockout is requested, increment access failed count which might lock out the user
        await UserManager.AccessFailedAsync(user, cancellationToken);
        if (await UserManager.IsLockedOutAsync(user, cancellationToken))
        {
            return SignInResult.LockedOut;
        }
    }
    return SignInResult.Failed;
}

PasswordSignInAsync 還有一個重寫方法,是獲取用戶信息的:UserManager.FindByNameAsync(userName, cancellationToken);,接着查看 FindByNameAsync 的定義,會找到這段代碼:Store.FindByNameAsync(userName, cancellationToken),Store 是什麼?類型定義爲:IUserStore<TUser> Store,它就像一個倉庫,爲用戶驗證提供查詢及存儲服務,除了 IUserStore,在 UserManager 中,你還會發現有很多的“Store”,比如 IUserLoginStore、IUserRoleStore、IUserClaimStore 等等,但都是繼承於 IUserStore,在 ConfigureServices 進行配置服務的時候,services.AddIdentity 還有一個 AddEntityFrameworkStores 方法,範型類型爲 TContext,上面所有的 Store 上下文都是從它繼承來的,再查看 AddEntityFrameworkStores 的實現:

public static IdentityBuilder AddEntityFrameworkStores<TContext>(this IdentityBuilder builder)
    where TContext : DbContext
{
    builder.Services.Add(IdentityEntityFrameworkServices.GetDefaultServices(builder.UserType, builder.RoleType, typeof(TContext)));
    return builder;
}

builder.Services.Add 所起到的作用就是往 IoC 中注入類型,這樣所有用到此類型的引用,都可以通過構造函數注入方式獲取其實現,再查看 GetDefaultServices 的具體實現,因爲看不懂代碼,就不貼出來了,其實裏面操作的就三個類型:TUser、TRole 和 TContext,這也是 ASP.NET Identity 操作的三個基本類型,在 Identity 操作中,基本上是兩大操作類,一個是 SignInManager,另一個就是 UserManager,其實查看
SignInManager 的具體實現代碼,你會發現,關於用戶的獲取及存儲,都是通過 UserManager 進行操作的,而 UserManager 又是通過 IUserStore 的具體實現類進行操作的,SignInManager 只不過是一個用戶驗證的操作類,比如我們一開始說到的 SignInManager.PasswordSignInAsync,上面已經貼出代碼了,你會看到基本上都是 UserManager.什麼,比如 UserManager.CheckPasswordAsync、UserManager.SupportsUserLockout、UserManager.AccessFailedAsync 等等,在 PasswordSignInAsync 代碼實現中,不關於用戶操作的,最核心的就是這段代碼:SignInOrTwoFactorAsync(user, isPersistent, cancellationToken);,查看其具體實現:

private async Task<SignInResult> SignInOrTwoFactorAsync(TUser user, bool isPersistent,
    CancellationToken cancellationToken, string loginProvider = null)
{
    if (UserManager.SupportsUserTwoFactor && 
        await UserManager.GetTwoFactorEnabledAsync(user, cancellationToken) &&
        (await UserManager.GetValidTwoFactorProvidersAsync(user, cancellationToken)).Count > 0)
    {
        if (!await IsTwoFactorClientRememberedAsync(user, cancellationToken))
        {
            // Store the userId for use after two factor check
            var userId = await UserManager.GetUserIdAsync(user, cancellationToken);
            Context.Response.SignIn(StoreTwoFactorInfo(userId, loginProvider));
            return SignInResult.TwoFactorRequired;
        }
    }
    // Cleanup external cookie
    if (loginProvider != null)
    {
        Context.Response.SignOut(IdentityOptions.ExternalCookieAuthenticationType);
    }
    await SignInAsync(user, isPersistent, loginProvider, cancellationToken);
    return SignInResult.Success;
}

再次拋開一大堆的 UserManager 操作,找到最核心的:Context.Response.SignIn(StoreTwoFactorInfo(userId, loginProvider)),StoreTwoFactorInfo 方法返回類型爲 ClaimsIdentity,在返回之前,根據 userId 創建 Claim 對象,並添加到 ClaimsIdentity 集合中,接下來的操作就是:Context.Response.SignIn,將用戶身份信息輸入到當前上下文,接着查看 HttpResponse 抽象類關於 SignIn 的定義:

public virtual void SignIn(IEnumerable<ClaimsIdentity> identities);
public virtual void SignIn(ClaimsIdentity identity);
public abstract void SignIn(AuthenticationProperties properties, IEnumerable<ClaimsIdentity> identities);
public virtual void SignIn(AuthenticationProperties properties, params ClaimsIdentity[] identities);
public virtual void SignIn(AuthenticationProperties properties, ClaimsIdentity identity);

在以前如果使用 SignIn,其調用方式是 System.Web.Security.FormsAuthentication.SetAuthCookie("userName", false);,採用的是 Forms 認證,但是在 ASP.NET 5 中,已經訪問不到 SetAuthCookie 了,原來的 SetAuthCookie 實現方式不知道是怎樣的,如果在 ASP.NET 5 中實現 SetAuthCookie 類似的效果,我們該怎麼做呢?只需要在 Startup.cs 的 Configure 方法中進行下面配置:

//app.UseIdentity();
app.UseCookieAuthentication((cookieOptions) =>
{
    cookieOptions.AuthenticationType = IdentityOptions.ApplicationCookieAuthenticationType;
    cookieOptions.AuthenticationMode = AuthenticationMode.Active;
    cookieOptions.CookieHttpOnly = true;
    cookieOptions.CookieName = ".CookieName";
    cookieOptions.LoginPath = new PathString("/Account/Login");
    //cookieOptions.CookieDomain = ".mysite.com";
}, "AccountAuthorize");

其實我們下面進行自定義的配置和上面註釋的 UseIdentity 是一樣的效果,只不過有些操作是在 Microsoft.AspNet.Identity.IdentityServiceCollectionExtensions 中默認完成的,注意上面配置中,我們將之前的 services.AddDefaultIdentity<ApplicationDbContext, ApplicationUser, IdentityRole>(Configuration); 代碼給註釋了,再來看下 Account 中的 Login 代碼:

[AllowAnonymous]
public void Login(string returnUrl = null)
{
    var userId = "xishuai";
    var identity = new ClaimsIdentity(IdentityOptions.ApplicationCookieAuthenticationType);
    identity.AddClaim(new Claim(ClaimTypes.Name, userId));
    Response.SignIn(identity);
}

上面的操作其實就是之前的 SignInManager.PasswordSignInAsync 一樣,只不過是一個簡化版本,另外,IdentityOptions.ApplicationCookieAuthenticationType 也沒什麼神奇的地方,就是一個類型字符串:

public static string ApplicationCookieAuthenticationType { get; set; } = typeof(IdentityOptions).Namespace + ".Application";
public static string ExternalCookieAuthenticationType { get; set; } = typeof(IdentityOptions).Namespace + ".External";
public static string TwoFactorUserIdCookieAuthenticationType { get; set; } = typeof(IdentityOptions).Namespace + ".TwoFactorUserId";
public static string TwoFactorRememberMeCookieAuthenticationType { get; set; } = typeof(IdentityOptions).Namespace + ".TwoFactorRemeberMe";

Login 登錄驗證效果:

112251532653794.png

總結:上面也說了不少內容,說真的,其實我也不知道自己說了什麼,有幾點感觸需要總結下,在多個應用程序共享身份驗證的時候(CookieDomain),不管是使用 FormsAuthentication,還是使用 SignInManager.SignInAsync,又或者使用 UserStore 進行用戶管理,但用戶進行驗證的程序只有一個,這個按照自己的想法,想怎麼實現就怎麼實現,其他的應用程序都只不過是判斷用戶是否通過驗證請求、及獲取用戶標識的,就這兩個操作,用戶的驗證不管上面的何種實現,我們都可以通過 User.Identity 獲取用戶驗證的信息,類型爲 IIdentity

不知者無罪,知罪卻不贖罪,那就是有罪!!!


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章