追本溯源,从使用开始
首先看一下我们通常是如何使用微软自带的认证,一般在Startup里面配置我们所需的依赖认证服务,这里通过JWT的认证方式讲解
public void ConfigureServices(IServiceCollection services) { services.AddAuthentication(authOpt => { authOpt.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; authOpt.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; }) .AddJwtBearer(o => { o.TokenValidationParameters = new TokenValidationParameters { //配置自己所要验证的参数 }; }); }
我们来看一下源码AddAuthentication主要做了什么
public static class AuthenticationServiceCollectionExtensions { public static AuthenticationBuilder AddAuthentication( this IServiceCollection services, Action<AuthenticationOptions> configureOptions) { if (services == null) throw new ArgumentNullException(nameof (services)); if (configureOptions == null) throw new ArgumentNullException(nameof (configureOptions)); AuthenticationBuilder authenticationBuilder = services.AddAuthentication(); services.Configure<AuthenticationOptions>(configureOptions); return authenticationBuilder; } public static AuthenticationBuilder AddAuthentication( this IServiceCollection services) { if (services == null) throw new ArgumentNullException(nameof (services)); services.AddAuthenticationCore(); services.AddDataProtection(); services.AddWebEncoders(); services.TryAddSingleton<ISystemClock, SystemClock>(); return new AuthenticationBuilder(services); } public static AuthenticationBuilder AddAuthentication( this IServiceCollection services, string defaultScheme) { return services.AddAuthentication((Action<AuthenticationOptions>) (o => o.DefaultScheme = defaultScheme)); } ..... }
ConfigureServices方法基本都是服务的注册,基于微软的风格,这里的AddAuthenticationCore肯定是我们的认证服务注册方法,来看一下
public static class AuthenticationCoreServiceCollectionExtensions { /// <summary> /// Add core authentication services needed for <see cref="T:Microsoft.AspNetCore.Authentication.IAuthenticationService" />. /// </summary> public static IServiceCollection AddAuthenticationCore( this IServiceCollection services) { if (services == null) throw new ArgumentNullException(nameof (services)); services.TryAddScoped<IAuthenticationService, AuthenticationService>(); services.TryAddSingleton<IClaimsTransformation, NoopClaimsTransformation>(); services.TryAddScoped<IAuthenticationHandlerProvider, AuthenticationHandlerProvider>(); services.TryAddSingleton<IAuthenticationSchemeProvider, AuthenticationSchemeProvider>(); return services; } /// <summary> /// Add core authentication services needed for <see cref="T:Microsoft.AspNetCore.Authentication.IAuthenticationService" />. /// </summary> public static IServiceCollection AddAuthenticationCore( this IServiceCollection services, Action<AuthenticationOptions> configureOptions) { if (services == null) throw new ArgumentNullException(nameof (services)); if (configureOptions == null) throw new ArgumentNullException(nameof (configureOptions)); services.AddAuthenticationCore(); services.Configure<AuthenticationOptions>(configureOptions); return services; } }
我们看到这里主要注册了AuthenticationService, AuthenticationHandlerProvider, AuthenticationSchemeProvider这三个对象,如文章开头所说,追本溯源,从使用开始,我们先看一下这三个对象是如何在认证体系中使用的,且是如何发挥作用的。
从使用开始
看一下我们的认证管道构建
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory) { ... app.UseAuthentication(); ... } public static class AuthAppBuilderExtensions { public static IApplicationBuilder UseAuthentication( this IApplicationBuilder app) { if (app == null) throw new ArgumentNullException(nameof (app)); return app.UseMiddleware<AuthenticationMiddleware>(); } }
这里使用了约定的注册方式UseMiddleware,并且指定使用中间件AuthenticationMiddleware
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)); this._next = next; this.Schemes = schemes; } public IAuthenticationSchemeProvider Schemes { get; set; } public async Task Invoke(HttpContext context) { context.Features.Set<IAuthenticationFeature>((IAuthenticationFeature) new AuthenticationFeature() { OriginalPath = context.Request.Path, OriginalPathBase = context.Request.PathBase }); IAuthenticationHandlerProvider handlers = context.RequestServices.GetRequiredService<IAuthenticationHandlerProvider>(); foreach (AuthenticationScheme authenticationScheme in await this.Schemes.GetRequestHandlerSchemesAsync()) { IAuthenticationRequestHandler handlerAsync = await handlers.GetHandlerAsync(context, authenticationScheme.Name) as IAuthenticationRequestHandler; bool flag = handlerAsync != null; if (flag) flag = await handlerAsync.HandleRequestAsync(); if (flag) return; } AuthenticationScheme authenticateSchemeAsync = await this.Schemes.GetDefaultAuthenticateSchemeAsync(); if (authenticateSchemeAsync != null) { AuthenticateResult authenticateResult = await context.AuthenticateAsync(authenticateSchemeAsync.Name); //实际的认证业务 if (authenticateResult?.Principal != null) context.User = authenticateResult.Principal; } await this._next(context); } }
在继续往下之前,我们先看一下这个认证中间件的作用结果,当认证通过时,在HttpContext的User属性(ClaimPrincipal)赋予身份标识,所以在后续的请求管道中都是基于认证结果中的身份标识做鉴权,这个我们会在后面的实际操作中会提到。
言归正传,在这里引出了我们的两个对象AuthenticationHandlerProvider, AuthenticationSchemeProvider。
重要对象讲解
IAuthenticationSchemeProvider
从名字来看,IAuthenticationSchemeProvider的作用应该是提供Scheme的,这也是Provider在微软的风格里面起的作用(类似于工厂模式)。
这个Scheme是什么呢?很明显,在Framework时代,也是有基于不同Scheme验证的,比如Bearer,Cookie,在Aspnet Core中定义不同的Scheme代表着不同的认证处理方式,具体体现是在每个Scheme中包含对应的IAuthenticationHandler类型的Handler,由它来完成跟自身Scheme相关的认证处理。如果没有定义会怎么样?仔细看上面这块源码,只有当AuthenticationScheme不为空时才会做认证,否则一旦在Controller打上鉴权标签[Authorize],将会直接返回401,所以我们必须指定自己的Scheme。
那么我们在哪里指定我们的Scheme类似呢?我们先返回到ConfigureService的AddJwtBearer,使用过的朋友们肯定知道,这里获取的Scheme是我们在ConfigureService通过Addxxx scheme指定的Scheme类型。这里我们是使用JWT的
在这里指定了TOptions 为JwtBearerOptions,而THandler为JwtBearerHandler。
public virtual AuthenticationBuilder AddScheme<TOptions, THandler>( string authenticationScheme, string displayName, Action<TOptions> configureOptions) where TOptions : AuthenticationSchemeOptions, new() where THandler : AuthenticationHandler<TOptions> { return this.AddSchemeHelper<TOptions, THandler>(authenticationScheme, displayName, configureOptions); } private AuthenticationBuilder AddSchemeHelper<TOptions, THandler>( string authenticationScheme, string displayName, Action<TOptions> configureOptions) where TOptions : class, new() where THandler : class, IAuthenticationHandler { this.Services.Configure<AuthenticationOptions>((Action<AuthenticationOptions>) (o => o.AddScheme(authenticationScheme, (Action<AuthenticationSchemeBuilder>) (scheme => { scheme.HandlerType = typeof (THandler); scheme.DisplayName = displayName; })))); if (configureOptions != null) this.Services.Configure<TOptions>(authenticationScheme, configureOptions); this.Services.AddTransient<THandler>(); return this; }
注意这里TOptions 是需要继承AuthenticationSchemeOptions的,在这里是JwtBearerOptions,而THandler是AuthenticationHandler<TOptions>类型的Handler,在这里是JwtBearerHandler。
我们回到Scheme的分析继续往下,首先看一下AuthenticationScheme的定义
public class AuthenticationScheme { /// <summary>Constructor.</summary> public AuthenticationScheme(string name, string displayName, Type handlerType) { if (name == null) throw new ArgumentNullException(nameof (name)); if (handlerType == (Type) null) throw new ArgumentNullException(nameof (handlerType)); if (!typeof (IAuthenticationHandler).IsAssignableFrom(handlerType)) throw new ArgumentException("handlerType must implement IAuthenticationHandler."); this.Name = name; this.HandlerType = handlerType; this.DisplayName = displayName; } /// <summary>The name of the authentication scheme.</summary> public string Name { get; } /// <summary> /// The display name for the scheme. Null is valid and used for non user facing schemes. /// </summary> public string DisplayName { get; } /// <summary> /// The <see cref="T:Microsoft.AspNetCore.Authentication.IAuthenticationHandler" /> type that handles this scheme. /// </summary> public Type HandlerType { get; } }
在这里可以看到,如果要使用Aspnet Core自身的认证体系,需先注册Scheme,并且该Scheme必须指定一个类型为IAuthenticationHandler的Handler,否则会抛出异常。(这个其实在AddxxxScheme的时候已经指定了AuthenticationHandler)
我们再看一下IAuthenticationSchemeProvider的GetRequestHandlerSchemesAsync方法做了什么
public virtual Task<IEnumerable<AuthenticationScheme>> GetRequestHandlerSchemesAsync() { return Task.FromResult<IEnumerable<AuthenticationScheme>>((IEnumerable<AuthenticationScheme>) this._requestHandlers); }
这东西返回了_requestHandlers,这是什么?看代码
public class AuthenticationSchemeProvider : IAuthenticationSchemeProvider { private readonly object _lock = new object(); private readonly AuthenticationOptions _options; private readonly IDictionary<string, AuthenticationScheme> _schemes; private readonly List<AuthenticationScheme> _requestHandlers; /// <summary> /// Creates an instance of <see cref="T:Microsoft.AspNetCore.Authentication.AuthenticationSchemeProvider" /> /// using the specified <paramref name="options" />, /// </summary> public AuthenticationSchemeProvider(IOptions<AuthenticationOptions> options) : this(options, (IDictionary<string, AuthenticationScheme>) new Dictionary<string, AuthenticationScheme>((IEqualityComparer<string>) StringComparer.Ordinal)) { } /// <summary> /// Creates an instance of <see cref="T:Microsoft.AspNetCore.Authentication.AuthenticationSchemeProvider" /> /// using the specified <paramref name="options" /> and <paramref name="schemes" />. /// </summary> protected AuthenticationSchemeProvider( IOptions<AuthenticationOptions> options, IDictionary<string, AuthenticationScheme> schemes) { this._options = options.Value; IDictionary<string, AuthenticationScheme> dictionary = schemes; if (dictionary == null) throw new ArgumentNullException(nameof (schemes)); this._schemes = dictionary; this._requestHandlers = new List<AuthenticationScheme>(); foreach (AuthenticationSchemeBuilder scheme in this._options.Schemes) this.AddScheme(scheme.Build()); } public virtual void AddScheme(AuthenticationScheme scheme) { if (this._schemes.ContainsKey(scheme.Name)) throw new InvalidOperationException("Scheme already exists: " + scheme.Name); lock (this._lock) { if (this._schemes.ContainsKey(scheme.Name)) throw new InvalidOperationException("Scheme already exists: " + scheme.Name); if (typeof (IAuthenticationRequestHandler).IsAssignableFrom(scheme.HandlerType)) this._requestHandlers.Add(scheme); this._schemes[scheme.Name] = scheme; } } ..... }
这东西就是把我们在认证注册服务中指定的scheme,通过解析出的AuthenticationSchemeProvider 的构造函数加载来的,进而返回一系列的List<AuthenticationScheme>,OK拿到这些scheme之后有什么用呢?这里引出了我们的第二个对象AuthenticationHandlerProvider,下面我们来了解一下。
IAuthenticationHandlerProvider
我们看到,AuthenticationMiddleware中用到了IAuthenticationHandlerProvider的GetHandlerAsync方法,那我们先看一下这个方法的作用
public class AuthenticationHandlerProvider : IAuthenticationHandlerProvider { private Dictionary<string, IAuthenticationHandler> _handlerMap = new Dictionary<string, IAuthenticationHandler>((IEqualityComparer<string>) StringComparer.Ordinal); /// <summary>Constructor.</summary> public AuthenticationHandlerProvider(IAuthenticationSchemeProvider schemes) { this.Schemes = schemes; } /// <summary> /// The <see cref="T:Microsoft.AspNetCore.Authentication.IAuthenticationHandlerProvider" />. /// </summary> public IAuthenticationSchemeProvider Schemes { get; } /// <summary>Returns the handler instance that will be used.</summary> public async Task<IAuthenticationHandler> GetHandlerAsync( HttpContext context, string authenticationScheme) { if (this._handlerMap.ContainsKey(authenticationScheme)) return this._handlerMap[authenticationScheme]; AuthenticationScheme schemeAsync = await this.Schemes.GetSchemeAsync(authenticationScheme); if (schemeAsync == null) return (IAuthenticationHandler) null; IAuthenticationHandler handler = (context.RequestServices.GetService(schemeAsync.HandlerType) ?? ActivatorUtilities.CreateInstance(context.RequestServices, schemeAsync.HandlerType)) as IAuthenticationHandler; if (handler != null) { await handler.InitializeAsync(schemeAsync, context); this._handlerMap[authenticationScheme] = handler; } return handler; } }
在创建Handler的时候,是先从AuthenticationScheme中获取,如果不存在则通过ActivatorUtilities创建。获取到Handle后,将会放在_handlerMap字典里面, 当下次获取Handler的时候,将直接从缓存中获取。
IAuthenticationService
这个对象是在AuthenticationMiddleware中最后才用到的,而且是基于HttpContext的扩展被调用
public static class AuthenticationHttpContextExtensions { public static Task<AuthenticateResult> AuthenticateAsync(this HttpContext context, string scheme) => context.RequestServices.GetRequiredService<IAuthenticationService>().AuthenticateAsync(context, scheme); .... }
这里主要调用了IAuthenticationService的AuthenticateAsync方法,看一下这个方法做了什么
public class AuthenticationService : IAuthenticationService { public IAuthenticationSchemeProvider Schemes { get; } public IAuthenticationHandlerProvider Handlers { get; } public IClaimsTransformation Transform { get; } public virtual async Task<AuthenticateResult> AuthenticateAsync(HttpContext context, string scheme) { if (scheme == null) { var scheme = (await this.Schemes.GetDefaultAuthenticateSchemeAsync())?.Name; if (scheme == null) throw new InvalidOperationException($"No authenticationScheme was specified, and there was no DefaultAuthenticateScheme found."); } var handler = await Handlers.GetHandlerAsync(context, scheme); if(handler == null) throw await this.CreateMissingHandlerException(scheme); AuthenticateResult result = await handler.AuthenticateAsync(); if (result != null && result.Succeeded) return AuthenticateResult.Success(new AuthenticationTicket(await Transform.TransformAsync(result.Principal), result.Properties, result.Ticket.AuthenticationScheme)); return result; } }
这里其实就是我们在前面讲的根据Scheme获取对应的AuthenticationHandler,然后调用AuthenticateAsync()方法,这个方法调用了核心方法HandleAuthenticateOnceAsync,然后再调用HandleAuthenticateAsync()这个核心的认证方法。
从上图看到这个HandleAuthenticateAsync是个抽象方法,我们的子类都需要实现这个方法的动作,基于本文的例子,我们看一下JwtBearerHandler的一个实际认证。
public class JwtBearerHandler : AuthenticationHandler<JwtBearerOptions> { protected override async Task<AuthenticateResult> HandleAuthenticateAsync() { JwtBearerHandler jwtBearerHandler = this; string token = (string) null; object obj; AuthenticationFailedContext authenticationFailedContext; int num; try { MessageReceivedContext messageReceivedContext = new MessageReceivedContext(jwtBearerHandler.Context, jwtBearerHandler.Scheme, jwtBearerHandler.Options); await jwtBearerHandler.Events.MessageReceived(messageReceivedContext); if (messageReceivedContext.Result != null) return messageReceivedContext.Result; token = messageReceivedContext.Token; if (string.IsNullOrEmpty(token)) { string header = (string) jwtBearerHandler.Request.Headers["Authorization"]; if (string.IsNullOrEmpty(header)) return AuthenticateResult.NoResult(); if (header.StartsWith("Bearer ", StringComparison.OrdinalIgnoreCase)) token = header.Substring("Bearer ".Length).Trim(); if (string.IsNullOrEmpty(token)) return AuthenticateResult.NoResult(); } if (jwtBearerHandler._configuration == null && jwtBearerHandler.Options.ConfigurationManager != null) { OpenIdConnectConfiguration configurationAsync = await jwtBearerHandler.Options.ConfigurationManager.GetConfigurationAsync(jwtBearerHandler.Context.RequestAborted); jwtBearerHandler._configuration = configurationAsync; } TokenValidationParameters validationParameters1 = jwtBearerHandler.Options.TokenValidationParameters.Clone(); if (jwtBearerHandler._configuration != null) { string[] strArray = new string[1] { jwtBearerHandler._configuration.Issuer }; TokenValidationParameters validationParameters2 = validationParameters1; IEnumerable<string> validIssuers = validationParameters1.get_ValidIssuers(); object obj1 = (validIssuers != null ? (object) validIssuers.Concat<string>((IEnumerable<string>) strArray) : (object) null) ?? (object) strArray; validationParameters2.set_ValidIssuers((IEnumerable<string>) obj1); TokenValidationParameters validationParameters3 = validationParameters1; IEnumerable<SecurityKey> issuerSigningKeys = validationParameters1.get_IssuerSigningKeys(); IEnumerable<SecurityKey> securityKeys = (issuerSigningKeys != null ? issuerSigningKeys.Concat<SecurityKey>((IEnumerable<SecurityKey>) jwtBearerHandler._configuration.get_SigningKeys()) : (IEnumerable<SecurityKey>) null) ?? (IEnumerable<SecurityKey>) jwtBearerHandler._configuration.get_SigningKeys(); validationParameters3.set_IssuerSigningKeys(securityKeys); } List<Exception> exceptionList = (List<Exception>) null; foreach (ISecurityTokenValidator securityTokenValidator in (IEnumerable<ISecurityTokenValidator>) jwtBearerHandler.Options.SecurityTokenValidators) { if (securityTokenValidator.CanReadToken(token)) { SecurityToken securityToken; ClaimsPrincipal claimsPrincipal; try { claimsPrincipal = securityTokenValidator.ValidateToken(token, validationParameters1, ref securityToken); } catch (Exception ex) { jwtBearerHandler.Logger.TokenValidationFailed(ex); if (jwtBearerHandler.Options.RefreshOnIssuerKeyNotFound && jwtBearerHandler.Options.ConfigurationManager != null && ex is SecurityTokenSignatureKeyNotFoundException) jwtBearerHandler.Options.ConfigurationManager.RequestRefresh(); if (exceptionList == null) exceptionList = new List<Exception>(1); exceptionList.Add(ex); continue; } jwtBearerHandler.Logger.TokenValidationSucceeded(); TokenValidatedContext validatedContext = new TokenValidatedContext(jwtBearerHandler.Context, jwtBearerHandler.Scheme, jwtBearerHandler.Options); validatedContext.Principal = claimsPrincipal; validatedContext.SecurityToken = securityToken; TokenValidatedContext tokenValidatedContext = validatedContext; await jwtBearerHandler.Events.TokenValidated(tokenValidatedContext); if (tokenValidatedContext.Result != null) return tokenValidatedContext.Result; if (jwtBearerHandler.Options.SaveToken) tokenValidatedContext.Properties.StoreTokens((IEnumerable<AuthenticationToken>) new AuthenticationToken[1] { new AuthenticationToken() { Name = "access_token", Value = token } }); tokenValidatedContext.Success(); return tokenValidatedContext.Result; } } if (exceptionList == null) return AuthenticateResult.Fail("No SecurityTokenValidator available for token: " + token ?? "[null]"); authenticationFailedContext = new AuthenticationFailedContext(jwtBearerHandler.Context, jwtBearerHandler.Scheme, jwtBearerHandler.Options) { Exception = exceptionList.Count == 1 ? exceptionList[0] : (Exception) new AggregateException((IEnumerable<Exception>) exceptionList) }; await jwtBearerHandler.Events.AuthenticationFailed(authenticationFailedContext); return authenticationFailedContext.Result == null ? AuthenticateResult.Fail(authenticationFailedContext.Exception) : authenticationFailedContext.Result; } catch (Exception ex) { obj = (object) ex; num = 1; } if (num == 1) { Exception ex = (Exception) obj; jwtBearerHandler.Logger.ErrorProcessingMessage(ex); authenticationFailedContext = new AuthenticationFailedContext(jwtBearerHandler.Context, jwtBearerHandler.Scheme, jwtBearerHandler.Options) { Exception = ex }; await jwtBearerHandler.Events.AuthenticationFailed(authenticationFailedContext); if (authenticationFailedContext.Result != null) return authenticationFailedContext.Result; Exception source = obj as Exception; if (source == null) throw obj; ExceptionDispatchInfo.Capture(source).Throw(); authenticationFailedContext = (AuthenticationFailedContext) null; } obj = (object) null; token = (string) null; AuthenticateResult authenticateResult; return authenticateResult; } }
这个方法有点长,主要是从Request.Headers里面获取Authorization的Bearer出来解析,再在AddJwtBearer中传入的委托参数JwtBearerOptions的TokenValidationParameters属性作为依据进行对比来进行认证是否通过与否。
总结
本文对 ASP.NET Core 的认证流程做了一个源码分析流程介绍,由于是源码分析篇,所以可能会比较枯燥和苦涩难懂。在后面的真正使用过程中,然后再结合本篇的一个总结流程,相信大家会逐渐开朗。
- 在Startup类中的ConfigureServices方法通过添加AddAuthentication注册我们最主要的三个对象AuthenticationService, AuthenticationHandlerProvider, AuthenticationSchemeProvider
- 通过AddAuthentication返回的AuthenticationBuilder 通过AddJwtBearer(或者AddCookie)来指定Scheme类型和需要验证的参数
- 在Startup类中的Configure方法通过添加UseAuthentication注册认证中间件
- 在认证过程中,通过AuthenticationSchemeProvider获取正确的Scheme,在AuthenticationService中通过Scheme和AuthenticationHandlerProvider获取正确的AuthenticationHandler,最后通过对应的AuthenticationHandler的AuthenticateAsync方法进行认证流程
出处:https://cloud.tencent.com/developer/article/1498055
=======================================================================================
ASP.NET Core 2.2 : 二十六. 应用JWT进行用户认证及Token的刷新
本文将通过实际的例子来演示如何在ASP.NET Core中应用JWT进行用户认证以及Token的刷新方案(ASP.NET Core 系列目录)
一、什么是JWT?
JWT(json web token)基于开放标准(RFC 7519),是一种无状态的分布式的身份验证方式,主要用于在网络应用环境间安全地传递声明。它是基于JSON的,所以它也像json一样可以在.Net、JAVA、JavaScript,、PHP等多种语言使用。
为什么要使用JWT?
传统的Web应用一般采用Cookies+Session来进行认证。但对于目前越来越多的App、小程序等应用来说,它们对应的服务端一般都是RestFul 类型的无状态的API,再采用这样的的认证方式就不是很方便了。而JWT这种无状态的分布式的身份验证方式恰好符合这样的需求。
二、JWT的组成:
JWT是什么样子的呢?它就是下面这样的一段字符串:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJodHRwOi8vc2NoZW1hcy54bWxzb2FwLm9yZy93cy8yMDA1LzA1L2lkZW50aXR5L2NsYWltcy9uYW1laWRlbnRpZmllciI6IjAwMiIsImh0dHA6Ly9zY2hlbWFzLnhtbHNvYXAub3JnL3dzLzIwMDUvMDUvaWRlbnRpdHkvY2xhaW1zL25hbWUiOiLmnY7lm5siLCJuYmYiOjE1NjU5MjMxMjIsImV4cCI6MTU2NTkyMzI0MiwiaXNzIjoiaHR0cDovL2xvY2FsaG9zdDo1NDIxNCIsImF1ZCI6Imh0dHA6Ly9sb2NhbGhvc3Q6NTQyMTUifQ.Mrta7nftmfXeo_igBVd4rl2keMmm0rg0WkqRXoVAeik
它是由三段“乱码”字符串通过两个“.”连接在一起组成。官网https://jwt.io/提供了它的验证方式
它的三个字符串分别对应了上图右侧的Header、Payload和Signature三部分。
Header:
Header:
{
"alg": "HS256",
"typ": "JWT"
}
标识加密方式为HS256,Token类型为JWT, 这段JSON通过Base64Url编码形成上例的第一个字符串
Payload
Payload是JWT用于信息存储部分,其中包含了许多种的声明(claims)。
可以自定义多个声明添加到Payload中,系统也提供了一些默认的类型
iss (issuer):签发人
exp (expiration time):过期时间
sub (subject):主题
aud (audience):受众
nbf (Not Before):生效时间
iat (Issued At):签发时间
jti (JWT ID):编号
这部分通过Base64Url编码生成第二个字符串。
Signature
Signature是用于Token的验证。它的值类似这样的表达式:Signature = HMACSHA256( base64UrlEncode(header) + "." + base64UrlEncode(payload), secret),也就是说,它是通过将前两个字符串加密后生成的一个新字符串。
所以只有拥有同样加密密钥的人,才能通过前两个字符串获得同样的字符串,通过这种方式保证了Token的真实性。
三、认证流程
大概的流程是这样的:
- 认证服务器:用于用户的登录验证和Token的发放。
- 应用服务器:业务数据接口。被保护的API。
- 客户端:一般为APP、小程序等。
认证流程:
- 用户首先通过登录,到认证服务器获取一个Token。
- 在访问应用服务器的API的时候,将获取到的Token放置在请求的Header中。
- 应用服务器验证该Token,通过后返回对应的结果。
说明:这只是示例方案,实际项目中可能有所不同。
- 对于小型项目,可能认证服务和应用服务在一起。本例通过分开的方式来实现,使我们能更好的了解二者之间的认证流程。
- 对于复杂一些的项目,可能存在多个应用服务,用户获取到的Token可以在多个分布式服务中被认证,这也是JWT的优势之一。
关于JWT的文章很多,这里就不做过多介绍了。下面通过实际的例子来看一下 它是如何在ASP.NET Core 中应用的。
四、应用实例
上一节的图:“JWT的认证流程”中涉及到客户端、认证服务器、应用服务器三部分,下面通过示例来对这三部分进行模拟:
- 认证服务器:新建一个WebApi的解决方案,名为FlyLolo.JWT.Server。
- 应用服务器:新建一个WebApi的解决方案,名为FlyLolo.JWT.API。
- 客户端:这里用Fiddler发送请求做测试。
认证服务
首先新建一个ASP.NET Core 的解决方案WebApi的解决方案
将其命名为FlyLolo.JWT.Server。
首先新建一个TokenController用于登录和Token的发放:
[Route("api/[controller]")] public class TokenController : Controller { private ITokenHelper tokenHelper = null; public TokenController(ITokenHelper _tokenHelper) { tokenHelper = _tokenHelper; } [HttpGet] public IActionResult Get(string code, string pwd) { User user = TemporaryData.GetUser(code); if (null != user && user.Password.Equals(pwd)) { return Ok(tokenHelper.CreateToken(user)); } return BadRequest(); } }
它有个名为Get的Action用于接收提交的用户名和密码,并进行验证,验证通过后,调用TokenHelper的CreateToken方法生成Token返回。
这里涉及到了User和TokenHelper两个类。
User相关:
public class User { public string Code { get; set; } public string Name { get; set; } public string Password { get; set; } }
由于只是Demo,User类只含有以上三个字段。在TemporaryData类中做了User的模拟数据
/// <summary> /// 虚拟数据,模拟从数据库或缓存中读取用户 /// </summary> public static class TemporaryData { private static List<User> Users = new List<User>() { new User { Code = "001", Name = "张三", Password = "111111" }, new User { Code = "002", Name = "李四", Password = "222222" } }; public static User GetUser(string code) { return Users.FirstOrDefault(m => m.Code.Equals(code)); } }
这只是模拟数据,实际项目中应该从数据库或者缓存等读取。
TokenHelper:
public class TokenHelper : ITokenHelper { private IOptions<JWTConfig> _options; public TokenHelper(IOptions<JWTConfig> options) { _options = options; } public Token CreateToken(User user) { Claim[] claims = { new Claim(ClaimTypes.NameIdentifier,user.Code),new Claim(ClaimTypes.Name,user.Name) }; return CreateToken(claims); } private Token CreateToken(Claim[] claims) { var now = DateTime.Now;var expires = now.Add(TimeSpan.FromMinutes(_options.Value.AccessTokenExpiresMinutes)); var token = new JwtSecurityToken( issuer: _options.Value.Issuer, audience: _options.Value.Audience, claims: claims, notBefore: now, expires: expires, signingCredentials: new SigningCredentials(new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_options.Value.IssuerSigningKey)), SecurityAlgorithms.HmacSha256)); return new Token { TokenContent = new JwtSecurityTokenHandler().WriteToken(token), Expires = expires }; } }
通过CreateToken方法创建Token,这里有几个关键参数:
- issuer Token发布者
- Audience Token接受者
- expires 过期时间
- IssuerSigningKey 签名秘钥
对应的Token代码如下:
public class Token { public string TokenContent { get; set; } public DateTime Expires { get; set; } }
这样通过TokenHelper的CreateToken方法生成了一个Token返回给了客户端。到现在来看,貌似所有的工作已经完成了。并非如此,我们还需要在Startup文件中做一些设置。
public class Startup {
// 。。。。。。此处省略部分代码
public void ConfigureServices(IServiceCollection services) {
//读取配置信息 services.AddSingleton<ITokenHelper, TokenHelper>(); services.Configure<JWTConfig>(Configuration.GetSection("JWT")); //启用JWT services.AddAuthentication(Options => { Options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; Options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; }). AddJwtBearer(); services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2); } public void Configure(IApplicationBuilder app, IHostingEnvironment env) { if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); }
//启用认证中间件 app.UseAuthentication(); app.UseMvc(); } }
这里用到了配置信息,在appsettings.json中对认证信息做配置如下:
"JWT": { "Issuer": "FlyLolo", "Audience": "TestAudience", "IssuerSigningKey": "FlyLolo1234567890", "AccessTokenExpiresMinutes": "30" }
运行这个项目,并通过Fidder以Get方式访问api/token?code=002&pwd=222222,返回结果如下:
{"tokenContent":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJodHRwOi8vc2NoZW1hcy54bWxzb2FwLm9yZy93cy8
yMDA1LzA1L2lkZW50aXR5L2NsYWltcy9uYW1laWRlbnRpZmllciI6IjAwMiIsImh0dHA6Ly9zY2hlbWFzLnhtbHNvYXAub3JnL
3dzLzIwMDUvMDUvaWRlbnRpdHkvY2xhaW1zL25hbWUiOiLmnY7lm5siLCJuYmYiOjE1NjY3OTg0NzUsImV4cCI6MTU2NjgwMDI
3NSwiaXNzIjoiRmx5TG9sbyIsImF1ZCI6IlRlc3RBdWRpZW5jZSJ9.BVf3gOuW1E9RToqKy8XXp8uIvZKL-lBA-q9fB9QTEZ4",
"expires":"2019-08-26T21:17:55.1183172+08:00"}
客户端登录成功并成功返回了一个Token,认证服务创建完成
应用服务
新建一个WebApi的解决方案,名为FlyLolo.JWT.API。
添加BookController用作业务API。
[Route("api/[controller]")] [Authorize] public class BookController : Controller { // GET: api/<controller> [HttpGet] [AllowAnonymous] public IEnumerable<string> Get() { return new string[] { "ASP", "C#" }; } // POST api/<controller> [HttpPost] public JsonResult Post() { return new JsonResult("Create Book ..."); } }
对此Controller添加了[Authorize]标识,表示此Controller的Action被访问时需要进行认证,而它的名为Get的Action被标识了[AllowAnonymous],表示此Action的访问可以跳过认证。
在Startup文件中配置认证:
public class Startup { // 省略部分代码 public void ConfigureServices(IServiceCollection services) { #region 读取配置 JWTConfig config = new JWTConfig(); Configuration.GetSection("JWT").Bind(config); #endregion #region 启用JWT认证 services.AddAuthentication(options => { options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; }). AddJwtBearer(options => { options.TokenValidationParameters = new TokenValidationParameters { ValidIssuer = config.Issuer, ValidAudience = config.Audience, IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(config.IssuerSigningKey)), ClockSkew = TimeSpan.FromMinutes(1) }; }); #endregion services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2); } // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. public void Configure(IApplicationBuilder app, IHostingEnvironment env) { if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); } app.UseAuthentication(); app.UseMvc(); } }
这里同样用到了配置:
public class JWTConfig { public string Issuer { get; set; } public string Audience { get; set; } public string IssuerSigningKey { get; set; } public int AccessTokenExpiresMinutes { get; set; } }
appsettings.json:
"JWT": { "Issuer": "FlyLolo", "Audience": "TestAudience", "IssuerSigningKey": "FlyLolo1234567890", "AccessTokenExpiresMinutes": "30" }
关于JWT认证,这里通过options.TokenValidationParameters对认证信息做了设置,ValidIssuer、ValidAudience、IssuerSigningKey这三个参数用于验证Token生成的时候填写的Issuer、Audience、IssuerSigningKey,所以值要和生成Token时的设置一致。
ClockSkew默认值为5分钟,它是一个缓冲期,例如Token设置有效期为30分钟,到了30分钟的时候是不会过期的,会有这么个缓冲时间,也就是35分钟才会过期。为了方便测试(不想等太长时间),这里我设置了1分钟。
TokenValidationParameters还有一些其他参数,在它的构造方法中已经做了默认设置,代码如下:
public TokenValidationParameters() { RequireExpirationTime = true; RequireSignedTokens = true; SaveSigninToken = false; ValidateActor = false; ValidateAudience = true; //是否验证接受者 ValidateIssuer = true; //是否验证发布者 ValidateIssuerSigningKey = false; //是否验证秘钥 ValidateLifetime = true; //是否验证过期时间 ValidateTokenReplay = false; }
访问api/book,正常返回了结果
["ASP","C#"]
通过POST方式访问,返回401错误。
这就需要使用获取到的Toke了,如下图方式再次访问
添加了“Authorization: bearer Token内容”这样的Header,可以正常访问了。
至此,简单的JWT认证示例就完成了,代码地址https://github.com/FlyLolo/JWT.Demo/releases/tag/1.0。
这里可能会有个疑问,例如:
1.Token被盗了怎么办?
答: 在启用Https的情况下,Token被放在Header中还是比较安全的。另外Token的有效期不要设置过长。例如可以设置为1小时(微信公众号的网页开发的Token有效期为2小时)。
2. Token到期了如何处理?
答:理论上Token过期应该是跳到登录界面,但这样太不友好了。可以在后台根据Token的过期时间定期去请求新的Token。下一节来演示一下Token的刷新方案。
五、Token的刷新
为了使客户端能够获取到新的Token,对上文的例子进行改造,大概思路如下:
- 用户登录成功的时候,一次性给他两个Token,分别为AccessToken和RefreshToken,AccessToken用于正常请求,也就是上例中原有的Token,RefreshToken作为刷新AccessToken的凭证。
- AccessToken的有效期较短,例如一小时,短一点安全一些。RefreshToken有效期可以设置长一些,例如一天、一周等。
- 当AccessToken即将过期的时候,例如提前5分钟,客户端利用RefreshToken请求指定的API获取新的AccessToken并更新本地存储中的AccessToken。
所以只需要修改FlyLolo.JWT.Server即可。
首先修改Token的返回方案,新增一个Model
public class ComplexToken { public Token AccessToken { get; set; } public Token RefreshToken { get; set; } }
包含AccessToken和RefreshToken,用于用户登录成功后的Token结果返回。
修改 appsettings.json,添加两个配置项:
"RefreshTokenAudience": "RefreshTokenAudience", "RefreshTokenExpiresMinutes": "10080" //60*24*7
RefreshTokenExpiresMinutes用于设置RefreshToken的过期时间,这里设置了7天。RefreshTokenAudience用于设置RefreshToken的接受者,与原Audience值不一致,作用是使RefreshToken不能用于访问应用服务的业务API,而AccessToken不能用于刷新Token。
修改TokenHelper:
public enum TokenType { AccessToken = 1, RefreshToken = 2 } public class TokenHelper : ITokenHelper { private IOptions<JWTConfig> _options; public TokenHelper(IOptions<JWTConfig> options) { _options = options; } public Token CreateAccessToken(User user) { Claim[] claims = new Claim[] { new Claim(ClaimTypes.NameIdentifier, user.Code), new Claim(ClaimTypes.Name, user.Name) }; return CreateToken(claims, TokenType.AccessToken); } public ComplexToken CreateToken(User user) { Claim[] claims = new Claim[] { new Claim(ClaimTypes.NameIdentifier, user.Code), new Claim(ClaimTypes.Name, user.Name) //下面两个Claim用于测试在Token中存储用户的角色信息,对应测试在FlyLolo.JWT.API的两个测试Controller的Put方法,若用不到可删除 , new Claim(ClaimTypes.Role, "TestPutBookRole"), new Claim(ClaimTypes.Role, "TestPutStudentRole") }; return CreateToken(claims); } public ComplexToken CreateToken(Claim[] claims) { return new ComplexToken { AccessToken = CreateToken(claims, TokenType.AccessToken), RefreshToken = CreateToken(claims, TokenType.RefreshToken) }; } /// <summary> /// 用于创建AccessToken和RefreshToken。 /// 这里AccessToken和RefreshToken只是过期时间不同,【实际项目】中二者的claims内容可能会不同。 /// 因为RefreshToken只是用于刷新AccessToken,其内容可以简单一些。 /// 而AccessToken可能会附加一些其他的Claim。 /// </summary> /// <param name="claims"></param> /// <param name="tokenType"></param> /// <returns></returns> private Token CreateToken(Claim[] claims, TokenType tokenType) { var now = DateTime.Now; var expires = now.Add(TimeSpan.FromMinutes(tokenType.Equals(TokenType.AccessToken) ? _options.Value.AccessTokenExpiresMinutes : _options.Value.RefreshTokenExpiresMinutes));//设置不同的过期时间 var token = new JwtSecurityToken( issuer: _options.Value.Issuer, audience: tokenType.Equals(TokenType.AccessToken) ? _options.Value.Audience : _options.Value.RefreshTokenAudience,//设置不同的接受者 claims: claims, notBefore: now, expires: expires, signingCredentials: new SigningCredentials(new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_options.Value.IssuerSigningKey)), SecurityAlgorithms.HmacSha256)); return new Token { TokenContent = new JwtSecurityTokenHandler().WriteToken(token), Expires = expires }; } public Token RefreshToken(ClaimsPrincipal claimsPrincipal) { var code = claimsPrincipal.Claims.FirstOrDefault(m => m.Type.Equals(ClaimTypes.NameIdentifier)); if (null != code ) { return CreateAccessToken(TemporaryData.GetUser(code.Value.ToString())); } else { return null; } } }
在登录后,生成两个Token返回给客户端。在TokenHelper添加了一个RefreshToken方法,用于生成新的AccessToken。对应在TokenController中添加一个名为Post的Action,用于调用这个RefreshToken方法刷新Token
[HttpPost] [Authorize] public IActionResult Post() { return Ok(tokenHelper.RefreshToken(Request.HttpContext.User)); }
这个方法添加了[Authorize]标识,说明调用它需要RefreshToken认证通过。既然启用了认证,那么在Startup文件中需要像上例的业务API一样做JWT的认证配置。
public void ConfigureServices(IServiceCollection services) { #region 读取配置信息 services.AddSingleton<ITokenHelper, TokenHelper>(); services.Configure<JWTConfig>(Configuration.GetSection("JWT")); JWTConfig config = new JWTConfig(); Configuration.GetSection("JWT").Bind(config); #endregion #region 启用JWT services.AddAuthentication(Options => { Options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; Options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; }). AddJwtBearer(options => { options.TokenValidationParameters = new TokenValidationParameters { ValidIssuer = config.Issuer, ValidAudience = config.RefreshTokenAudience, IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(config.IssuerSigningKey)) }; }); #endregion services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2); }
注意这里的ValidAudience被赋值为config.RefreshTokenAudience,和FlyLolo.JWT.API中的不一致,用于防止AccessToken和RefreshToken的混用。
再次访问/api/token?code=002&pwd=222222,会返回两个Token:
{"accessToken":{"tokenContent":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJodHRwOi8vc2NoZW1hcy54bWxzb2FwLm9yZy93cy8y
MDA1LzA1L2lkZW50aXR5L2NsYWltcy9uYW1laWRlbnRpZmllciI6IjAwMiIsImh0dHA6Ly9zY2hlbWFzLnhtbHNvYXAub3JnL3dzLzIwMDUvMDUva
WRlbnRpdHkvY2xhaW1zL25hbWUiOiLmnY7lm5siLCJodHRwOi8vc2NoZW1hcy5taWNyb3NvZnQuY29tL3dzLzIwMDgvMDYvaWRlbnRpdHkvY2xhaW
1zL3JvbGUiOlsiVGVzdFB1dEJvb2tSb2xlIiwiVGVzdFB1dFN0dWRlbnRSb2xlIl0sIm5iZiI6MTU2NjgwNjQ3OSwiZXhwIjoxNTY2ODA4Mjc5LCJ
pc3MiOiJGbHlMb2xvIiwiYXVkIjoiVGVzdEF1ZGllbmNlIn0.wlMorS1V0xP0Fb2MDX7jI7zsgZbb2Do3u78BAkIIwGg",
"expires":"2019-08-26T22:31:19.5312172+08:00"},
"refreshToken":{"tokenContent":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJodHRwOi8vc2NoZW1hcy54bWxzb2FwLm9yZy93cy8y
MDA1LzA1L2lkZW50aXR5L2NsYWltcy9uYW1laWRlbnRpZmllciI6IjAwMiIsImh0dHA6Ly9zY2hlbWFzLnhtbHNvYXAub3JnL3dzLzIwMDUvMDUva
WRlbnRpdHkvY2xhaW1zL25hbWUiOiLmnY7lm5siLCJodHRwOi8vc2NoZW1hcy5taWNyb3NvZnQuY29tL3dzLzIwMDgvMDYvaWRlbnRpdHkvY2xhaW
1zL3JvbGUiOlsiVGVzdFB1dEJvb2tSb2xlIiwiVGVzdFB1dFN0dWRlbnRSb2xlIl0sIm5iZiI6MTU2NjgwNjQ3OSwiZXhwIjoxNTY3NDExMjc5LCJ
pc3MiOiJGbHlMb2xvIiwiYXVkIjoiUmVmcmVzaFRva2VuQXVkaWVuY2UifQ.3EDi6cQBqa39-ywq2EjFGiM8W2KY5l9QAOWaIDi8FnI",
"expires":"2019-09-02T22:01:19.6143038+08:00"}}
可以使用RefreshToken去请求新的AccessToken
测试用AccessToken可以正常访问FlyLolo.JWT.API,用RefreshToken则不可以。
至此,Token的刷新功能改造完成。代码地址:https://github.com/FlyLolo/JWT.Demo/releases/tag/1.1
疑问:RefreshToken有效期那么长,被盗了怎么办,和直接将AccessToken的有效期延长有什么区别?
个人认为:1. RefreshToken不像AccessToken那样在大多数请求中都被使用。2. 应用类的API较多,对应的服务(器)也可能较多,所以泄露的概率更大一些。
出处:https://www.cnblogs.com/FlyLolo/p/ASPNETCore2_26.html
=======================================================================================
ASP.NET Core 2.2 : 二十七. JWT与用户授权(细化到Action)
上一章分享了如何在ASP.NET Core中应用JWT进行用户认证以及Token的刷新,本章继续进行下一步,用户授权。涉及到的例子也以上一章的为基础。(ASP.NET Core 系列目录)
一、概述
首先说一下认证(authentication)与授权(authorization),它们经常在一起工作,所以有时候会分不清楚。并且这两个英文单词长得也像兄弟。举例来说,我刷门禁卡进入公司,门禁【认证】了我是这里的员工,可以进入;但进入公司以后,我并不是所有房间都可以进,比如“机房重地,闲人免进”,我能进入哪些房间,需要公司的【授权】。这就是认证和授权的区别。
ASP.NET Core提倡的是基于声明(Claim)的授权,关于这个Claim,上一章用到过,有如下这样的代码,但没有介绍:
Claim[] claims = new Claim[] { new Claim(ClaimTypes.NameIdentifier, user.Code), new Claim(ClaimTypes.Name, user.Name) };
这是一个声明的集合,它包含了两个 声明,用于保存了用户的唯一ID和用户名。当然我们还可以添加更多的Claim。对应Claim,还有ClaimsIdentity 和ClaimsPrincipal 两个类型。
ClaimsIdentity相当于是一个证件,例如上例的门禁卡;ClaimsPrincipal 则是证件的持有者,也就是我本人;那么对应的Claim就是门禁卡内存储的一些信息,例如证件号、持有人姓名等。
我除了门禁卡还有身份证、银行卡等,也就是说一个ClaimsPrincipal中可以有多个ClaimsIdentity,而一个ClaimsIdentity中可以有多个Claim。ASP.NET Core的授权模型大概就是这样的一个体系。
ASP.NET Core支持多种授权方式,包括兼容之前的角色授权。下面通过几个例子说明一下(例子依然以上一章的代码为基础)。
二、基于角色授权
ASP.NET Core兼容之前的角色授权模式,如何使用呢?由于不是本文的重点,这里只是简要说一下。修改FlyLolo.JWT.Server的TokenHelper临时为张三添加了一个名为“TestPutBookRole”的权限(实际权限来源此处不做展示)。
public ComplexToken CreateToken(User user) { Claim[] claims = new Claim[] { new Claim(ClaimTypes.NameIdentifier, user.Code), new Claim(ClaimTypes.Name, user.Name) }; //下面对code为001的张三添加了一个Claim,用于测试在Token中存储用户的角色信息,对应测试在FlyLolo.JWT.API的BookController的Put方法,若用不到可删除 if (user.Code.Equals("001")) { claims = claims.Append(new Claim(ClaimTypes.Role, "TestPutBookRole")).ToArray(); } return CreateToken(claims); }
修改FlyLolo.JWT.API的BookController,添加了一个Action如下
/// <summary> /// 测试在JWT的token中添加角色,在此验证 见TokenHelper /// </summary> /// <returns></returns> [HttpPut] [Authorize(Roles = "TestPutBookRole")] public JsonResult Put() { return new JsonResult("Put Book ..."); }
访问这个Action,只有用张三登录后获取的Token能正常访问。
三、基于声明授权
对于上例来说,本质上也是基于声明(Claim)的授权,因为张三的"TestPutBookRole"角色也是作为一个Claim添加到证书中的。只不过采用了特定的ClaimTypes.Role。那么是否可以将其他的普通Claim作为授权的依据呢?当然是可以的。
这里涉及到了另一个单词“Policy”,翻译为策略?也就是说,可以把一系列的规则(例如要求姓名为李四,账号为002,国籍为中国等等)组合在一起,形成一个Policy,只有满足这个Policy的才可以被授权访问。
下面我们就新建一个Policy,在Startup的ConfigureServices中添加授权代码:
services.AddAuthorization(options=>options.AddPolicy("Name",policy=> { policy.RequireClaim(ClaimTypes.Name, "张三"); policy.RequireClaim(ClaimTypes.NameIdentifier,"001"); }));
在BookController中添加一个Action如下
[HttpDelete] [Authorize(Policy = "TestPolicy")] public JsonResult Delete() { return new JsonResult("Delete Book ..."); }
可以通过张三和李四的账号测试一下,只有使用张三的账号获取的Token能访问成功。
四、基于策略自定义授权
上面介绍了两种授权方式,现在有个疑问,通过角色授权,只适合一些小型项目,将几个功能通过角色区分开就可以了。
通过声明的方式,目测实际项目中需要在Startup中先声明一系列的Policy,然后在Controller或Action中使用。
这两种方式都感觉不好。例如经常存在这样的需求:一个用户可以有多个角色,每个角色对应多个可访问的API地址(将授权细化到具体的Action)。用户还可以被特殊的授予某个API地址的权限。
这样的需求采用上面的两种方式实现起来都很麻烦,好在ASP.NET Core提供了方便的扩展方式。
1.样例数据
将上面的需求汇总一下,最终可以形成如下形式的数据:
/// <summary> /// 虚拟数据,模拟从数据库或缓存中读取用户相关的权限 /// </summary> public static class TemporaryData { public readonly static List<UserPermissions> UserPermissions = new List<UserPermissions> { new UserPermissions { Code = "001", Permissions = new List<Permission> { new Permission { Code = "A1", Name = "student.create", Url = "/api/student",Method="post" }, new Permission { Code = "A2", Name = "student.delete", Url = "/api/student",Method="delete"} } }, new UserPermissions { Code = "002", Permissions = new List<Permission> { new Permission { Code = "B1", Name = "book.create", Url = "/api/book" ,Method="post"}, new Permission { Code = "B2", Name = "book.delete", Url = "/api/book" ,Method="delete"} } }, }; public static UserPermissions GetUserPermission(string code) { return UserPermissions.FirstOrDefault(m => m.Code.Equals(code)); } }
涉及到的两个类如下:
public class Permission { public string Code { get; set; } public string Name { get; set; } public string Url { get; set; } public string Method { get; set; } } public class UserPermissions { public string Code { get; set; } public List<Permission> Permissions { get; set; } }
2.自定义处理程序
下面就是根据样例数据来制定相应的处理程序了。这涉及到IAuthorizationRequirement和AuthorizationHandler两个内容。
IAuthorizationRequirement是一个空的接口,主要用于提供授权所需要满足的“要求”,或者说是“规则”。AuthorizationHandler则是对请求和“要求”的联合处理。
新建一个PermissionRequirement实现IAuthorizationRequirement接口。
public class PermissionRequirement: IAuthorizationRequirement { public List<UserPermissions> UsePermissionList { get { return TemporaryData.UserPermissions; } } }
很简单的内容。它的“要求”也就是用户的权限列表了,用户的权限列表中包含当前访问的API,则授权通过,否则不通过。
判断逻辑放在新建的PermissionHandler中:
public class PermissionHandler : AuthorizationHandler<PermissionRequirement> { protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, PermissionRequirement requirement) { var code = context.User.Claims.FirstOrDefault(m => m.Type.Equals(ClaimTypes.NameIdentifier)); if (null != code) { UserPermissions userPermissions = requirement.UsePermissionList.FirstOrDefault(m => m.Code.Equals(code.Value.ToString())); var Request = (context.Resource as AuthorizationFilterContext).HttpContext.Request; if (null != userPermissions && userPermissions.Permissions.Any(m => m.Url.ToLower().Equals(Request.Path.Value.ToLower()) && m.Method.ToLower().Equals(Request.Method.ToLower()) )) { context.Succeed(requirement); } else { context.Fail(); } } else { context.Fail(); } return Task.CompletedTask; } }
逻辑很简单不再描述。
3.使用自定义的处理程序
在Startup的ConfigureServices中添加授权代码
services.AddAuthorization(options => options.AddPolicy("Permission", policy => policy.Requirements.Add(new PermissionRequirement()))); services.AddSingleton<IAuthorizationHandler, PermissionHandler>();
将BookController的Delete Action修改一下:
[HttpDelete] //[Authorize(Policy = "TestPolicy")] [Authorize(Policy = "Permission")] public JsonResult Delete() { return new JsonResult("Delete Book ..."); }
测试一下只有李四可以访问这个Action。
出处:https://www.cnblogs.com/FlyLolo/p/ASPNETCore2_27.html
=======================================================================================
ASP.net Core 2.2中Jwt验证的使用方法及在微信小程序上应用
Jwt简单介绍
什么是Jwt
Json web token (JWT), 是为了在网络应用环境间传递声明而执行的一种基于JSON的开放标准((RFC 7519).该token被设计为紧凑且安全的,特别适用于分布式站点的单点登录(SSO)场景。JWT的声明一般被用来在身份提供者和服务提供者间传递被认证的用户身份信息,以便于从资源服务器获取资源,也可以增加一些额外的其它业务逻辑所必须的声明信息,该token也可直接被用于认证,也可被加密。
以上内容来自下面这位大佬的简书,强烈推荐各位小伙伴去拜读一下。
作者:Dearmadman
链接:https://www.jianshu.com/p/576dbf44b2ae
Jwt由3个信息段构成,分别是:头部(Header),载荷(Payload),签证信息(signature)。以上三个部分通过头部中声明的加密算法加密并以’.'连接就成了Jwt字符串。
头部保存的是声明信息,声明这是Jwt和声明加密算法。
载荷保存的是我们服务器给这个用户的信息,一般是分配一个Guid特定识别这个用户以及这个Token的有效期信息。
签证信息保存的是与加密算法相关的信息,在 ASP.Net Core中这些信息可以调用现成的方法直接生成。
Jwt安全吗
首先,从服务器的角度看,Jwt是服务器自己做出来的,用于特定识别某个用户的登录令牌,也就是说不管是谁,只要能截取到这个登录令牌就能伪造请求。
从客户端的角度来说,这个登录令牌只是一段字符串,在请求非公共接口时在Http请求的Header中带上就完事了,也不用去解析获取令牌里面的载荷信息。除非客户端也有Jwt中的签证信息,也能自己生成一个合法的令牌,不过在客户端中保存签证信息太危险了。
那么,我们能截取到这个登录令牌吗?如果是http请求的话,是能轻易抓取到的,但如果是https请求的话就不简单了,因为https的加密范围是包含请求的Header和Body,只要你不是通过url的形式传递令牌就能被加密到。所以Jwt还是应该配合https一起使用,不过这些都是防君子不防小人,任何东西在网络上传输都不能做到绝对安全。
在 ASP.NET Core中使用Jwt
授权和认证
在上代码之前,有一些概念必须要理解清楚。授权(Authorization)和认证(Authentication)的概念一定要明白。这两个东西都是Auth开头,tion结尾,不仔细看还真挺难区分。
首先,在访问非公开接口前先要经过认证,然后要验证授权。举个例子,认证就好比是登录游戏,但是在游戏里能不能使用VIP特权就要经过授权验证。认证(Authentication)只是说明你是合法用户,但不是每个用户都有权限执行这个操作,这就需要授权(Authorization)。
对于非公共接口,一定要有认证,但不一定要有授权(没有VIP的玩家总不能连刷图都不让吧?)。
三步走代码例子
在 ASP.Net Core 2.2中使用Jwt认证分三步走。
如果不需要自定义授权认证可以忽略第二步。
生成Jwt
首先,我们需要编辑一个登录接口,用户登录成功后就给他一个Jwt令牌,以后访问所有非公共接口都通过这个令牌识别该用户。令牌中包含一个Guid,登录成功后将该Guid和用户信息关联记录起来,后续操作根据Guid获取该用户信息。
[HttpPost("login")]
public IActionResult Login([FromBody]Client user)
{
IActionResult ret = null; // 用户登录验证
if(CheckUser(user))
{
// 验证通过,生成唯一识别码放到Token的Payload(载荷)里面
string guid = Guid.NewGuid().ToString();
List<Claim> payloadList = new List<Claim>();
payloadList.Add(new Claim("Guid", guid));
// payloadList.Add(new Claim("Other", data)); // 根据需要继续添加
Claim[] payload = payloadList.ToArray();
string securityKey = _configuration["SecurityKey"];
// sign the token using a secret key.This secret will be shared between your API and anything that needs to check that the token is legit.
// 读取配置文件中的秘钥
var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(securityKey));
// 设置加密算法(签证信息)
var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
// 把设置填到token里(这里我省略了发布者和使用者的验证)
JwtSecurityToken token = new JwtSecurityToken(
claims: payload, // payload
signingCredentials: creds, // 签证信息
expires: DateTime.Now.AddMinutes(60)); // 过期时间
string tokenStr = new JwtSecurityTokenHandler().WriteToken(token);
// 把该用户和该Token中的guid关联起来,其他接口根据Token中的guid获取用户信息
SetUserGuid(user, guid);
// 回送Token和有效时长
ret = Ok(new { Token = tokenStr, Expire = 60 }); //做回送
}
else
{
ret = BadRequest();
}
return ret;
}
编辑授权策略
这里需要新建两个类,一个是授权验证策略类,一个是该授权验证策略需要的条件类。
有不明白的地方这里推荐查看微软的官方文档。
https://docs.microsoft.com/zh-cn/aspnet/core/security/authorization/iauthorizationpolicyprovider?view=aspnetcore-2.2
说明一下,这里的Client类是自己写的用户类,有用户名和密码什么的。
认证失败之后统一重定向到指定方法,该方法直接调用Forbid()返回403错误码。
/// <summary>
/// 授权验证策略条件(可以理解为验证该授权需要的东西)
/// </summary>
public class PermissionRequirement : IAuthorizationRequirement
{
public bool CheckPermission(Client user)
{
bool ret = true;
// 检查用户权限
// Coding...
return ret;
}
}
/// <summary>
/// 授权验证策略
/// </summary>
public class TokenPolicy : AuthorizationHandler<PermissionRequirement>
{
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, PermissionRequirement requirement)
{
var httpContext = (context.Resource as Microsoft.AspNetCore.Mvc.Filters.AuthorizationFilterContext).HttpContext;
string guid = "";
if (httpContext.User.Identity.IsAuthenticated)
{
var auth = httpContext.AuthenticateAsync().Result.Principal.Claims;
var guidClaim = auth.FirstOrDefault(s => s.Type == "Guid");
if (guidClaim != null)
{
guid = guidClaim.Value;
// 根据Guid获取用户信息(该方法是自己编写的)
if (GetUserByGuid(guid, out Client user))
{
// 验证成功且拥有权限
if(requirement.CheckPermission(user))
{
context.Succeed(requirement);
}
else
{
// 验证成功但权限不足
httpContext.Response.Redirect($"api/identify/forbidden");
}
}
else
{
// 验证成功,但Guid非法
httpContext.Response.Redirect($"api/identify/forbidden");
}
}
else
{
// 验证成功,但没有包含Guid
httpContext.Response.Redirect($"api/identify/forbidden");
}
}
else
{
// 验证失败,没有包含验证信息
httpContext.Response.Redirect($"api/identify/forbidden");
}
return Task.CompletedTask;
}
}
在StartUp中配置
在StartUp类的ConfigureServices方法中注册Jwt认证和我们自定义的授权验证方法。
如果不需要自定义授权验证,可以不调用AddAuthorization方法。
public void ConfigureServices(IServiceCollection services)
{
// 注册自定义的授权验证方法
services.AddAuthorization(options =>
{
options.AddPolicy("Permission", policy => policy.Requirements.Add(new PermissionRequirement()));
})
.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) // 注册Jwt认证
.AddJwtBearer(option =>
{
// 读取配置文件中的秘钥
string securityKey = Config["SecurityKey"];
option.TokenValidationParameters = new Microsoft.IdentityModel.Tokens.TokenValidationParameters
{
ValidateLifetime = true,
ValidateIssuer = false,
ValidateIssuerSigningKey = true,
ValidateAudience = false,
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(securityKey))
};
});
// 不需要自定义授权验证的可以不添加这个单例
services.AddSingleton<IAuthorizationHandler, TokenPolicy>();
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
}
最后在StartUp类Configure方法中添加我们的方法。这里一定要注意先 UseAuthentication() 再 UserMvc()
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseAuthentication();
app.UseMvc();
}
添加特性
以上步骤完事之后我们就可以在需要验证的接口或Controller上添加[Authorize("{授权策略名}")]特性。具有该特性的Controller或者接口在调用前就会自动执行Jwt认证和自定义授权策略的HandleRequirementAsync方法。
在接口方法里用User.Claims.First()方法获取Jwt中包含的载荷(Payload)
[Authorize("Permission")]
[HttpGet("{id}")]
public ActionResult<string> Get(int id)
{
//if (User.HasClaim(s => s.Type == "Guid"))
string guid = User.Claims.First(s => s.Type == "Guid").Value;
// 根据guid获取用户信息继续操作
return guid;
}
没有自定义授权方法的添加[Authorize]特性即可,不用指定授权验证策略。
客户端使用
测试截图
首先是Login方法,用户通过登录验证后获得Jwt令牌和有效时间。
然后我们尝试不用令牌访问带有验证规则的接口
返回了401提示需要登录。
然后我们用刚刚获取到的Jwt令牌,在请求的Header中添加Authorization再访问一次。
这里要注意,Authorization的值要以Bearer开头,空格,再填入刚刚获取到的Jwt令牌,这个是在Jwt验证规则里面默认的。
请求成功。
微信小程序中使用
如果你在Jwt字符串制作时有添加有效期这个东西的话,在小程序中的util.js里面就可能需要一个定时刷新Jwt的方法。
// 定时刷新句柄全局变量
var refreshHandle = 0;
function TokenRefresh(min){
refreshHandle = setInterval(function(){
var app = getApp();
var user = app.globalData.userInfo; //保存在全局变量里的用户信息
wx.request({
url: domain + 'values/login',
method: 'POST',
data: user,
dataType: 'json',
responseType: 'text',
success: function (data) {
var result = data.data;
// 登录成功
if (data.statusCode == 200) {
app.globalData.jwtToken = result.token;
}
else{
// 登录失败
}
}
})
},(min-2) * 60 * 1000); // 这里提前两分钟刷新
}
// 用户登出时停止自动刷新
function StopRefresh(){
clearInterval(refreshHandle);
}
用户登录方法
UserLogin:function(usrName,psd){
var user = {
UserName: usrName,
Password: psd
}
wx.request({
url: domain + 'values/login',
method: 'POST',
data: user,
dataType: 'json',
responseType: 'text',
success: function (data) {
var result = data.data;
// 登录成功
if (data.statusCode == 200) {
// 有效时间
let expire = result.expire;
// 定时刷新Token
Utils.TokenRefresh(expire);
// 记在全局变量里面
getApp().globalData.jwtToken = result.token;
}
else {
// 登录失败
}
},
fail: function () {
// 连接错误
},
complete: function () {}
})
},
访问非公共接口
Test:function(){
let token = getApp().globalData.jwtToken;
// 在Header中带上验证信息
var head = {
'content-type': 'application/json;charset=utf-8',
'Authorization': 'Bearer ' + token
}
wx.request({
url: domain + '/values/66',
method: 'GET',
header: head,
dataType: 'json',
responseType: 'text',
success: function (data) {
// Coding
},
fail:function(){ },
complete:function(){ }
})
}
参考资料
Asp.net Core使用jwt:https://www.jianshu.com/p/294ea94f0087
jwt介绍:https://www.jianshu.com/p/576dbf44b2ae
出处:https://blog.csdn.net/weixin_38138153/article/details/99170994