IdentityServer4之Jwt身份驗證方案分析


一,准備內容 

在之前講過的asp.net core 實現OAuth2.0四種模式系列中的IdentityApi客戶端用到了以下配置代碼

  public void ConfigureServices(IServiceCollection services)
        {
            services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
            services.AddAuthentication("Bearer").AddJwtBearer(r => {
                //認證地址
                r.Authority = "http://localhost:5000";
                //權限標識
                r.Audience = "secretapi";
                //是否必需HTTPS
                r.RequireHttpsMetadata = false;
            });
        }
   app.UseAuthentication();

 AddJwtBearer到底起到什么作用呢。首先熟習兩個概念

1,中間件(Middleware)

中間件是組裝到Asp.net core應用程序管道中以處理請求和響應的軟件。可以這樣理解:一根管道從水源(用戶)連接到家庭(資源)。水源的水是不能直接飲用的,需要重重過濾,這些過濾手段就是中間件,在處理過程中決定是否往下繼續傳送,可能丟棄,也可能轉到其它地方。請參考我之前寫的《Asp.net core之中間件》

2,身份認證執行方案(AuthenticationSchemes)

 在一個啟用身份認證的Asp.net core應用中可以有幾個執行方案,分工不同,功能也不同。可以指定由那個方案進行身份認證,如以下代碼

      [HttpGet]
        [Route("api/identity")]
        [Microsoft.AspNetCore.Authorization.Authorize(Roles ="admin",AuthenticationSchemes ="Bearer")]
        public object GetUserClaims()

 指定了方案名為“Bearer”的方案來做這個Api接口的認證。這個"Bearer"是怎么來的呢,看一下services.AddAuthentication方法有幾個重載,我們上面用的重載是傳遞一個字符串指定默認方案為“Bearer”,那么程序是如果根據"Bearer"這個方案名找到對應的執行方案的呢?

二,AddJwtBearer添加Jwt證書驗證執行方案

AddJwtBearer是Microsoft.AspNetCore.Authentication.JwtBearer對AuthenticationBuilder的一個擴寫方法,看一下源碼

  public static class JwtBearerExtensions
    {
        public static AuthenticationBuilder AddJwtBearer(this AuthenticationBuilder builder)
            => builder.AddJwtBearer(JwtBearerDefaults.AuthenticationScheme, _ => { });

        public static AuthenticationBuilder AddJwtBearer(this AuthenticationBuilder builder, Action<JwtBearerOptions> configureOptions)
            => builder.AddJwtBearer(JwtBearerDefaults.AuthenticationScheme, configureOptions);

        public static AuthenticationBuilder AddJwtBearer(this AuthenticationBuilder builder, string authenticationScheme, Action<JwtBearerOptions> configureOptions)
            => builder.AddJwtBearer(authenticationScheme, displayName: null, configureOptions: configureOptions);

        public static AuthenticationBuilder AddJwtBearer(this AuthenticationBuilder builder, string authenticationScheme, string displayName, Action<JwtBearerOptions> configureOptions)
        {
            builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton<IPostConfigureOptions<JwtBearerOptions>, JwtBearerPostConfigureOptions>());
            return builder.AddScheme<JwtBearerOptions, JwtBearerHandler>(authenticationScheme, displayName, configureOptions);
        }
    }

 有四個方法重載,但最后運行的是最后一個重載,最后一個重載用了builder.AddScheme方法添加方案,所以,AddJwtBearer本質上就是添加驗證方案。前二個方法重載沒有傳“authenticationScheme"參數,使用的是JwtBearerDefaults.AuthenticationScheme這個值,我們上邊用的代碼是第二個重載,傳了configOptions,沒傳authenticationScheme,JwtBearerDefaults.AuthenticationScheme這個值預設為Bearer(見以下源碼),所以根據Bearer這個方案名找到的方案就是我們運行AddJwtBearer所添加的方案。

 public static class JwtBearerDefaults
    {
        /// <summary>
        /// Default value for AuthenticationScheme property in the JwtBearerAuthenticationOptions
        /// </summary>
        public const string AuthenticationScheme = "Bearer";
    }

