ASP.NET Core应用JWT进行用户认证及Token的刷新方案

2022-04-16 00:20:34
目录
一、什么是JWT?为什么要使用JWT?二、JWT的组成:HeaderPayloadSignature三、认证流程四、应用实例认证服务User相关:TokenHelper:应用服务五、Token的刷新

本文将通过实际的例子来演示如何在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是什么样子的呢?它就是下面这样的一段字符串:

{ 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.eyJodHRwOi8vc2NoZW1hcy54bWxzb2FwLm9yZy93cy8yMDA1LzA1L2lkZW50aXR5L2NsYWltcy9uYW1laWRlbnRpZmllciI6IjAwMiIsImh0dHA6Ly9zY2hlbWFzLnhtbHNvYXAub3JnL3dzLzIwMDUvMDUvaWRlbnRpdHkvY2xhaW1zL25hbWUiOiLmnY7lm5siLCJuYmYiOjE1NjY3OTg0NzUsImV4cCI6MTU2NjgwMDI3NSwiaXNzIjoiRmx5TG9sbyIsImF1ZCI6IlRlc3RBdWRpZW5jZSJ9.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了,如下图方式再次访问

ASP.NETCore应用JWT进行用户认证及Token的刷新方案

添加了“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 www.easck.comRefreshToken(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.eyJodHRwOi8vc2NoZW1hcy54bWxzb2FwLm9yZy93cy8yMDA1LzA1L2lkZW50aXR5L2NsYWltcy9uYW1laWRlbnRpZmllciI6IjAwMiIsImh0dHA6Ly9zY2hlbWFzLnhtbHNvYXAub3JnL3dzLzIwMDUvMDUvaWRlbnRpdHkvY2xhaW1zL25hbWUiOiLmnY7lm5siLCJodHRwOi8vc2NoZW1hcy5taWNyb3NvZnQuY29tL3dzLzIwMDgvMDYvaWRlbnRpdHkvY2xhaW1zL3JvbGUiOlsiVGVzdFB1dEJvb2tSb2xlIiwiVGVzdFB1dFN0dWRlbnRSb2xlIl0sIm5iZiI6MTU2NjgwNjQ3OSwiZXhwIjoxNTY2ODA4Mjc5LCJpc3MiOiJGbHlMb2xvIiwiYXVkIjoiVGVzdEF1ZGllbmNlIn0.wlMorS1V0xP0Fb2MDX7jI7zsgZbb2Do3u78BAkIIwGg","expires":"2019-08-26T22:31:19.5312172+08:00"},

"refreshToken":{"tokenContent":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJodHRwOi8vc2NoZW1hcy54bWxzb2FwLm9yZy93cy8yMDA1LzA1L2lkZW50aXR5L2NsYWltcy9uYW1laWRlbnRpZmllciI6IjAwMiIsImh0dHA6Ly9zY2hlbWFzLnhtbHNvYXAub3JnL3dzLzIwMDUvMDUvaWRlbnRpdHkvY2xhaW1zL25hbWUiOiLmnY7lm5siLCJodHRwOi8vc2NoZW1hcy5taWNyb3NvZnQuY29tL3dzLzIwMDgvMDYvaWRlbnRpdHkvY2xhaW1zL3JvbGUiOlsiVGVzdFB1dEJvb2tSb2xlIiwiVGVzdFB1dFN0dWRlbnRSb2xlIl0sIm5iZiI6MTU2NjgwNjQ3OSwiZXhwIjoxNTY3NDExMjc5LCJpc3MiOiJGbHlMb2xvIiwiYXVkIjoiUmVmcmVzaFRva2VuQXVkaWVuY2UifQ.3EDi6cQBqa39-ywq2EjFGiM8W2KY5l9QAOWaIDi8FnI","expires":"2019-09-02T22:01:19.6143038+08:00"}}

可以使用RefreshToken去请求新的AccessToken

ASP.NETCore应用JWT进行用户认证及Token的刷新方案

测试用AccessToken可以正常访问FlyLolo.JWT.API,用RefreshToken则不可以。

至此,Token的刷新功能改造完成。代码地址:https://github.com/FlyLolo/JWT.Demo/releases/tag/1.1

疑问:RefreshToken有效期那么长,被盗了怎么办,和直接将AccessToken的有效期延长有什么区别?

个人认为:

1. RefreshToken不像AccessToken那样在大多数请求中都被使用。 2. 应用类的API较多,对应的服务(器)也可能较多,所以泄露的概率更大一些。

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持我们。