• .net core 第三方登录关于AddOAuth


    在.net 如果需要用到认证授权,第三方登录,肯定离不开 AddAuthentication(),他的实现就是AuthenticationHandler,如果是需要添加自定义认证就可以去重写AuthenticationHandler,尽管有些认证我们没有看到服务注入AddAuthentication,但是实际上他们源码都是去重写了他,我们平时用到的都是他们封装好了的,例如Identity,AddMicrosoftAccount等等

    我们这里介绍的第三方认证其实也就是在AuthenticationHandler的基础上实现的。

    先看看AuthenticationHandler的子类RemoteAuthenticationHandler,看这个名字就知道是远程认证,所以当我们需要远程认证的时候就可以去重写RemoteAuthenticationHandler,微软已经给我们提供了,不需要我们去重写AuthenticationHandler。

    再看看RemoteAuthenticationHandler的子类OAuthHandler,这个类名包含了OAuth,所以它就是基于OAuth协议来实现的地方认证,如果要使用他必须要满足需要认证的第三方认证服务必须是基于OAuth协议的。

    再进入今天的主题 AddOAuth ,他的主要实现就是去实现了OAuthHandler,我们平时如果需要用到的第三方认证是基于OAuth协议的话,那我们就可以用到它了,例如当我们用微软账号第三方登录,QQ登录,微信扫码登录,IdentityServer4等等都是可以通过AddOAuth 实现的。这里可以看到.AddOAuth的几种方式

    大概就是两种方式,一种需要注入OAuthOptions和OAuthHandlerSource去实现,这里可以去继承重写这两项;另外一种就是不去重写这两项,默认用原生的OAuthHandlerSource去实现。
    例如IdentityServer4的认证,下面这个是通过第一种方式去实现的登录的,我这里用OAuthHandlerSource去重写了OAuthHandler

    builder.Services.AddAuthentication()
        .AddOAuth<OAuthOptions, OAuthHandlerSource>("SourceIdentity", options =>
        {
            options.ClientId = "fcbtest";
            options.ClientSecret = "fcbtest";
            options.Scope.Add("openid");
            options.Scope.Add("profile");
            options.SignInScheme = "Cookies";
            options.SaveTokens = true;
            options.CallbackPath = new PathString("/Test/OauthRedirct2");
            options.AuthorizationEndpoint = "http://192.168.0.66:7200/connect/authorize";
            options.TokenEndpoint = "http://192.168.0.66:7200/connect/token";
            options.UserInformationEndpoint = "http://192.168.0.66:7200/connect/userinfo";
        })

     还有另外一种方式,不去重写这两项,默认用原生的OAuthHandlerSource去实现,直接去重写相应的事件就可以了,具体的注释已经写在上面了

    .AddOAuth("localhost", options =>
        {
            options.ClientId = "fcbtest"; //ClientId
            options.ClientSecret = "fcbtest"; //ClientSecret
            options.CallbackPath = new PathString("/Test/OauthRedirct"); //获取Code之后的回调地址
            options.AuthorizationEndpoint = "http://192.168.0.66:7200/connect/authorize"; //认证重定向地址
            options.TokenEndpoint = "http://192.168.0.66:7200/connect/token"; //获取Token地址
            options.UserInformationEndpoint = "http://192.168.0.66:7200/connect/userinfo"; //获取认证成功后人员信息
            options.SaveTokens = true; //是否向客户端的cookie中写入access_token、refresh_token、token_type,如果写入肯定会增大cookie的大小
            options.SignInScheme = "Cookies"; //远程注册的方案
            options.Events = new OAuthEvents() //当然这里如果代码很多,可以直接去重写OAuthEvents这个事件,
            {
                OnCreatingTicket = async context => //在创建票据之后的事件触发。这里除了这两种事件还有其他时间,可以去OAuthEvents里面查看
                {
                    //这里和主要是将用户信息写入Claim中,具体写入哪些到Claim中主要是下面options.ClaimActions去配置,如果是options.ClaimActions.MapAll()就是将所有的用户信息写入Claim中,如果是自定义加入就用options.ClaimActions.MapJsonKey()去指定
                    var request = new HttpRequestMessage(HttpMethod.Get, context.Options.UserInformationEndpoint);
                    request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", context.AccessToken);
                    request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
                    var response = await context.Backchannel.SendAsync(request, context.HttpContext.RequestAborted);
                    response.EnsureSuccessStatusCode();
                    var res = await response.Content.ReadAsStringAsync();
                    using (var user = JsonDocument.Parse(await response.Content.ReadAsStringAsync()))
                    {
                        context.RunClaimActions(user.RootElement);
                    }
                },
                OnTicketReceived = ticketcontext => //接受到票据的时候触发
                {
                    return Task.CompletedTask;
                }
            };
            options.Scope.Add("openid");  //需要添加的scope
            options.Scope.Add("profile");
            //options.ClaimActions.MapAll(); //把所有用户信息写入Claim中
            options.ClaimActions.MapJsonKey(ClaimTypes.NameIdentifier, "sub"); //这种就是自定义写入Claim中
            options.ClaimActions.MapJsonKey(ClaimTypes.Name, "name");
            options.ClaimActions.MapJsonKey("test", "name", ClaimValueTypes.Email);
        })
    

     

    当然我们也可能在基于OAuth协议的基础上,参数会有一些变化,例如微信扫码登录,需要拿取code重定向授权地址的时候将clientid改成了appid

    如果出现这种情况我们可以去自定义继承OAuthOptions和OAuthHandler去处理,下一篇文章就会介绍微信扫码登录,就是通过继承OAuthOptions和OAuthHandler去重写实现的,这里记录一下OAuthHandler的源码,方便到时候需要重写时直接复制过去,重写相应的地方就行了

    /// <summary>
        /// OAuthHandler里面一般需要用到的处理程序,需要那一步重写可以直接重写
        /// 这里Handler需要绑定OAuthOptions的泛型类,如果业务需要也可以 创建一个类去继承OAuthOptions,方便写入自定义属性
        /// </summary>
        public class OAuthHandlerSource : OAuthHandler<OAuthOptions>
        {
            public OAuthHandlerSource(IOptionsMonitor<OAuthOptions> options, ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock) : base(options, logger, encoder, clock)
            {
    
            }
            /// <summary>
            /// OAuth每次请求的Handle
            /// </summary>
            /// <returns></returns>
            /// <exception cref="Exception"></exception>
            public override async Task<bool> HandleRequestAsync()
            {
                //var a = Options.CallbackPath;
                //var b = Request.Path;
                /* ShouldHandleRequestAsync()  源码就是比较回调地址和请求地址是否一致    
                ShouldHandleRequestAsync()  => Task.FromResult(Options.CallbackPath == Request.Path);
                */
                if (!await ShouldHandleRequestAsync())
                {
                    return false;
                }
                //拿到code回调之后会进行后续操作
                AuthenticationTicket? ticket = null;
                Exception? exception = null;
                AuthenticationProperties? properties = null;
                try
                {
                    var authResult = await HandleRemoteAuthenticateAsync();
                    if (authResult == null)
                    {
                        exception = new InvalidOperationException("Invalid return state, unable to redirect.");
                    }
                    else if (authResult.Handled)
                    {
                        return true;
                    }
                    else if (authResult.Skipped || authResult.None)
                    {
                        return false;
                    }
                    else if (!authResult.Succeeded)
                    {
                        exception = authResult.Failure ?? new InvalidOperationException("Invalid return state, unable to redirect.");
                        properties = authResult.Properties;
                    }
    
                    ticket = authResult?.Ticket;
                }
                catch (Exception ex)
                {
                    exception = ex;
                }
    
                if (exception != null)
                {
    
                    var errorContext = new RemoteFailureContext(Context, Scheme, Options, exception)
                    {
                        Properties = properties
                    };
                    await Events.RemoteFailure(errorContext);
    
                    if (errorContext.Result != null)
                    {
                        if (errorContext.Result.Handled)
                        {
                            return true;
                        }
                        else if (errorContext.Result.Skipped)
                        {
                            return false;
                        }
                        else if (errorContext.Result.Failure != null)
                        {
                            throw new Exception("An error was returned from the RemoteFailure event.", errorContext.Result.Failure);
                        }
                    }
    
                    if (errorContext.Failure != null)
                    {
                        throw new Exception("An error was encountered while handling the remote login.", errorContext.Failure);
                    }
                }
    
                // We have a ticket if we get here
                var ticketContext = new TicketReceivedContext(Context, Scheme, Options, ticket)
                {
                    ReturnUri = ticket.Properties.RedirectUri
                };
    
                ticket.Properties.RedirectUri = null;
    
                // Mark which provider produced this identity so we can cross-check later in HandleAuthenticateAsync
                ticketContext.Properties!.Items[".AuthScheme"] = Scheme.Name;
    
                await Events.TicketReceived(ticketContext);
    
                if (ticketContext.Result != null)
                {
                    if (ticketContext.Result.Handled)
                    {
                        return true;
                    }
                    else if (ticketContext.Result.Skipped)
                    {
                        return false;
                    }
                }
    
                await Context.SignInAsync(SignInScheme, ticketContext.Principal!, ticketContext.Properties);
    
                // Default redirect path is the base path
                if (string.IsNullOrEmpty(ticketContext.ReturnUri))
                {
                    ticketContext.ReturnUri = "/";
                }
    
                Response.Redirect(ticketContext.ReturnUri);
                return true;
            }
            /// <summary>
            /// 最初Challenge 之后的Handle
            /// </summary>
            /// <param name="properties"></param>
            /// <returns></returns>
            protected override async Task HandleChallengeAsync(AuthenticationProperties properties)
            {
                if (string.IsNullOrEmpty(properties.RedirectUri))
                {
                    properties.RedirectUri = OriginalPathBase + OriginalPath + Request.QueryString;
                }
    
                // OAuth2 10.12 CSRF
                GenerateCorrelationId(properties);
    
                var authorizationEndpoint = BuildChallengeUrl(properties, BuildRedirectUri(Options.CallbackPath));
                var redirectContext = new RedirectContext<OAuthOptions>(
                    Context, Scheme, Options,
                    properties, authorizationEndpoint);
                await Events.RedirectToAuthorizationEndpoint(redirectContext);
    
                var location = Context.Response.Headers.Location;
                if (location == StringValues.Empty)
                {
                    location = "(not set)";
                }
    
                var cookie = Context.Response.Headers.SetCookie;
                if (cookie == StringValues.Empty)
                {
                    cookie = "(not set)";
                }
    
                //Logger.HandleChallenge(location.ToString(), cookie.ToString());
            }
            /// <summary>
            /// 第一步 获取Code之前,需要重定向的地址,可以再这里重写跳转地址的参数
            /// </summary>
            /// <param name="properties">The <see cref="AuthenticationProperties"/>.</param>
            /// <param name="redirectUri">The url to redirect to once the challenge is completed.</param>
            /// <returns>The challenge url.</returns>
            protected override string BuildChallengeUrl(AuthenticationProperties properties, string redirectUri)
            {
                var scopeParameter = properties.GetParameter<ICollection<string>>(OAuthChallengeProperties.ScopeKey);
                var scope = scopeParameter != null ? FormatScope(scopeParameter) : FormatScope();
                //OAuth协议
                var parameters = new Dictionary<string, string>
                {
                    { "client_id", Options.ClientId },
                    { "scope", scope },
                    { "response_type", "code" },
                    { "redirect_uri", redirectUri },
                };
                //properties.RedirectUri = redirectUri;
                if (Options.UsePkce)
                {
                    var bytes = new byte[32];
                    RandomNumberGenerator.Fill(bytes);
                    var codeVerifier = Base64UrlTextEncoder.Encode(bytes);
    
                    // Store this for use during the code redemption.
                    properties.Items.Add(OAuthConstants.CodeVerifierKey, codeVerifier);
    
                    var challengeBytes = SHA256.HashData(Encoding.UTF8.GetBytes(codeVerifier));
                    var codeChallenge = WebEncoders.Base64UrlEncode(challengeBytes);
    
                    parameters[OAuthConstants.CodeChallengeKey] = codeChallenge;
                    parameters[OAuthConstants.CodeChallengeMethodKey] = OAuthConstants.CodeChallengeMethodS256;
                }
    
                parameters["state"] = Options.StateDataFormat.Protect(properties);
    
                return QueryHelpers.AddQueryString(Options.AuthorizationEndpoint, parameters!);
            }
            /// <summary>
            /// 第二步 接收到code后,获取token,再创建票据   这里可以重写需要的claims信息等等
            /// </summary>
            /// <returns></returns>
            protected override async Task<HandleRequestResult> HandleRemoteAuthenticateAsync()
            {
                var query = Request.Query;
    
                var state = query["state"];
                var properties = Options.StateDataFormat.Unprotect(state);
    
                if (properties == null)
                {
                    return HandleRequestResult.Fail("The oauth state was missing or invalid.");
                }
    
                // OAuth2 10.12 CSRF
                if (!ValidateCorrelationId(properties))
                {
                    return HandleRequestResult.Fail("Correlation failed.", properties);
                }
    
                var error = query["error"];
                if (!StringValues.IsNullOrEmpty(error))
                {
                    // Note: access_denied errors are special protocol errors indicating the user didn't
                    // approve the authorization demand requested by the remote authorization server.
                    // Since it's a frequent scenario (that is not caused by incorrect configuration),
                    // denied errors are handled differently using HandleAccessDeniedErrorAsync().
                    // Visit https://tools.ietf.org/html/rfc6749#section-4.1.2.1 for more information.
                    var errorDescription = query["error_description"];
                    var errorUri = query["error_uri"];
                    if (StringValues.Equals(error, "access_denied"))
                    {
                        var result = await HandleAccessDeniedErrorAsync(properties);
                        if (!result.None)
                        {
                            return result;
                        }
                        var deniedEx = new Exception("Access was denied by the resource owner or by the remote server.");
                        deniedEx.Data["error"] = error.ToString();
                        deniedEx.Data["error_description"] = errorDescription.ToString();
                        deniedEx.Data["error_uri"] = errorUri.ToString();
    
                        return HandleRequestResult.Fail(deniedEx, properties);
                    }
    
                    var failureMessage = new StringBuilder();
                    failureMessage.Append(error);
                    if (!StringValues.IsNullOrEmpty(errorDescription))
                    {
                        failureMessage.Append(";Description=").Append(errorDescription);
                    }
                    if (!StringValues.IsNullOrEmpty(errorUri))
                    {
                        failureMessage.Append(";Uri=").Append(errorUri);
                    }
    
                    var ex = new Exception(failureMessage.ToString());
                    ex.Data["error"] = error.ToString();
                    ex.Data["error_description"] = errorDescription.ToString();
                    ex.Data["error_uri"] = errorUri.ToString();
    
                    return HandleRequestResult.Fail(ex, properties);
                }
    
                var code = query["code"];
    
                if (StringValues.IsNullOrEmpty(code))
                {
                    return HandleRequestResult.Fail("Code was not found.", properties);
                }
    
                var codeExchangeContext = new OAuthCodeExchangeContext(properties, code.ToString(), BuildRedirectUri(Options.CallbackPath));
                using var tokens = await ExchangeCodeAsync(codeExchangeContext);
    
                if (tokens.Error != null)
                {
                    return HandleRequestResult.Fail(tokens.Error, properties);
                }
    
                if (string.IsNullOrEmpty(tokens.AccessToken))
                {
                    return HandleRequestResult.Fail("Failed to retrieve access token.", properties);
                }
                #region 这里是测试写入claims 
                //List<Claim> claims = new List<Claim>();
                //claims.Add(new Claim(ClaimTypes.NameIdentifier, tokens.AccessToken));
                //claims.Add(new Claim("test", "范臣斌"));
                //var identity = new ClaimsIdentity(claims, ClaimsIssuer);
    
                var identity = new ClaimsIdentity(ClaimsIssuer);
                #endregion
                if (Options.SaveTokens)
                {
                    var authTokens = new List<AuthenticationToken>();
    
                    authTokens.Add(new AuthenticationToken { Name = "access_token", Value = tokens.AccessToken });
                    if (!string.IsNullOrEmpty(tokens.RefreshToken))
                    {
                        authTokens.Add(new AuthenticationToken { Name = "refresh_token", Value = tokens.RefreshToken });
                    }
    
                    if (!string.IsNullOrEmpty(tokens.TokenType))
                    {
                        authTokens.Add(new AuthenticationToken { Name = "token_type", Value = tokens.TokenType });
                    }
    
                    if (!string.IsNullOrEmpty(tokens.ExpiresIn))
                    {
                        int value;
                        if (int.TryParse(tokens.ExpiresIn, NumberStyles.Integer, CultureInfo.InvariantCulture, out value))
                        {
                            // https://www.w3.org/TR/xmlschema-2/#dateTime
                            // https://msdn.microsoft.com/en-us/library/az4se3k1(v=vs.110).aspx
                            var expiresAt = Clock.UtcNow + TimeSpan.FromSeconds(value);
                            authTokens.Add(new AuthenticationToken
                            {
                                Name = "expires_at",
                                Value = expiresAt.ToString("o", CultureInfo.InvariantCulture)
                            });
                        }
                    }
    
                    properties.StoreTokens(authTokens);
                }
    
                var ticket = await CreateTicketAsync(identity, properties, tokens);
                if (ticket != null)
                {
                    return HandleRequestResult.Success(ticket);
                }
                else
                {
                    return HandleRequestResult.Fail("Failed to retrieve user information from remote server.", properties);
                }
            }
            /// <summary>
            /// 第三步 通过到code获取Token
            /// Exchanges the authorization code for a authorization token from the remote provider.
            /// </summary>
            /// <param name="context">The <see cref="OAuthCodeExchangeContext"/>.</param>
            /// <returns>The response <see cref="OAuthTokenResponse"/>.</returns>
            protected override async Task<OAuthTokenResponse> ExchangeCodeAsync(OAuthCodeExchangeContext context)
            {
                var tokenRequestParameters = new Dictionary<string, string>()
                {
                    { "client_id", Options.ClientId },
                    { "redirect_uri", context.RedirectUri },
                    { "client_secret", Options.ClientSecret },
                    { "code", context.Code },
                    { "grant_type", "authorization_code" },
                };
    
                // PKCE https://tools.ietf.org/html/rfc7636#section-4.5, see BuildChallengeUrl
                if (context.Properties.Items.TryGetValue(OAuthConstants.CodeVerifierKey, out var codeVerifier))
                {
                    tokenRequestParameters.Add(OAuthConstants.CodeVerifierKey, codeVerifier!);
                    context.Properties.Items.Remove(OAuthConstants.CodeVerifierKey);
                }
    
                var requestContent = new FormUrlEncodedContent(tokenRequestParameters!);
    
                var requestMessage = new HttpRequestMessage(HttpMethod.Post, Options.TokenEndpoint);
                requestMessage.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
                requestMessage.Content = requestContent;
                requestMessage.Version = Backchannel.DefaultRequestVersion;
                var response = await Backchannel.SendAsync(requestMessage, Context.RequestAborted);
                var body = await response.Content.ReadAsStringAsync();
    
                return response.IsSuccessStatusCode switch
                {
                    true => OAuthTokenResponse.Success(JsonDocument.Parse(body)),
                    //false =>PrepareFailedOAuthTokenReponse(response, body)
                    false =>OAuthTokenResponse.Failed(new Exception("OAuth token endpoint failure"))
                };
            }
            //private static OAuthTokenResponse PrepareFailedOAuthTokenReponse(HttpResponseMessage response, string body)
            //{
            //    var exception = OAuthTokenResponse.GetStandardErrorException(JsonDocument.Parse(body));
    
            //    if (exception is null)
            //    {
            //        var errorMessage = $"OAuth token endpoint failure: Status: {response.StatusCode};Headers: {response.Headers};Body: {body};";
            //        return OAuthTokenResponse.Failed(new Exception(errorMessage));
            //    }
    
            //    return OAuthTokenResponse.Failed(exception);
            //}
            /// <summary>
            /// 第四步  拿到Token之后创建票据
            /// Creates an <see cref="AuthenticationTicket"/> from the specified <paramref name="tokens"/>.
            /// </summary>
            /// <param name="identity">The <see cref="ClaimsIdentity"/>.</param>
            /// <param name="properties">The <see cref="AuthenticationProperties"/>.</param>
            /// <param name="tokens">The <see cref="OAuthTokenResponse"/>.</param>
            /// <returns>The <see cref="AuthenticationTicket"/>.</returns>
            protected virtual async Task<AuthenticationTicket> CreateTicketAsync(ClaimsIdentity identity, AuthenticationProperties properties, OAuthTokenResponse tokens)
            {
                using (var user = JsonDocument.Parse("{}"))
                {
                    var context = new OAuthCreatingTicketContext(new ClaimsPrincipal(identity), properties, Context, Scheme, Options, Backchannel, tokens, user.RootElement);
                    await Events.CreatingTicket(context);
                    return new AuthenticationTicket(context.Principal!, context.Properties, Scheme.Name);
                }
            }
        }
    

     

  • 相关阅读:
    理解 RESTful:理论与最佳实践
    Shiro 性能优化:解决 Session 频繁读写问题
    单点登录的三种实现方式
    理解 Spring(二):AOP 的概念与实现原理
    理解 Spring(一):Spring 与 IoC
    MFC查内存泄漏方法
    024 --- 第28章 访问者模式
    023 --- 第27章 解释器模式
    022 --- 第26章 享元模式
    021 --- 第25章 中介者模式
  • 原文地址:https://www.cnblogs.com/roubaozidd/p/15855191.html
Copyright © 2020-2023  润新知