在线时间:8:00-16:00
迪恩网络APP
随时随地掌握行业动态
扫描二维码
关注迪恩网络微信公众号
本文将通过实际的例子来演示如何在ASP.NET Core中应用JWT进行用户认证以及Token的刷新方案 一、什么是JWT?JWT(json web token)基于开放标准(RFC 7519),是一种无状态的分布式的身份验证方式,主要用于在网络应用环境间安全地传递声明。它是基于JSON的,所以它也像json一样可以在.Net、JAVA、JavaScript,、PHP等多种语言使用。 为什么要使用JWT?传统的Web应用一般采用Cookies+Session来进行认证。但对于目前越来越多的App、小程序等应用来说,它们对应的服务端一般都是RestFul 类型的无状态的API,再采用这样的的认证方式就不是很方便了。而JWT这种无状态的分布式的身份验证方式恰好符合这样的需求。 二、JWT的组成:JWT是什么样子的呢?它就是下面这样的一段字符串:
它是由三段“乱码”字符串通过两个“.”连接在一起组成。官网https://jwt.io/提供了它的验证方式 它的三个字符串分别对应了上图右侧的Header、Payload和Signature三部分。 HeaderHeader: { "alg": "HS256", "typ": "JWT" } 标识加密方式为HS256,Token类型为JWT, 这段JSON通过Base64Url编码形成上例的第一个字符串 PayloadPayload是JWT用于信息存储部分,其中包含了许多种的声明(claims)。 可以自定义多个声明添加到Payload中,系统也提供了一些默认的类型
这部分通过Base64Url编码生成第二个字符串。 SignatureSignature是用于Token的验证。它的值类似这样的表达式:Signature = HMACSHA256( base64UrlEncode(header) + "." + base64UrlEncode(payload), secret),也就是说,它是通过将前两个字符串加密后生成的一个新字符串。 所以只有拥有同样加密密钥的人,才能通过前两个字符串获得同样的字符串,通过这种方式保证了Token的真实性。 三、认证流程大概的流程是这样的:
认证流程:
关于JWT的文章很多,这里就不做过多介绍了。下面通过实际的例子来看一下 它是如何在ASP.NET Core 中应用的。 四、应用实例上一节的图:“JWT的认证流程”中涉及到客户端、认证服务器、应用服务器三部分,下面通过示例来对这三部分进行模拟:
认证服务首先新建一个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,这里有几个关键参数:
对应的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,返回结果如下:
客户端登录成功并成功返回了一个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,正常返回了结果
通过POST方式访问,返回401错误。 这就需要使用获取到的Toke了,如下图方式再次访问 添加了“Authorization: bearer Token内容”这样的Header,可以正常访问了。 至此,简单的JWT认证示例就完成了,代码地址https://github.com/FlyLolo/JWT.Demo/releases/tag/1.0。 这里可能会有个疑问,例如:
五、Token的刷新为了使客户端能够获取到新的Token,对上文的例子进行改造,大概思路如下:
所以只需要修改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:
可以使用RefreshToken去请求新的AccessToken 测试用AccessToken可以正常访问FlyLolo.JWT.API,用RefreshToken则不可以。 至此,Token的刷新功能改造完成。代码地址:https://github.com/FlyLolo/JWT.Demo/releases/tag/1.1 疑问:RefreshToken有效期那么长,被盗了怎么办,和直接将AccessToken的有效期延长有什么区别? 个人认为:
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持极客世界。 |
请发表评论