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

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

时间: 2024-10-11 19:49:30

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

扩展 Microsoft.Owin.Security

微软在 OWIN 框架中对 OAuth 认证的支持非常好, 使用现有的 OWIN 中间件可以做到: 使用 Microsoft.Owin.Security.OAuth 搭建自己的 OAuth2 服务端, 没做过的可以参考这个简单教程: 使用 OWIN 搭建 OAuth2 服务器 : 使用 Microsoft.Owin.Security.Facebook 连接 Facbook 认证, 让用户使用 Facebook 帐户来登录: 使用 Microsoft.Owin.Security.Google 连接

Spring Security OAuth2 授权码模式

 背景: 由于业务实现中涉及到接入第三方系统(app接入有赞商城等),所以涉及到第三方系统需要获取用户信息(用户手机号.姓名等),为了保证用户信息的安全和接入方式的统一, 采用Oauth2四种模式之一的授权码模式.  介绍:        第三方系统调用我方提供的授权接口(步骤1) 用户同意授权,后跳转第三方系统(步骤2.3) 第三方系统获得code,根据code到我方系统获取token(步骤5.6 ) 根据获取token访问受保护的资源(步骤8.9)    实际应用中由于合作商户,所以需要直接

Springboot2.0 + OAuth2.0之授权码模式

综述:上两回讲了密码模式,应该是浅显易懂.如有补充,不足或者是有误之处还请多多指出.现在重头戏,授权码模式开始. 一.具体操作流程 - 用户访问客户端,后者将前者导向认证服务器,认证服务器返回认证页面(账号密码或者其他认证方式) - 用户选择是否给予客户端授权. - 假设用户给予授权,认证服务器将用户导向客户端事先指定的"重定向URI"(redirection URI),同时附上一个授权码. - 客户端收到授权码,附上早先的"重定向URI",向认证服务器申请令牌.这

Oauth2.0认证---授权码模式

目录: 1.功能描述 2.客户端的授权模式 3.授权模式认证流程 4.代码实现 1.功能描述 OAuth在"客户端"与"服务提供商"之间,设置了一个授权层(authorization layer)."客户端"不能直接登录"服务提供商",只能登录授权层,以此将用户与客户端区分开来. "客户端"登录授权层所用的令牌(token),与用户的密码不同.用户可以在登录的时候,指定授权层令牌的权限范围和有效期. 2.客

微信开发第五篇手机端微信公众号自定义菜单及OAuth2.0授权页面

说到自定义菜单,首先要想到调用微信的接口,其实微信公众号开发本身就是看对不对微信公众号接口的熟悉程度,我也是在项目中才开始接触微信公众号开发的,很感谢公司能给我这个项目机会.其实对于一个程序员来说最宝贵的是他的学习能力,而不是经验,不扯没用的了. 菜单上一篇讲到了怎么查看微信开发文档,那么很容易找到自定义菜单管理,根据里面的内容可以做一下思路 手机微信客户端与微信服务器交互,再由微信服务器与咱们自己的服务器交互,在第一次交互时先删除原始的那种可以打字的那种菜单,之后设置自己新的菜单,最后把自己的

OAuth2.0学习(4-12)spring-oauth-server分析 - 授权码模式验证过程

1.发起授权码请求 服务端url:authorization_code 2.服务端验证client_id返回授权码,      客户端,提示 用户User登录 3.服务端验证client_id,提示返回authorization_code 4.客户端接收authorization_code后,转发给了 /oauth/token  4.显示access_token和refresh_token

Spring Security实现OAuth2.0授权服务 - 进阶版

<Spring Security实现OAuth2.0授权服务 - 基础版>介绍了如何使用Spring Security实现OAuth2.0授权和资源保护,但是使用的都是Spring Security默认的登录页.授权页,client和token信息也是保存在内存中的. 本文将介绍如何在Spring Security OAuth项目中自定义登录页面.自定义授权页面.数据库配置client信息.数据库保存授权码和token令牌. 一.引入依赖 需要在基础版之上引入thymeleaf.JDBC.my

使用微服务架构思想,设计部署OAuth2.0授权认证框架

1,授权认证与微服务架构 1.1,由不同团队合作引发的授权认证问题 去年的时候,公司开发一款新产品,但人手不够,将B/S系统的Web开发外包,外包团队使用Vue.js框架,调用我们的WebAPI,但是这些WebAPI并不在一台服务器上,甚至可能是第三方提供的WebAPI.同时处于系统安全的架构设计,后端WebAPI是不能直接暴露在外面的:另一方面,我们这个新产品还有一个C/S系统,C端登录的时候,要求统一到B/S端登录,可以从C端无障碍的访问任意B/S端的页面,也可以调用B/S系统的一些API,

Spring-Cloud之OAuth2开放授权-11

一.OAuth(开放授权)是一个开放标准,允许用户授权第三方移动应用访问他们存储在另外的服务提供者上的信息,而不需要将用户名和密码提供给第三方移动应用或分享他们数据的所有内容,OAuth2.0是OAuth协议的延续版本,但不向后兼容OAuth 1.0即完全废止了OAuth1.0. 二.认证和授权过程. 1)主要包含3中角色: (1)服务提供方:Authorization Server (2)资源拥有者:Resource Server (3)客户端:Client 2)授权过程 (1)用户打开授权页