三,JwtBearer執行方案具體做了什么工作

上面說過AddJwtBearer本質上就是添加一個執行方案。先看下添加執行方案的關鍵源碼

 

 

 把方案的HandlerType指定為方法的第二個泛型,方便從根據方案實例化Hndler,並將這個泛型添加進了服務依賴。從AddJwtBearer源碼可看到出這個泛型為:JwtBearerHandler

  public static AuthenticationBuilder AddJwtBearer(this AuthenticationBuilder builder, string authenticationScheme, string displayName, Action<JwtBearerOptions> configureOptions)
        {
            builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton<IPostConfigureOptions<JwtBearerOptions>, JwtBearerPostConfigureOptions>());
            return builder.AddScheme<JwtBearerOptions, JwtBearerHandler>(authenticationScheme, displayName, configureOptions);
        }

分析JwtBearerHandler源碼,JwtBearerHandler主要是能干三件事

 1,HandleAuthenticateAsync:獲取HTTP請求頭里的Authorization頭。先驗證是不是Bearer格式,再用JwtSecurityTokenHandler這個工具類驗證Jwt數據,包括長度,格式,是否過期,簽發地址等。

         觸發事件:1),MessageReceived:接收到請時觸發。

         2),TokenValidated:驗證Jwt數據成功時觸發。

        3),AuthenticationFailed:驗證Jwt數據失敗時觸發。

附源碼

  protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
        {
            string token = null;
            try
            {
                // Give application opportunity to find from a different location, adjust, or reject token
                var messageReceivedContext = new MessageReceivedContext(Context, Scheme, Options);

                // event can set the token
                await Events.MessageReceived(messageReceivedContext);
                if (messageReceivedContext.Result != null)
                {
                    return messageReceivedContext.Result;
                }

                // If application retrieved token from somewhere else, use that.
                token = messageReceivedContext.Token;

                if (string.IsNullOrEmpty(token))
                {
                    string authorization = Request.Headers[HeaderNames.Authorization];

                    // If no authorization header found, nothing to process further
                    if (string.IsNullOrEmpty(authorization))
                    {
                        return AuthenticateResult.NoResult();
                    }

                    if (authorization.StartsWith("Bearer ", StringComparison.OrdinalIgnoreCase))
                    {
                        token = authorization.Substring("Bearer ".Length).Trim();
                    }

                    // If no token found, no further work possible
                    if (string.IsNullOrEmpty(token))
                    {
                        return AuthenticateResult.NoResult();
                    }
                }

                if (_configuration == null && Options.ConfigurationManager != null)
                {
                    _configuration = await Options.ConfigurationManager.GetConfigurationAsync(Context.RequestAborted);
                }

                var validationParameters = Options.TokenValidationParameters.Clone();
                if (_configuration != null)
                {
                    var issuers = new[] { _configuration.Issuer };
                    validationParameters.ValidIssuers = validationParameters.ValidIssuers?.Concat(issuers) ?? issuers;

                    validationParameters.IssuerSigningKeys = validationParameters.IssuerSigningKeys?.Concat(_configuration.SigningKeys)
                        ?? _configuration.SigningKeys;
                }

                List<Exception> validationFailures = null;
                SecurityToken validatedToken;
                foreach (var validator in Options.SecurityTokenValidators)
                {
                    if (validator.CanReadToken(token))
                    {
                        ClaimsPrincipal principal;
                        try
                        {
                            principal = validator.ValidateToken(token, validationParameters, out validatedToken);
                        }
                        catch (Exception ex)
                        {
                            Logger.TokenValidationFailed(ex);

                            // Refresh the configuration for exceptions that may be caused by key rollovers. The user can also request a refresh in the event.
                            if (Options.RefreshOnIssuerKeyNotFound && Options.ConfigurationManager != null
                                && ex is SecurityTokenSignatureKeyNotFoundException)
                            {
                                Options.ConfigurationManager.RequestRefresh();
                            }

                            if (validationFailures == null)
                            {
                                validationFailures = new List<Exception>(1);
                            }
                            validationFailures.Add(ex);
                            continue;
                        }

                        Logger.TokenValidationSucceeded();

                        var tokenValidatedContext = new TokenValidatedContext(Context, Scheme, Options)
                        {
                            Principal = principal,
                            SecurityToken = validatedToken
                        };

                        await Events.TokenValidated(tokenValidatedContext);
                        if (tokenValidatedContext.Result != null)
                        {
                            return tokenValidatedContext.Result;
                        }

                        if (Options.SaveToken)
                        {
                            tokenValidatedContext.Properties.StoreTokens(new[]
                            {
                                new AuthenticationToken { Name = "access_token", Value = token }
                            });
                        }

                        tokenValidatedContext.Success();
                        return tokenValidatedContext.Result;
                    }
                }

                if (validationFailures != null)
                {
                    var authenticationFailedContext = new AuthenticationFailedContext(Context, Scheme, Options)
                    {
                        Exception = (validationFailures.Count == 1) ? validationFailures[0] : new AggregateException(validationFailures)
                    };

                    await Events.AuthenticationFailed(authenticationFailedContext);
                    if (authenticationFailedContext.Result != null)
                    {
                        return authenticationFailedContext.Result;
                    }

                    return AuthenticateResult.Fail(authenticationFailedContext.Exception);
                }

                return AuthenticateResult.Fail("No SecurityTokenValidator available for token: " + token ?? "[null]");
            }
            catch (Exception ex)
            {
                Logger.ErrorProcessingMessage(ex);

                var authenticationFailedContext = new AuthenticationFailedContext(Context, Scheme, Options)
                {
                    Exception = ex
                };

                await Events.AuthenticationFailed(authenticationFailedContext);
                if (authenticationFailedContext.Result != null)
                {
                    return authenticationFailedContext.Result;
                }

                throw;
            }
        }

  

   2,HandleChallengeAsync:驗證失敗時挑戰驗證結果,有點像網球比賽的挑戰鷹眼功能。但Jwt的挑戰驗證極其簡單,就是重新調用了一次HandleAuthenticateAsync,然后就是挑戰失敗后設置請求上下文的狀態碼為:401,也就是我們在前端訪問的Response狀態碼,再往Http回應的Http Header上加上一個名為WWWAuthenticate的頭。觸發Challenge事件表示挑戰失敗。

