首先回想一下Cookie認證,Cookie認證在用戶登陸成功以後將用戶信息加密後寫入瀏覽器Cookie中,服務端經過解析Cookie內容來驗證用戶登陸狀態。這樣作有幾個缺陷:html
JwtBear能夠解決上面的缺點git
提供了四個重載方法,主要設置配置類 JwtBearerOptions。
默認添加名稱爲Bearer的認證Schema,JwtBearerHandler爲處理器類。github
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); } }
一般來講用默認配置就夠了。web
public class JwtBearerOptions : AuthenticationSchemeOptions { /// <summary> /// Gets or sets if HTTPS is required for the metadata address or authority. /// The default is true. This should be disabled only in development environments. /// </summary> public bool RequireHttpsMetadata { get; set; } = true; /// <summary> /// Gets or sets the discovery endpoint for obtaining metadata /// </summary> public string MetadataAddress { get; set; } /// <summary> /// Gets or sets the Authority to use when making OpenIdConnect calls. /// </summary> public string Authority { get; set; } /// <summary> /// Gets or sets a single valid audience value for any received OpenIdConnect token. /// This value is passed into TokenValidationParameters.ValidAudience if that property is empty. /// </summary> /// <value> /// The expected audience for any received OpenIdConnect token. /// </value> public string Audience { get; set; } /// <summary> /// Gets or sets the challenge to put in the "WWW-Authenticate" header. /// </summary> public string Challenge { get; set; } = JwtBearerDefaults.AuthenticationScheme; /// <summary> /// The object provided by the application to process events raised by the bearer authentication handler. /// The application may implement the interface fully, or it may create an instance of JwtBearerEvents /// and assign delegates only to the events it wants to process. /// </summary> public new JwtBearerEvents Events { get { return (JwtBearerEvents)base.Events; } set { base.Events = value; } } /// <summary> /// The HttpMessageHandler used to retrieve metadata. /// This cannot be set at the same time as BackchannelCertificateValidator unless the value /// is a WebRequestHandler. /// </summary> public HttpMessageHandler BackchannelHttpHandler { get; set; } /// <summary> /// Gets or sets the timeout when using the backchannel to make an http call. /// </summary> public TimeSpan BackchannelTimeout { get; set; } = TimeSpan.FromMinutes(1); /// <summary> /// Configuration provided directly by the developer. If provided, then MetadataAddress and the Backchannel properties /// will not be used. This information should not be updated during request processing. /// </summary> public OpenIdConnectConfiguration Configuration { get; set; } /// <summary> /// Responsible for retrieving, caching, and refreshing the configuration from metadata. /// If not provided, then one will be created using the MetadataAddress and Backchannel properties. /// </summary> public IConfigurationManager<OpenIdConnectConfiguration> ConfigurationManager { get; set; } /// <summary> /// Gets or sets if a metadata refresh should be attempted after a SecurityTokenSignatureKeyNotFoundException. This allows for automatic /// recovery in the event of a signature key rollover. This is enabled by default. /// </summary> public bool RefreshOnIssuerKeyNotFound { get; set; } = true; /// <summary> /// Gets the ordered list of <see cref="ISecurityTokenValidator"/> used to validate access tokens. /// </summary> public IList<ISecurityTokenValidator> SecurityTokenValidators { get; } = new List<ISecurityTokenValidator> { new JwtSecurityTokenHandler() }; /// <summary> /// Gets or sets the parameters used to validate identity tokens. /// </summary> /// <remarks>Contains the types and definitions required for validating a token.</remarks> /// <exception cref="ArgumentNullException">if 'value' is null.</exception> public TokenValidationParameters TokenValidationParameters { get; set; } = new TokenValidationParameters(); /// <summary> /// Defines whether the bearer token should be stored in the /// <see cref="AuthenticationProperties"/> after a successful authorization. /// </summary> public bool SaveToken { get; set; } = true; /// <summary> /// Defines whether the token validation errors should be returned to the caller. /// Enabled by default, this option can be disabled to prevent the JWT handler /// from returning an error and an error_description in the WWW-Authenticate header. /// </summary> public bool IncludeErrorDetails { get; set; } = true; }
這裏會對配置作校驗。JwtBear默認是沒有提供發放Token的方法的,須要咱們本身實現,這個後面再說。發放Token能夠本地發放,也能夠請求遠程地址。算法
不少配置都是使用OpenConnectId協議來實現遠程認證須要的,若是是本地發放token則不要配置。後端
/// <summary> /// Invoked to post configure a JwtBearerOptions instance. /// </summary> /// <param name="name">The name of the options instance being configured.</param> /// <param name="options">The options instance to configure.</param> public void PostConfigure(string name, JwtBearerOptions options) { if (string.IsNullOrEmpty(options.TokenValidationParameters.ValidAudience) && !string.IsNullOrEmpty(options.Audience)) { options.TokenValidationParameters.ValidAudience = options.Audience; } if (options.ConfigurationManager == null) { if (options.Configuration != null) { options.ConfigurationManager = new StaticConfigurationManager<OpenIdConnectConfiguration>(options.Configuration); } else if (!(string.IsNullOrEmpty(options.MetadataAddress) && string.IsNullOrEmpty(options.Authority))) { if (string.IsNullOrEmpty(options.MetadataAddress) && !string.IsNullOrEmpty(options.Authority)) { options.MetadataAddress = options.Authority; if (!options.MetadataAddress.EndsWith("/", StringComparison.Ordinal)) { options.MetadataAddress += "/"; } options.MetadataAddress += ".well-known/openid-configuration"; } if (options.RequireHttpsMetadata && !options.MetadataAddress.StartsWith("https://", StringComparison.OrdinalIgnoreCase)) { throw new InvalidOperationException("The MetadataAddress or Authority must use HTTPS unless disabled for development by setting RequireHttpsMetadata=false."); } var httpClient = new HttpClient(options.BackchannelHttpHandler ?? new HttpClientHandler()); httpClient.Timeout = options.BackchannelTimeout; httpClient.MaxResponseContentBufferSize = 1024 * 1024 * 10; // 10 MB options.ConfigurationManager = new ConfigurationManager<OpenIdConnectConfiguration>(options.MetadataAddress, new OpenIdConnectConfigurationRetriever(), new HttpDocumentRetriever(httpClient) { RequireHttps = options.RequireHttpsMetadata }); } } }
上面提到了JwtBear項目沒有提供發放Token的方法,可使用微軟的擴展庫來實現。
SymmetricSecurityKey :表示使用對稱算法生成的全部密鑰的抽象基類。api
using Microsoft.AspNetCore.Mvc; using System; using System.Text; using IdentityModel; using Microsoft.IdentityModel.Tokens; using System.IdentityModel.Tokens.Jwt; using System.Security.Claims; [Route("api/user/login")] [HttpPost] public IActionResult Login([FromBody]UserDto dto) { //驗證username.password等邏輯..略 var tokenHandler = new JwtSecurityTokenHandler(); var key = Encoding.ASCII.GetBytes("this is a SecretKey"); var authTime = DateTime.UtcNow; var expiresAt = authTime.AddDays(7); var tokenDescriptor = new SecurityTokenDescriptor { Subject = new ClaimsIdentity(new Claim[] { new Claim(JwtClaimTypes.Id, "1"), //誰用token new Claim(JwtClaimTypes.Audience,"http://localhost:5000"), //誰發token new Claim(JwtClaimTypes.Issuer,"http://localhost:5000"), }), Expires = expiresAt, SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(key), SecurityAlgorithms.HmacSha256Signature) }; var token = tokenHandler.CreateToken(tokenDescriptor); var tokenString = tokenHandler.WriteToken(token); return Ok(tokenString); }
HS256算法要求key大於128bit即16字節,不然會出錯
擴展庫源碼地址:跨域
https://github.com/AzureAD/azure-activedirectory-identitymodel-extensions-for-dotnet/issues瀏覽器
上面的代碼只實現了很簡單的token頒發的功能,刷新token,scope的校驗,單點登陸等都沒有實現,不建議生產環境使用(除非你的需求十分簡單已經能夠知足)。實現這些十分麻煩,一般須要藉助框架好比IdentityServer,這個後面再聊。安全
Cookie認證簡圖
Cookie認證須要通知瀏覽器操做cookie,以及302跳轉,因此先後端同域的web場景比較合適。
Jwt認證簡圖
能夠看到服務端只負責頒發token、校驗token,校驗失敗返回標準401,至於401怎麼處理在於客戶端,服務端不依賴於瀏覽器,因此用於非web端、或者先後端分離的場景比較合適
JwtBearerHandler繼承自AuthenticationHandler,比CookieHandler少了SignIn和Signout的實現,它只處理認證(Authenticate)、質詢(Chanllenge)和拒絕(Forbid),上面已經說明過緣由了。
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; } }
JwtBearOptions配置類的這段代碼能夠看到, 默認校驗類是JwtSecurityTokenHandler,這是上面提到的擴展包裏面的類,命名空間是System.IdentityModel.Tokens.Jwt
/// <summary> /// Gets the ordered list of <see cref="ISecurityTokenValidator"/> used to validate access tokens. /// </summary> public IList<ISecurityTokenValidator> SecurityTokenValidators { get; } = new List<ISecurityTokenValidator> { new JwtSecurityTokenHandler() };
看一看代碼,代碼比較簡單,就是解碼token,而後將claims信息返回。以前生成jwt也是使用的這個類。
若是須要額外的校驗邏輯,能夠本身實現ISecurityTokenValidator,用這個類解碼token獲得claims以後實現本身的業務邏輯。
public override ClaimsPrincipal ValidateToken(string token, TokenValidationParameters validationParameters, out SecurityToken validatedToken) { if (string.IsNullOrWhiteSpace(token)) throw LogHelper.LogArgumentNullException(nameof(token)); if (validationParameters == null) throw LogHelper.LogArgumentNullException(nameof(validationParameters)); if (token.Length > MaximumTokenSizeInBytes) throw LogHelper.LogExceptionMessage(new ArgumentException(LogHelper.FormatInvariant(TokenLogMessages.IDX10209, token.Length, MaximumTokenSizeInBytes))); var tokenParts = token.Split(new char[] { '.' }, JwtConstants.MaxJwtSegmentCount + 1); if (tokenParts.Length != JwtConstants.JwsSegmentCount && tokenParts.Length != JwtConstants.JweSegmentCount) throw LogHelper.LogExceptionMessage(new ArgumentException(LogHelper.FormatInvariant(LogMessages.IDX12741, token))); if (tokenParts.Length == JwtConstants.JweSegmentCount) { var jwtToken = ReadJwtToken(token); var decryptedJwt = DecryptToken(jwtToken, validationParameters); var innerToken = ValidateSignature(decryptedJwt, validationParameters); jwtToken.InnerToken = innerToken; validatedToken = jwtToken; return ValidateTokenPayload(innerToken, validationParameters); } else { validatedToken = ValidateSignature(token, validationParameters); return ValidateTokenPayload(validatedToken as JwtSecurityToken, validationParameters); } }
質詢邏輯簡單說下,執行認證方法,成功則返回結果,失敗返回401,生成的報文大體這樣
https://tools.ietf.org/html/rfc6750#section-3.1 WWW-Authenticate: Bearer realm="example", error="invalid_token", error_description="The access token expired"
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()); } }
返回403
protected override Task HandleForbiddenAsync(AuthenticationProperties properties) { var forbiddenContext = new ForbiddenContext(Context, Scheme, Options); Response.StatusCode = 403; return Events.Forbidden(forbiddenContext); }
參考資料:
Cookie的SameSite屬性
CORS
ASPNET Core 認證與受權[4]:JwtBearer認證
https://www.cnblogs.com/RainingNight/p/jwtbearer-authentication-in-asp-net-core.html