Microsoft.Owin.Security 自定義AuthenticationHandler 實現oauth2 的受權碼模式

三個網站:web

1. www.cnblogs.com        用戶直接面對和使用的web應用   之後簡稱w數據庫

2.passport.cnblogs.com   認證服務器,能夠訪問用戶表的用戶名和密碼                             之後簡稱p服務器

3.users.cnblogs.com        給其它應用程序提供服務,本例當中只是提供受權用戶的我的信息,例如手機 郵箱等信息                            之後簡稱ucookie

 

利用 Microsoft.Owin.Security 來實現oauth2 的受權碼模式app

 

主要流程:async

1. w判斷本身網站的cookie有沒有,若是沒有,跳轉到本身的登陸頁ide

2.w登陸頁 action 記錄下須要跳轉的url地址,切換到 第三方登陸 模式,返回401函數

3. w的第三方登陸發現本身是當前的登陸模式,並且狀態碼仍是401,接管response,再也不返回401,而是重定向至p站的Authorize學習

4.p站的Authorize查看當前用戶是否已經登陸,若是登陸,返回受權頁(大體內容是:你是否贊成 第三方網站w使用你的我的信息) 注:這個地方也能夠直接返回受權碼,即再也不須要用戶確認一次,參見下方代碼網站

5.用戶贊成後,p站生成受權碼,並將請求過來的state參數附加在返回的url後,跳轉回w網站

6.w網站拿到code和state後,首先檢查state是否和當初請求時同樣,而後在服務器構造一個新的請求,到p站拿回真實的access_token和refresh_token等信息

7.w網站用access_token到u站獲取到用戶的信息,而後構造ClaimsIdentity,在w網站登陸,登陸成功之後取出第2步存入的訪問url,跳轉

 

 

一些關鍵代碼

w網站啓動

//
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
AuthenticationType=DefaultAuthenticationTypes.ApplicationCookie,
LoginPath=new PathString("/Account/Login")
});

//這段代碼的做用是,若是第三方登陸中沒有配置SignInAsAuthenticationType
//則須要開啓之後,在/Account/OAuthLoginCallBack 中獲取到ExternalCookie 而後再實現當前網站的登陸
//在OAuthLoginCallBack中再說起這部分代碼
//app.UseExternalSignInCookie(DefaultAuthenticationTypes.ExternalCookie);

//使用第三方登陸的配置
//此處的oauth是參照修改Microsoft.Owin.Security.Google 而來
//不復雜,但有幾處暗坑
//關於callbackpath ,因爲oauth2.0 的規範要求 請求code和請求access_token的url中的redirect_uri 必須一致,因此增長了一個第三方登陸完畢之後跳回來的地址
app.UseAxOAuth2Authentication(new AxOAuth2AuthenticationOptions()
{
TokenEndpoint="http://localhost:8099/token",
AuthorizeEndpoint="http://localhost:8099/oauth/authorize",
UserInfoEndpoint="http://localhost:8053/User/UserIdInfo",
ClientId="clientId",
ClientSecret="ClientSecret",
CallbackPath=new PathString("/Account/OAuthLoginCallBack"),
SignInAsAuthenticationType=DefaultAuthenticationTypes.ApplicationCookie
});

 

w網站  /Account/Login 完成 流程中第一步 第二步的功能

       [AllowAnonymous]
        public ActionResult Login(string returnUrl)
        {
            var properties = new AuthenticationProperties
            {
                RedirectUri=returnUrl
            };

//AxOauth是個人第三方登陸模塊的名稱
//

HttpContext.GetOwinContext().Authentication.Challenge(properties, "AxOauth");
return new HttpUnauthorizedResult();
 }

 

實現流程第三步 AxOAuth2AuthenticationHandler中的第一個主要函數 ,參考 Microsoft.Owin.Security.Google修改

protected override Task ApplyResponseChallengeAsync()
        {
//401判斷
if (Response.StatusCode != 401) { return Task.FromResult<object>(null); }
//若是在login中不 Challenge 此處就會爲null
var challenge = Helper.LookupChallenge(Options.AuthenticationType, Options.AuthenticationMode); if (challenge != null) { var baseUri = Request.Scheme + Uri.SchemeDelimiter + Request.Host + Request.PathBase; var currentUri = baseUri + Request.Path + Request.QueryString; var redirectUri = baseUri + Options.CallbackPath; var properties = challenge.Properties; if (string.IsNullOrEmpty(properties.RedirectUri)) { properties.RedirectUri = currentUri; } // OAuth2 10.12 CSRF GenerateCorrelationId(properties); var queryStrings = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase) { {"response_type", "code"}, {"client_id", Options.ClientId}, {"redirect_uri", redirectUri} }; // space separated var scope = string.Join(" ", Options.Scope); if (string.IsNullOrEmpty(scope)) { // Google OAuth 2.0 asks for non-empty scope. If user didn't set it, set default scope to // "openid profile email" to get basic user information. scope = "openid profile email"; } AddQueryString(queryStrings, properties, "scope", scope); AddQueryString(queryStrings, properties, "access_type", Options.AccessType); AddQueryString(queryStrings, properties, "approval_prompt"); AddQueryString(queryStrings, properties, "login_hint"); var state = Options.StateDataFormat.Protect(properties); queryStrings.Add("state", state); var authorizationEndpoint = WebUtilities.AddQueryString(Options.AuthorizeEndpoint, queryStrings); var redirectContext = new AxOAuth2ApplyRedirectContext( Context, Options, properties, authorizationEndpoint); Options.Provider.ApplyRedirect(redirectContext); } return Task.FromResult<object>(null); }

 

p站啓動代碼Startup.Auth.cs   Paths本身定義,相關的provider能夠閱讀其它相關資料

 app.UseCookieAuthentication(new CookieAuthenticationOptions
            {
                AuthenticationType=Paths.AuthenticationType,
                LoginPath=new PathString(Paths.LoginPath),
                LogoutPath=new PathString(Paths.LogoutPath),
            });

            app.UseOAuthBearerTokens(new OAuthAuthorizationServerOptions
            {
                AuthorizeEndpointPath=new PathString(Paths.AuthorizePath),
                TokenEndpointPath=new PathString(Paths.TokenPath),
                AccessTokenExpireTimeSpan=TimeSpan.FromHours(2),
                Provider=new AuthorizationServerProvider(),
                AuthorizationCodeProvider=new AuthorizationCodeProvider(),
                RefreshTokenProvider=new RefreshTokenProvider(),
                ApplicationCanDisplayErrors=true,
               
#if DEBUG
                //HTTPS is allowed only AllowInsecureHttp = false
                AllowInsecureHttp=true,
#endif
            });

p站的 Authorize

 public ActionResult Authorize()
        {
            //是否要求必須用戶顯示受權 彈出確認受權的頁面
            var userGrant = ConfigurationManager.AppSettings["oAuthUserGrant"]=="1";

            IAuthenticationManager authentication = HttpContext.GetOwinContext().Authentication;
            AuthenticateResult ticket = authentication.AuthenticateAsync(Paths.AuthenticationType).Result;
            ClaimsIdentity identity = ticket?.Identity;

            if (identity==null)
            {
                //若是沒有驗證經過,則必須先經過身份驗證,跳轉到驗證方法
                authentication.Challenge(Paths.AuthenticationType);
                return new HttpUnauthorizedResult();
            }

            if (Request.HttpMethod!="POST"&&userGrant)
            {
                return View();
            }

            //返回給其它系統的用戶身份只是在當前系統內的身份基礎上增長了role
            identity=new ClaimsIdentity(identity.Claims, "Bearer");

            //hardcode添加一些Claim,正常是從數據庫中根據用戶ID來查找添加
            identity.AddClaim(new Claim(ClaimTypes.Role, "GetUserInfo"));
         
            authentication.SignIn(new AuthenticationProperties() { IsPersistent=true }, identity);
            return View();
        }

 

w站的 OAuthLoginCallBack,此處很簡單,AxOAuth2AuthenticationHandler 已經把其它工做作完了

 [AllowAnonymous]
        public ActionResult OAuthLoginCallBack(string returnUrl)
        {
            return Redirect(returnUrl??"/");
        }

 

 

AxOAuth2AuthenticationHandler 中的另兩個主要函數,其它內容請參照Microsoft.Owin.Security.Google

 private async Task<bool> InvokeReplyPathAsync()
        {
            if (Options.CallbackPath.HasValue && Options.CallbackPath == Request.Path)
            {
                // TODO: error responses
                //會調用下方的函數
                var ticket = await AuthenticateAsync();
                if (ticket == null)
                {
                    _logger.WriteWarning("Invalid return state, unable to redirect.");
                    Response.StatusCode = 500;
                    return true;
                }

                var context = new AxOAuth2ReturnEndpointContext(Context, ticket);
                context.SignInAsAuthenticationType = Options.SignInAsAuthenticationType;
                context.RedirectUri = ticket.Properties.RedirectUri;

                await Options.Provider.ReturnEndpoint(context);

//已經設置了type,則會登陸,就再也不須要callback中去實現登陸了
if (context.SignInAsAuthenticationType != null && context.Identity != null) { var grantIdentity = context.Identity; if ( !string.Equals(grantIdentity.AuthenticationType, context.SignInAsAuthenticationType, StringComparison.Ordinal)) { grantIdentity = new ClaimsIdentity(grantIdentity.Claims, context.SignInAsAuthenticationType, grantIdentity.NameClaimType, grantIdentity.RoleClaimType); } Context.Authentication.SignIn(context.Properties, grantIdentity); }
//下方的RedirectUri 是在當處那個Login中記錄的
if (!context.IsRequestCompleted && context.RedirectUri != null) { var redirectUri = context.RedirectUri; if (context.Identity == null) { // add a redirect hint that sign-in failed in some way redirectUri = WebUtilities.AddQueryString(redirectUri, "error", "access_denied"); } Response.Redirect(redirectUri); context.RequestCompleted(); } return context.IsRequestCompleted; } return false; }

 

        protected override async Task<AuthenticationTicket> AuthenticateCoreAsync()
        {
            AuthenticationProperties properties = null;

            try
            {
                string code = null;
                string state = null;

                var query = Request.Query;
                var values = query.GetValues("code");
                if (values != null && values.Count == 1)
                {
                    code = values[0];
                }
                values = query.GetValues("state");
                if (values != null && values.Count == 1)
                {
                    state = values[0];
                }

                properties = Options.StateDataFormat.Unprotect(state);
                if (properties == null)
                {
                    return null;
                }

                // OAuth2 10.12 CSRF
                if (!ValidateCorrelationId(properties, _logger))
                {
                    return new AuthenticationTicket(null, properties);
                }

                var requestPrefix = Request.Scheme + "://" + Request.Host;
                var redirectUri = requestPrefix + Request.PathBase + Options.CallbackPath;

                // Build up the body for the token request
                var body = new List<KeyValuePair<string, string>>();
                body.Add(new KeyValuePair<string, string>("grant_type", "authorization_code"));
                body.Add(new KeyValuePair<string, string>("code", code));
                body.Add(new KeyValuePair<string, string>("redirect_uri", redirectUri));
                body.Add(new KeyValuePair<string, string>("client_id", Options.ClientId));
                body.Add(new KeyValuePair<string, string>("client_secret", Options.ClientSecret));

                // Request the token from p 
                var tokenResponse =
                    await _httpClient.PostAsync(Options.TokenEndpoint, new FormUrlEncodedContent(body));
                tokenResponse.EnsureSuccessStatusCode();
                var text = await tokenResponse.Content.ReadAsStringAsync();

                // Deserializes the token response
                var response = JObject.Parse(text);
                var accessToken = response.Value<string>("access_token");

                if (string.IsNullOrWhiteSpace(accessToken))
                {
                    _logger.WriteWarning("Access token was not found");
                    return new AuthenticationTicket(null, properties);
                }

                // Get the user from u
                var request = new HttpRequestMessage(HttpMethod.Get, Options.UserInfoEndpoint);
                request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);
                var graphResponse = await _httpClient.SendAsync(request, Request.CallCancelled);
                graphResponse.EnsureSuccessStatusCode();
                text = await graphResponse.Content.ReadAsStringAsync();
                var user = JsonSerializeHelper.Deserialize<UserInfo>(text);

                var context = new AxOAuth2AuthenticatedContext(Context, user, response);
                context.Identity = new ClaimsIdentity(Options.AuthenticationType,ClaimsIdentity.DefaultNameClaimType,ClaimsIdentity.DefaultRoleClaimType);
                context.Identity.AddClaim(new Claim("http://schemas.microsoft.com/accesscontrolservice/2010/07/claims/identityprovider", "ASP.NET Identity"));

                if (!string.IsNullOrEmpty(user.Id))
                {
                    context.Identity.AddClaim(new Claim(ClaimTypes.NameIdentifier, user.Id,
                        ClaimValueTypes.String, Options.AuthenticationType));
                }
                if (!string.IsNullOrEmpty(user.Name))
                {
                    context.Identity.AddClaim(new Claim(ClaimTypes.Name, user.Name, ClaimValueTypes.String,
                        Options.AuthenticationType));
                }
                if (!string.IsNullOrEmpty(user.Email))
                {
                    context.Identity.AddClaim(new Claim(ClaimTypes.Email, user.Email, ClaimValueTypes.String,
                        Options.AuthenticationType));
                }
               
                context.Properties = properties;

                await Options.Provider.Authenticated(context);

                return new AuthenticationTicket(context.Identity, context.Properties);
            }
            catch (Exception ex)
            {
                _logger.WriteError("Authentication failed", ex);
                return new AuthenticationTicket(null, properties);
            }
        }

 

u站 Startup.Auth.cs

app.UseOAuthBearerAuthentication(new Microsoft.Owin.Security.OAuth.OAuthBearerAuthenticationOptions());

在須要控制權限的地方加上[Authorize] 

配置webconfig中的machineKey和p站相同,則能夠解密獲取到access_token當中的用戶信息

 

利用oauth2.0 實現內部的統一身份認證,從開始到如今能全程走通,走了不少彎路,但願能對須要瞭解這一塊的朋友有所幫助,利用工做間隙的時間寫的,比較亂,有的地方還須要完善,請諒解。

有任何疑問請留言,一塊兒學習。

相關文章
相關標籤/搜索