附源碼

 protected override async Task HandleChallengeAsync(AuthenticationProperties properties)
        {
            var authResult = await HandleAuthenticateOnceSafeAsync();
            var eventContext = new JwtBearerChallengeContext(Context, Scheme, Options, properties)
            {
                AuthenticateFailure = authResult?.Failure
            };

            // Avoid returning error=invalid_token if the error is not caused by an authentication failure (e.g missing token).
            if (Options.IncludeErrorDetails && eventContext.AuthenticateFailure != null)
            {
                eventContext.Error = "invalid_token";
                eventContext.ErrorDescription = CreateErrorDescription(eventContext.AuthenticateFailure);
            }

            await Events.Challenge(eventContext);
            if (eventContext.Handled)
            {
                return;
            }

            Response.StatusCode = 401;

            if (string.IsNullOrEmpty(eventContext.Error) &&
                string.IsNullOrEmpty(eventContext.ErrorDescription) &&
                string.IsNullOrEmpty(eventContext.ErrorUri))
            {
                Response.Headers.Append(HeaderNames.WWWAuthenticate, Options.Challenge);
            }
            else
            {
                // https://tools.ietf.org/html/rfc6750#section-3.1
                // WWW-Authenticate: Bearer realm="example", error="invalid_token", error_description="The access token expired"
                var builder = new StringBuilder(Options.Challenge);
                if (Options.Challenge.IndexOf(' ') > 0)
                {
                    // Only add a comma after the first param, if any
                    builder.Append(',');
                }
                if (!string.IsNullOrEmpty(eventContext.Error))
                {
                    builder.Append(" error=\"");
                    builder.Append(eventContext.Error);
                    builder.Append("\"");
                }
                if (!string.IsNullOrEmpty(eventContext.ErrorDescription))
                {
                    if (!string.IsNullOrEmpty(eventContext.Error))
                    {
                        builder.Append(",");
                    }

                    builder.Append(" error_description=\"");
                    builder.Append(eventContext.ErrorDescription);
                    builder.Append('\"');
                }
                if (!string.IsNullOrEmpty(eventContext.ErrorUri))
                {
                    if (!string.IsNullOrEmpty(eventContext.Error) ||
                        !string.IsNullOrEmpty(eventContext.ErrorDescription))
                    {
                        builder.Append(",");
                    }

                    builder.Append(" error_uri=\"");
                    builder.Append(eventContext.ErrorUri);
                    builder.Append('\"');
                }

                Response.Headers.Append(HeaderNames.WWWAuthenticate, builder.ToString());
            }
        }

  3,HandleForbiddenAsync,驗證Jwt數據成功,但授權失敗時會調用這個方法,設置Response狀態碼為403,直接返回不再繼續往下。觸發Forbidden事件。

