首页 > 代码库 > Microsoft.Owin.Security 自定义AuthenticationHandler 实现oauth2 的授权码模式

Microsoft.Owin.Security 自定义AuthenticationHandler 实现oauth2 的授权码模式

三个网站:

1. www.cnblogs.com        用户直接面对和使用的web应用   以后简称w

2.passport.cnblogs.com   认证服务器,可以访问用户表的用户名和密码                             以后简称p

3.users.cnblogs.com        给其它应用程序提供服务,本例当中只是提供授权用户的个人信息,例如手机 邮箱等信息                            以后简称u

 

利用 Microsoft.Owin.Security 来实现oauth2 的授权码模式

 

主要流程:

1. w判断自己网站的cookie有没有,如果没有,跳转到自己的登录页

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 实现内部的统一身份认证,从开始到现在能全程走通,走了很多弯路,希望能对需要了解这一块的朋友有所帮助,利用工作间隙的时间写的,比较乱,有的地方还需要完善,请谅解。

有任何疑问请留言,一起学习。

 

Microsoft.Owin.Security 自定义AuthenticationHandler 实现oauth2 的授权码模式