zoukankan      html  css  js  c++  java
  • ASP.NET CORE中使用Jwt做身份认证(AccessToken、RefreshToken)

    1. 认证服务器:新建一个WebApi的解决方案,名为FlyLolo.JWT.Server。
    2. 应用服务器:新建一个WebApi的解决方案,名为FlyLolo.JWT.API。
    3. 客户端:这里用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,这里有几个关键参数:

    1. issuer            Token发布者
    2. Audience      Token接受者
    3. expires          过期时间
    4. 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,对上文的例子进行改造,大概思路如下:

    1. 用户登录成功的时候,一次性给他两个Token,分别为AccessToken和RefreshToken,AccessToken用于正常请求,也就是上例中原有的Token,RefreshToken作为刷新AccessToken的凭证。
    2. AccessToken的有效期较短,例如一小时,短一点安全一些。RefreshToken有效期可以设置长一些,例如一天、一周等。
    3. 当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

    https://www.cnblogs.com/FlyLolo/p/ASPNETCore2_27.html

  • 相关阅读:
    2019左其盛好书榜,没见过更好的榜单(截至4月30日)
    3星|菲利普·科特勒《我的营销人生》:大师一生经历、成就、著作回顾
    3星|樊登《低风险创业》:创业相关的书+樊登个人创业经验
    OKR能解决996吗?德鲁克怎么看?
    《中国合伙人》背后的故事:4星|俞敏洪《我曾走在崩溃的边缘》
    3星|路江涌《共演战略画布》:PPT技巧级别的创新,缺实际分析案例
    C# 通用数据库配置界面,微软原生DLL重整合
    SoapUI、Jmeter、Postman三种接口测试工具的比较分析
    用VS制作的windows服务安装包 安装完后如何让服务自动启动
    POI使用详解
  • 原文地址:https://www.cnblogs.com/fanfan-90/p/12747382.html
Copyright © 2011-2022 走看看