附源碼

 protected override Task HandleForbiddenAsync(AuthenticationProperties properties)
        {
            var forbiddenContext = new ForbiddenContext(Context, Scheme, Options);
            Response.StatusCode = 403;
            return Events.Forbidden(forbiddenContext);
        }
        

  

三,JwtBearer執行方案工作流程

上邊說了JwtBearerHandler的三個功能,這一小節來講講這三個功能在什么時候開始工作的。

上面我們使用AddAuthentication,AddJwtBearer只是把這個身份驗證這個功能加入到服務,好比你買了台冰箱放在家里,還沒有上電使用,占了個地方而已,怎么使用呢,這里就要用到中間件,中間件就像一個即插即用的插頭。啟用身份驗證的中間件用UseAuthentication方法。看一下這個方法的源碼,看它又做了什么事。

// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;

namespace Microsoft.AspNetCore.Authentication
{
    public class AuthenticationMiddleware
    {
        private readonly RequestDelegate _next;

        public AuthenticationMiddleware(RequestDelegate next, IAuthenticationSchemeProvider schemes)
        {
            if (next == null)
            {
                throw new ArgumentNullException(nameof(next));
            }
            if (schemes == null)
            {
                throw new ArgumentNullException(nameof(schemes));
            }

            _next = next;
            Schemes = schemes;
        }

        public IAuthenticationSchemeProvider Schemes { get; set; }

        public async Task Invoke(HttpContext context)
        {
            context.Features.Set<IAuthenticationFeature>(new AuthenticationFeature
            {
                OriginalPath = context.Request.Path,
                OriginalPathBase = context.Request.PathBase
            });

            // Give any IAuthenticationRequestHandler schemes a chance to handle the request
            var handlers = context.RequestServices.GetRequiredService<IAuthenticationHandlerProvider>();
            foreach (var scheme in await Schemes.GetRequestHandlerSchemesAsync())
            {
                var handler = await handlers.GetHandlerAsync(context, scheme.Name) as IAuthenticationRequestHandler;
                if (handler != null && await handler.HandleRequestAsync())
                {
                    return;
                }
            }

            var defaultAuthenticate = await Schemes.GetDefaultAuthenticateSchemeAsync();
            if (defaultAuthenticate != null)
            {
                var result = await context.AuthenticateAsync(defaultAuthenticate.Name);
                if (result?.Principal != null)
                {
                    context.User = result.Principal;
                }
            }

            await _next(context);
        }
    }
}

 重點是Invoke方法,看來就做了二件事

1,從當前方案集合里(可添加多個方案,目前我們只用了一個Bearer)篩選出IAuthenticationRequestHandler的實現類,執行他的HandleRequestAsync方法。

2,找到默認執行方案,執行他的AuthenticateAsync方法。

