ms隨vs2013推出了mvc5,mvc5自帶的模板項目中引用了新的身份認證框架 ms identity。其中owin部分實現了google,facebook,twitter等國外常見的第三方用戶。可惜國內沒人用這些。
只能照貓畫虎實現以下qq和sina的了
首先看ms自帶的google和facebook的代碼
每個類庫里,有6個類一個接口,其中擴展類放在Owin命名空間下,是為了在startup中方便調用
看看startup中被注釋掉的代碼就知道了,都是UsexxxAuthentication。同理,希望可以有
app.UseQQAuthentication和app.UseSinaAuthentication
通過查看擴展方法,可以發現,最終調用了app.Use
對應的FacebookAuthenticationMiddleware和GoogleAuthenticationMiddleware才是owin相關的關鍵部分
XXXMiddleware是一個實現了OwinMiddleware的類(Middleware譯為中間件),繼承他,然后Use到app里,就可以在Invoke時去處理請求
不過我們不需要繼承OwinMiddleware這么底層的類,因為有與身份認證相關的中間件基類了
public abstract class AuthenticationMiddleware<TOptions> : OwinMiddleware where TOptions : AuthenticationOptions
看此類的簽名就知道,我們還需要一個Options類,應該去繼承AuthenticationOptions類
看看微軟給的。在options類里,一般都會有一些屬性,去讓使用者設定appid和appkey等信息,對oauth驗證來說,基本就這兩個是最重要的。
稍微需要注意一下的是
base(“Google”)中的google是寫死傳進去的,這個對應的是
而Caption對應的是
如果希望按鈕的文字也由使用者提供,可以實現帶參的構造
來看看最核心的中間件吧。
已google的為例,發現他並沒有實現Invoke方法,那就應該是在基類里已經實現了。
反而是實現了一個CreateHandler方法
new了一個GoogleAuthenticationHandler返回
再看看facebook的
也一樣。在對象瀏覽器里沒發現這個Handler類啊。好在有各種反編譯。
這個是個internal的類(最恨internal和sealed)
看看中間件類里,比較簡單
1、構造函數就是給options賦一些默認值
2、創建Handler
3、私有方法,在構造中調用
沒了
那核心明顯從中間件轉到了Handler
再看Handler之前,先看看其他的那幾個類
XXXReturnEndpointContext實現ReturnEndpointContext,除了構造沒別的
太簡單了,咱也整個QQReturnEndpointContext和SinaReturnEndpointContext放着
再看IGoogleAuthenticationProvider和IFacebookAuthenticationProvider,都一樣的
Task Authenticated(FacebookAuthenticatedContext context);
Task ReturnEndpoint(FacebookReturnEndpointContext context);
除了參數的類型不一樣外,其他的都一樣的。我們也寫一個放着。
再看接口的實現,也一樣的,代碼不貼了。寫兩個放着。
到目前為止,我們有以下幾個類
1、擴展方法來
2、Options類
3、中間件類
4、Handler類
5、ReturnEndpointContext類
6、Provider接口
7、Provider實現
還剩下一個XXXAuthenticatedContext類
看google和facebook的實現可以發現,只有屬性,沒有方法,屬性就是記錄以下用戶id,用戶名字acesskey等信息
我們可以先把類建好,具體屬性需要什么根據對應的api再加。
OK,我們回來看核心的Handler類。
可以發現,他們都實現了基類的3個方法
1、
protected override Task ApplyResponseChallengeAsync()2、
public override async Task<bool> InvokeAsync()
3、
protected override async Task<AuthenticationTicket> AuthenticateCoreAsync()先說第一個,他的作用就是方法第三方api,看看是否授權了
對應的qq地址是:
https://graph.qq.com/oauth2.0/authorize?client_id={0}&response_type=code&redirect_uri={1}
對應的sina地址是:
https://api.weibo.com/oauth2/authorize?client_id={0}&redirect_uri={1}&response_type=code&state={2}
在方法的最后,通過
this.Response.StatusCode = 302; this.Response.Headers.Set("Location", url);
來設置瀏覽器跳轉
在連接中,我們需要有一個state參數,這個參數由app生成,傳給oauth,oauth在傳回來,對比驗證,state生成后,會自動保存到cookie里,這是基類幫我們做好的。
AuthenticationProperties properties = responseChallenge.Properties;
this.GenerateCorrelationId(properties);
var protector=this.Options.StateDataFormat.Protect(properties);
只需要3行代碼,就可以生成一個state驗證串,把這個字符串附加在連接后面傳給oauth即可
oauth的回調地址,是定義在Options里的CallbackPath
在對應的Options類里可以看到地址為/signin-google和signin-facebook,我們自己的qq和sina自然可以定義成signin-qq和signin-sina
找遍項目,也沒有發現signin-google這個controller或者頁面什么的,那為什么這個地址可以訪問呢,訪問他又會干什么呢
來看第二個方法InvokeAsync()
已google為例
public override async Task<bool> InvokeAsync() { bool flag; if (this.Options.CallbackPath.HasValue && this.Options.CallbackPath == this.Request.Path) flag = await this.InvokeReturnPathAsync(); else flag = false; return flag; }
在這里判斷了,如果當前訪問的路徑是CallbackPath的路徑,則去執行一些東西,下面的InvokeReturnPathAsync是一個protected方法
真正的邏輯在這個方法里
public async Task<bool> InvokeReturnPathAsync() { AuthenticationTicket model = await this.AuthenticateAsync(); bool flag; if (model == null) { LoggerExtensions.WriteWarning(this._logger, "Invalid return state, unable to redirect.", new string[0]); this.Response.StatusCode = 500; flag = true; } else { GoogleReturnEndpointContext context = new GoogleReturnEndpointContext(this.Context, model); context.SignInAsAuthenticationType = this.Options.SignInAsAuthenticationType; context.RedirectUri = model.Properties.RedirectUri; model.Properties.RedirectUri = (string) null; await this.Options.Provider.ReturnEndpoint(context); if (context.SignInAsAuthenticationType != null && context.Identity != null) { ClaimsIdentity claimsIdentity = context.Identity; if (!string.Equals(claimsIdentity.AuthenticationType, context.SignInAsAuthenticationType, StringComparison.Ordinal)) claimsIdentity = new ClaimsIdentity(claimsIdentity.Claims, context.SignInAsAuthenticationType, claimsIdentity.NameClaimType, claimsIdentity.RoleClaimType); this.Context.Authentication.SignIn(context.Properties, new ClaimsIdentity[1] { claimsIdentity }); } if (!context.IsRequestCompleted && context.RedirectUri != null) { if (context.Identity == null) context.RedirectUri = WebUtilities.AddQueryString(context.RedirectUri, "error", "access_denied"); this.Response.Redirect(context.RedirectUri); context.RequestCompleted(); } flag = context.IsRequestCompleted; } return flag; }
(google和facebook的,除了類名不一樣,其他的都一樣,copy一下就可以了。)
所以,當回調到signin-google這個地址的時候,是可以正常訪問的。
最后來看AuthenticateCoreAsync
首先是檢查回調地址的參數,如果沒有state,就返回null了。
然后是this.ValidateCorrelationId(properties, this._logger)通過這個方法,來與cookie里保存的state對比,如果不對,也返回null了
剩下的就是回調正確,改進行下一步了。
最終的目的是要返回一個AuthenticationTicketpublic AuthenticationTicket(ClaimsIdentity identity, AuthenticationProperties properties)需要2個參數,其中properties我們已經有了(在前面就可以構造出來,具體可看源碼)
這里主要是需要構造一個ClaimsIdentity
ClaimsIdentity identity = new ClaimsIdentity(this.Options.AuthenticationType);
理論上new一個就ok,並且,如果僅僅是這么寫,他也能過去的,會進入下一步,但是到下一步的時候,有時候會包nullreference異常。
這里應該傳入當前用戶id和名字
identity.AddClaim(new Claim(http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier,
“id”, "http://www.w3.org/2001/XMLSchema#string", this.Options.AuthenticationType));
identity.AddClaim(new Claim(http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name,
“名字”, "http://www.w3.org/2001/XMLSchema#string", this.Options.AuthenticationType));
至少需要id,第一次用第三方登錄進來時,他會讓你在本地注冊
此時AspNetUserLogins表中會加入一條數據
聯合主鍵,UserId對應AspNetUsers表中的主鍵
LoginProvider是你所使用的第三方登錄的標識就是Options調用base(“xxxx”)這里傳進去的那個值
而最后一個ProviderKey,則是你這個第三方登錄返回的用戶id,比如你用sina的登陸,這里記錄的應該是你sina的那個id
qq的,就是qq記錄你的那個id(qq的叫open id)
具體這幾個表是如何存儲,是和MS Identity有關的。
這個id和名字這么獲得?
當然是調用oauth的api了。
目前,我們只是調用了授權服務,回調的參數里有authorization code,我們需要拿這個authorization code 去請求access token,再拿access token 去請求當前的人的昵稱和id等其他信息
簡單實現了一下
https://github.com/czcz1024/OwinQQ
包括qq和sina的,因為不想引入其他的類庫,所以id,名字還包括access token等都是拿正則截取的,如果你覺得不爽,可以自己做json轉換