1. 程式人生 > >譯:Asp.Net Identity與Owin,到底誰是誰?

譯:Asp.Net Identity與Owin,到底誰是誰?

送給正在學習Asp.Net Identity的你 :-)

Recently I have found an excellent question on Stackoverflow. The OP asks why does claim added to Idenetity after calling AuthenticationManager.SignIn still persist to the cookie.

最近我在StackOverflow發現一個非常好的(妙啊)問題,OP提出的問題是,在呼叫AuthenticationManager.SignIn之後再向Identity新增Claim ,這些後新增的Claim仍然被儲存在了Cookies裡。

The sample code was like this:

程式碼是這樣的

ClaimsIdentity identity = UserManager.CreateIdentity(user, DefaultAuthenticationTypes.ApplicationCookie );

var claim1 = new Claim(ClaimTypes.Country, "Arctica");
identity.AddClaim(claim1);

AuthenticationManager.SignIn(new AuthenticationProperties { IsPersistent = true
}, identity ); var claim2 = new Claim(ClaimTypes.Country, "Antartica"); identity.AddClaim(claim2);

Yeah, why does claim2 is available after cookie is already set.

噢耶,為什麼在已經設定cookie之後claim2仍然是有效的呢?

After much digging I have discovered that AspNet Identity framework does not set the cookie. OWIN does. And OWIN is part of Katana Project which has open source code. Having the source code available is always nice – you can find out yourself why things work or don’t work the way you expect.

在我深度挖掘之後,我發現 AspNet Identity framework 並沒有真的去設定cookie,是Owin做的。Owin是開源專案Katana的一部分(譯者注:katana微軟對於Owin的實現,Owin的實現還有其它的)。能看原始碼總是一件很棒的事,因為你能自己去尋找為什麼事情是這樣的或者為什麼結果和預期的不一致的答案。

In this case I have spent a few minutes navigating Katana project and how AuthenticationManager works. Turned out that SingIn method does not set a cookie. It saves Identity objects into memory until time comes to set response cookies. And then claims are converted to a cookie and everything magically works :-)

在這件事裡,我花了幾分鐘時間找到Katana的原始碼尋找AuthenticationManager的工作原理。原來SignIng方法並沒有設定Cookie。直到設定相應cookie之前,它只是將Identity物件放在了記憶體裡。然後 claims被轉換成cookie,並且,神奇的事情發生了:-D(應該是這麼翻譯吧...)

譯者注:下面是SignIn的原始碼,原始碼地址

public void SignIn(AuthenticationProperties properties, params ClaimsIdentity[] identities)
{
    AuthenticationResponseRevoke priorRevoke = AuthenticationResponseRevoke;
    if (priorRevoke != null)
    {
        // Scan the sign-outs's and remove any with a matching auth type.
        string[] filteredSignOuts = priorRevoke.AuthenticationTypes
            .Where(authType => !identities.Any(identity => identity.AuthenticationType.Equals(authType, StringComparison.Ordinal)))
            .ToArray();
        if (filteredSignOuts.Length < priorRevoke.AuthenticationTypes.Length)
        {
            if (filteredSignOuts.Length == 0)
            {
                AuthenticationResponseRevoke = null;
            }
            else
            {
                AuthenticationResponseRevoke = new AuthenticationResponseRevoke(filteredSignOuts);
            }
        }
    }

    AuthenticationResponseGrant priorGrant = AuthenticationResponseGrant;
    if (priorGrant == null)
    {
        AuthenticationResponseGrant = new AuthenticationResponseGrant(new ClaimsPrincipal(identities), properties);
    }
    else
    {
        ClaimsIdentity[] mergedIdentities = priorGrant.Principal.Identities.Concat(identities).ToArray();

        if (properties != null && !object.ReferenceEquals(properties.Dictionary, priorGrant.Properties.Dictionary))
        {
            // Update prior properties
            foreach (var propertiesPair in properties.Dictionary)
            {
                priorGrant.Properties.Dictionary[propertiesPair.Key] = propertiesPair.Value;
            }
        }

        AuthenticationResponseGrant = new AuthenticationResponseGrant(new ClaimsPrincipal(mergedIdentities), priorGrant.Properties);
    }
}

This sparked another question. At the moment Identity does not have open source, but what is the role of OWIN in Identity and how Claims work here?

這引發了另一個問題,此時 Identity還沒有開源,但是Identity中OWIN的Role是什麼,並且,Claims是如何工作(譯者注:發揮作用)的?

Turns out that Identity framework deals only with user persistence, password hashing, validating if the password is correct, sending out email tokens for password reset, etc. But Identity does not actually authenticate users or create cookies. Cookies are handled by OWIN.

原來Identity framework做的事只有使用者持久化儲存、密碼Hash、驗證密碼是否是正確的、為重置密碼傳送帶有Token的電子郵件等等。但是identity 沒有真正的認證使用者或者建立Cookie。Owin在處理Cookie。

Take a look on this code for signing in:

我們來看一下SignIng的程式碼:

public async Task SignInAsync(Microsoft.Owin.Security.IAuthenticationManager authenticationManager, ApplicationUser applicationUser, bool isPersistent)
{
    authenticationManager.SignOut(DefaultAuthenticationTypes.ApplicationCookie);

    ClaimsIdentity identity = await UserManager.CreateIdentityAsync(applicationUser, DefaultAuthenticationTypes.ApplicationCookie);

    authenticationManager.SignIn(new Microsoft.Owin.Security.AuthenticationProperties() { IsPersistent = isPersistent }, identity);
}

Identity only creates ClaimsIdentity which you can study on ReferenceSource site. And ClaimsIdentity is part of .Net framework, not some nuget-package from the interwebz. Then this claimsIdentity is passed to OWIN AuthenticationManager where a callback is set to assign cookies on when time comes to write headers on the response.

Identity只是建立了ClaimsIdentity,你可以在ReferenceSource (譯者注:檢視.Net原始碼的地方,微軟官辦,沒儲存的同學快快收藏吧!)上學習他的程式碼。而且ClaimsIdentity是.Net Freamwork的一部分,不是從網際網路上下載的Nuget包(實在不懂interwebz是什麼意思 Orz)。然後claimsIdentity被傳給了OWIN AuthenticationManager,(斷句)OWIN AuthenticationManager是設定在派送Cookie(assign cookies)上的一個Callcack回撥,(斷句)這個assign cookies是放在一個什麼東西上,當到了向Response寫Header的時候就搞這個東西(這句話太難翻譯了,你一定要看看原話,我不保證這句話翻譯正確 (:逃)

So far, so good, we have 3 parts here: Identity framework creating a ClaimsIdentity, OWIN creating a cookie from this ClaimsIdentity. And .Net framework which holds the class for ClaimsIdentity.

目前看來還不錯。我們知道了這裡有3個部分:

  • Identity framework 建立了一個ClaimsIdentity
  • Owin為ClaimsIdentity建立了cookie
  • 持有 ClaimsIdentity的class的是.Net framework

When in your classes you access ClaimsPrincipal.Current, you only use .Net framework, no other libraries are used. And this is very handy!

當你在你的程式碼中訪問ClaimsPrincipal.Current時,你只用的了.Net framework,沒有其它庫(library)被用到,這是非常方便的。

Default Claims

Identity framework does a nice thing for you. By default it adds a number of claims to a principal when user is logged in. Here is the list:

Identity framework幫你做了很多有用的事。預設情況下,在使用者登入的時候,她會新增一些claims到principal(譯者注:看一下Mvc中的HttpContext.User的型別,就是IPrincipal)。接下來是列表:

  • User.Id的claims型別是“http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier” 或者通過ClaimTypes.NameIdentifier指定(譯者注:ClaimTypes.NameIdentifier的定義如下public const string NameIdentifier = "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier";

  • “ASP.NET Identity” is saved as claim type “http://schemas.microsoft.com/accesscontrolservice/2010/07/claims/identityprovider”. This is useful if you are using OpenId to do the authentication. Not much use if you are using only users stored in our database. See this page for more details.

  • Guid containing User’s Security Stamp is persisted in claim with type “AspNet.Identity.SecurityStamp“. Security Stamp is basically a snapshot of user state. If password/method of authentication, email, etc. is changed, Security Stamp is changed. This allows to “logout everywhere” on credentials change. Read more about what is security stamp in Hao Kung’s answer.

  • 包含使用者Security Stamp的Guid被存在type為“AspNet.Identity.SecurityStamp”的claim裡。 Security Stamp 總的來說是使用者狀態(user state)快照(snapshot),如果用於認證的 密碼/method (譯者注:用於認證的其他方式)、email等等這些資訊改變了。Security Stamp也會變。這就允許當credentials(憑證)改變時,在所有已登入的地方登出。瞭解更多請關於Security Stamp 請看Hao Kung(看起來像箇中國人)在SO上的答案

  • The most useful claims added by default are role. All roles assigned to the user are saved as ClaimTypes.Role or by “http://schemas.microsoft.com/ws/2008/06/identity/claims/role“. So next time you need to check current user’s roles, check the claims. This does not hit the database and is very quick. And in fact, if you call .IsInRole("RoleName") on ClaimsPrincipal, the framework goes into claims and checks if Role claims with this value is assigned.

  • 預設新增的最有用的claims就是role了,所有賦予這個使用者的的role都被儲存為type為 ClaimTypes.Role 或者 “http://schemas.microsoft.com/ws/2008/06/identity/claims/role”。所以以後你想檢查當前使用者的Role你就可以直接檢查這些claims。(譯者注:在登陸時儲存在Identity資料庫中的所有關於user的role,以及claim會被轉換成claim。IsInRole方法也是在判斷claim,詳情可查閱原始碼)。這些操作都不訪問資料庫所以非常快。而且事實上,如果你使用.IsInRole("RoleName") on ClaimsPrincipal,框架會去檢查claim中的role claim是否含有指定的值。

You can find the list of framework claim types on .Net Reference site. However, this list is not complete. You can make up your own claim types as you are pleased – this is just a string.

你能夠在.Net Reference網站上找到claim type的列表。然而,這個列表並不完整。如果你喜歡,你完全可以製造你自己的claim type,因為它只是一個字串而已。

If you want to add your own claim types, I recommend to use your own notation for the claim types. Something like “MyAppplication:GroupId” and keep all the claim types in one class as constants:

如果你想新增你自己的claim type ,我建議你在claim type上加上你自己獨特的標記。比如:“MyAppplication:GroupId”,然後將它們作為 constants存在某個類裡面。

public class MyApplicationClaimTypes
{
    public string const GroupId = "MyAppplication:GroupId";
    public string const PersonId = "MyAppplication:PersonId";
    // other claim types
} 

This way you can always find where the claims are used and will not clash with the framework claim types. Unless the claim you use matches framework claim types exactly, like ClaimTypes.Email.

這種方式讓你能夠找到哪裡引用了這些claims,並且不會和framework中的claim type發生衝突。除非你使用的claim type的framework中的claim type精確匹配,例如:ClaimTypes.Email

Adding default claims 新增預設的claims

I always add user’s email to the list of claims. I do that on user sign-in, same way the first code snippet adds claim1 and claim2:

我總是使用和第一個新增claim1和claim2相同的方式在使用者登入時將使用者的email新增到claims列表裡。

public async Task SignInAsync(IAuthenticationManager authenticationManager, ApplicationUser applicationUser, bool isPersistent)
{
    authenticationManager.SignOut(
        DefaultAuthenticationTypes.ExternalCookie,
        DefaultAuthenticationTypes.ApplicationCookie);

    var identity = await this.CreateIdentityAsync(applicationUser, DefaultAuthenticationTypes.ApplicationCookie);

    // using default claim type from the framework
    identity.AddClaim(new Claim(ClaimTypes.Email, applicationUser.Email));

    authenticationManager.SignIn(new AuthenticationProperties() { IsPersistent = isPersistent }, identity);
}

You can add your default claims to all users here as well. But there is IClaimsIdentityFactory class that is assigned in UserManager. There is only one method there:

你可可以向所有使用者新增預設的claims。但是在UserManager中有一個IClaimsIdentityFactory的類,它只定義了一個方法。

public interface IClaimsIdentityFactory<TUser, TKey> where TUser : class, IUser<TKey> where TKey : IEquatable<TKey>
{
    /// <summary>
    /// Create a ClaimsIdentity from an user using a UserManager
    /// </summary>
    Task<ClaimsIdentity> CreateAsync(UserManager<TUser, TKey> manager, TUser user, string authenticationType);
}

Default AspNet Identity implementation creates ClaimsIdentity, adds the default claims described above, adds claims stored in the database for the user: IdentityUserClaims. You can override this implementation and slip-in your own logic/claims:

預設的Asp.Net Identity實現建立ClaimsIdentity,新增之前提到的預設claims,新增儲存在satabase中的屬於這個使用者的claims:IdentityUserClaims。你可以重寫這個實現類,然後放進你自己的 logic/claims.
public class MyClaimsIdentityFactory : ClaimsIdentityFactory

        claimsIdentity.AddClaim(new Claim("MyApplication:GroupId", "42"));

        return claimsIdentity;
    }
}

and assign it in UserManager:

將它賦值給UserManager:

public UserManager(MyDbContext dbContext)
    : base(new UserStore<ApplicationUser>(dbContext))
{
    // other configurations

    // Alternatively you can have DI container to provide this class for better application flexebility
    this.ClaimsIdentityFactory = new MyClaimsIdentityFactory();
}

正文結束

補充一些譯者的話

如果你注意到文中提到登陸的程式碼http://blog.sina.com.cn/u/6272335754
http://blog.sina.com.cn/s/blog_175dc3f8a0102xgr6.html
http://blog.sina.com.cn/s/blog_175dc3f8a0102xgr5.html
http://blog.sina.com.cn/s/blog_175dc3f8a0102xgpo.html
http://blog.sina.com.cn/s/blog_175dc3f8a0102xgpp.html
http://blog.sina.com.cn/s/blog_175dc3f8a0102xgpr.html
http://blog.sina.com.cn/s/blog_175dc3f8a0102xgqa.html
http://blog.sina.com.cn/s/blog_175dc3f8a0102xgqb.html
http://blog.sina.com.cn/s/blog_175dc3f8a0102xgqc.html
http://blog.sina.com.cn/s/blog_175dc3f8a0102xgr2.html
http://blog.sina.com.cn/s/blog_175dc3f8a0102xgr4.htmlauthenticationManager.SignIn(new AuthenticationProperties() { IsPersistent = isPersistent }, identity);
在新的Mvc專案中勾選個人身份認證時所生成的基本程式碼已經變成了var result = await SignInManager.PasswordSignInAsync(model.Email, model.Password, model.RememberMe, shouldLockout: false); ,我試過使用文中的方法,是可以正常登陸的。

因為最終SignInManager的PasswordSignInAsync方法幾經輾轉也呼叫了IAuthenticationManager例項的Sign方法完成登陸。