第1件事,當前我添加的Bearer方案所用的JwtBearerHandler並沒有繼承自IAuthenticationRequestHandler,所以這一步在當前驗證方案就沒起作用,我們在以后講AddOpenIdConnect時會講到這一步,使用OpenidConnect做身份驗證時,OpenidConnect所用的OpenIdConnectHandler是RemoteAuthenticationHandler的實現,而RemoteAuthenticationHandler繼承了IAuthenticationRequestHandler

 public class JwtBearerHandler : AuthenticationHandler<JwtBearerOptions>

 

  public abstract class RemoteAuthenticationHandler<TOptions> : AuthenticationHandler<TOptions>, IAuthenticationRequestHandler

第2件事,執行AuthenticateAsync方法,在JwtBearerHandler中沒有這個方法,但他的父類 AuthenticationHandler<JwtBearerOptions>中是有的。在父類中執行AuthenticateAsync時如果沒有設置ForwardAuthenticate(驗證方案跳轉),會執行HandleAuthenticateOnceAsync方法,這個方法要注意:他是一個類似於單例的調用方式,在生命周期內只會觸發一次子類的HandleAuthenticateAsync方法。也就是JwtBearerHandler的HandleAuthenticateAsync方法。理解這個對后續的工作流很重要。

附源碼

public async Task<AuthenticateResult> AuthenticateAsync()
        {
            var target = ResolveTarget(Options.ForwardAuthenticate);
            if (target != null)
            {
                return await Context.AuthenticateAsync(target);
            }

            // Calling Authenticate more than once should always return the original value.
            var result = await HandleAuthenticateOnceAsync();
            if (result?.Failure == null)
            {
                var ticket = result?.Ticket;
                if (ticket?.Principal != null)
                {
                    Logger.AuthenticationSchemeAuthenticated(Scheme.Name);
                }
                else
                {
                    Logger.AuthenticationSchemeNotAuthenticated(Scheme.Name);
                }
            }
            else
            {
                Logger.AuthenticationSchemeNotAuthenticatedWithFailure(Scheme.Name, result.Failure.Message);
            }
            return result;
        }

        /// <summary>
        /// Used to ensure HandleAuthenticateAsync is only invoked once. The subsequent calls
        /// will return the same authenticate result.
        /// </summary>
        protected Task<AuthenticateResult> HandleAuthenticateOnceAsync()
        {
            if (_authenticateTask == null)
            {
                _authenticateTask = HandleAuthenticateAsync();
            }

            return _authenticateTask;
        }

好了,JwtBearerHandler的三個功能,我們已經搞清一個了,他的驗證功能在請求伊始就會能過身份驗證中間件觸發。那另二個呢,另外二個功能的觸發點需要用到另一個中間件,身份授權中間件(UseAuthorization)。這個中間件不用手動Use,AddMvc和UseMvc已經把這部份工作做了。這個中間件干了什么,看下他的中間件實現源碼

// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authorization.Policy;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;

namespace Microsoft.AspNetCore.Authorization
{
    public class AuthorizationMiddleware
    {
        // Property key is used by other systems, e.g. MVC, to check if authorization middleware has run
        private const string AuthorizationMiddlewareInvokedKey = "__AuthorizationMiddlewareInvoked";
        private static readonly object AuthorizationMiddlewareInvokedValue = new object();

        private readonly RequestDelegate _next;
        private readonly IAuthorizationPolicyProvider _policyProvider;

        public AuthorizationMiddleware(RequestDelegate next, IAuthorizationPolicyProvider policyProvider)
        {
            _next = next ?? throw new ArgumentNullException(nameof(next));
            _policyProvider = policyProvider ?? throw new ArgumentNullException(nameof(policyProvider));
        }

        public async Task Invoke(HttpContext context)
        {
            if (context == null)
            {
                throw new ArgumentNullException(nameof(context));
            }

            var endpoint = context.GetEndpoint();

            // Flag to indicate to other systems, e.g. MVC, that authorization middleware was run for this request
            context.Items[AuthorizationMiddlewareInvokedKey] = AuthorizationMiddlewareInvokedValue;

            // IMPORTANT: Changes to authorization logic should be mirrored in MVC's AuthorizeFilter
            var authorizeData = endpoint?.Metadata.GetOrderedMetadata<IAuthorizeData>() ?? Array.Empty<IAuthorizeData>();
            var policy = await AuthorizationPolicy.CombineAsync(_policyProvider, authorizeData);
            if (policy == null)
            {
                await _next(context);
                return;
            }

            // Policy evaluator has transient lifetime so it fetched from request services instead of injecting in constructor
            var policyEvaluator = context.RequestServices.GetRequiredService<IPolicyEvaluator>();

            var authenticateResult = await policyEvaluator.AuthenticateAsync(policy, context);

            // Allow Anonymous skips all authorization
            if (endpoint?.Metadata.GetMetadata<IAllowAnonymous>() != null)
            {
                await _next(context);
                return;
            }

            // Note that the resource will be null if there is no matched endpoint
            var authorizeResult = await policyEvaluator.AuthorizeAsync(policy, authenticateResult, context, resource: endpoint);

            if (authorizeResult.Challenged)
            {
                if (policy.AuthenticationSchemes.Any())
                {
                    foreach (var scheme in policy.AuthenticationSchemes)
                    {
                        await context.ChallengeAsync(scheme);
                    }
                }
                else
                {
                    await context.ChallengeAsync();
                }

                return;
            }
            else if (authorizeResult.Forbidden)
            {
                if (policy.AuthenticationSchemes.Any())
                {
                    foreach (var scheme in policy.AuthenticationSchemes)
                    {
                        await context.ForbidAsync(scheme);
                    }
                }
                else
                {
                    await context.ForbidAsync();
                }

                return;
            }

            await _next(context);
        }
    }
}

  1,先進行策略驗證,是不是該請求不需要授權,是的話就往下傳遞請求,不再執行后邊的代碼

  2,該請求需要授權訪問,請調用policyEvaluator.AuthorizeAsync進行身份及授權驗證

附源碼

 public virtual async Task<PolicyAuthorizationResult> AuthorizeAsync(AuthorizationPolicy policy, AuthenticateResult authenticationResult, HttpContext context, object resource)
        {
            if (policy == null)
            {
                throw new ArgumentNullException(nameof(policy));
            }

            var result = await _authorization.AuthorizeAsync(context.User, resource, policy);
            if (result.Succeeded)
            {
                return PolicyAuthorizationResult.Success();
            }

            // If authentication was successful, return forbidden, otherwise challenge
            return (authenticationResult.Succeeded) 
                ? PolicyAuthorizationResult.Forbid() 
                : PolicyAuthorizationResult.Challenge();
        }

如果身份和授權都驗證成功,則成功,如果身份驗證能過,授權沒通過則禁止訪問,直接回應,如果身份驗證沒通過就去挑戰驗證結果,挑戰成功繼續來一次來,挑戰失敗就直接回應了。源碼中的PolicyAuthorizationResult.Forbid() 和PolicyAuthorizationResult.Challenge()具體執行的是什么方法呢?看以下源碼

   public virtual Task<AuthenticationScheme> GetDefaultChallengeSchemeAsync()
            => _options.DefaultChallengeScheme != null
            ? GetSchemeAsync(_options.DefaultChallengeScheme)
            : GetDefaultSchemeAsync();
        public virtual Task<AuthenticationScheme> GetDefaultForbidSchemeAsync()
            => _options.DefaultForbidScheme != null
            ? GetSchemeAsync(_options.DefaultForbidScheme)
            : GetDefaultChallengeSchemeAsync();

 然來如果沒有指定特定的方案,就返回默認的方案。指定特定的Challenge方案和Forbid方案我們講OpenIdConnect時再詳細說。目前我們所用的只有一個默認方案:Bearer,所以會執行JwtBearerHandler的Challenge和Forbid方法。

如此一來,JwtBearerHandler的三種功能觸發時機,作用都已經搞清楚了,我畫了個圖方便大家理理解

 

 

 

 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM