从零开始搭建前后端分离的NetCore2.2(EF Core CodeFirst+Autofac)+Vue的项目框架之七使用JWT生成Token(个人见解)

  在 上一篇中讲到了在NetCore项目中如何进行全局的请求模型验证,只要在请求模型中加了验证特性,接口使用时只用将数据拿来使用,而不用去关系数据是否符合业务需求。

  这篇中将讲些个人对于JWT的看法和使用,在网上也能找到很多相关资料和如何使用,基本都是直接嵌到  Startup 类中来单独使用。而博主是将jwt当做一个验证方法来使用。使用起来更加方便,并且在做验证时也更加的灵活。

 1.什么是JWT?

  Json web token (JWT), 是为了在网络应用环境间传递声明而执行的一种基于JSON的开放标准((RFC 7519)。JWT的声明一般被用来在身份提供者和服务提供者间传递被认证的用户身份信息,以便于从资源服务器获取资源,也可以增加一些额外的其它业务逻辑所必须的声明信息,该token也可直接被用于认证。

传统的session认证

我们知道,http协议本身是一种无状态的协议,而这就意味着如果用户向我们的应用提供了用户名和密码来进行用户认证,那么下一次请求时,用户还要再一次进行用户认证才行,因为根据http协议,我们并不能知道是哪个用户发出的请求,所以为了让我们的应用能识别是哪个用户发出的请求,我们只能在服务器存储一份用户登录的信息,这份登录信息会在响应时传递给浏览器,告诉其保存为cookie,以便下次请求时发送给我们的应用,这样我们的应用就能识别请求来自哪个用户了,这就是传统的基于session认证。

但是这种基于session的认证使应用本身很难得到扩展,随着不同客户端用户的增加,独立的服务器已无法承载更多的用户,而这时候基于session认证应用的问题就会暴露出来.

基于session认证所显露的问题

Session: 每个用户经过我们的应用认证之后,我们的应用都要在服务端做一次记录,以方便用户下次请求的鉴别,通常而言session都是保存在内存中,而随着认证用户的增多,服务端的开销会明显增大。

扩展性: 用户认证之后,服务端做认证记录,如果认证的记录被保存在内存中的话,这意味着用户下次请求还必须要请求在这台服务器上,这样才能拿到授权的资源,这样在分布式的应用上,相应的限制了负载均衡器的能力。这也意味着限制了应用的扩展能力。

CSRF: 因为是基于cookie来进行用户识别的, cookie如果被截获,用户就会很容易受到跨站请求伪造的攻击。

基于token的鉴权机制

基于token的鉴权机制类似于http协议也是无状态的,它不需要在服务端去保留用户的认证信息或者会话信息。这就意味着基于token认证机制的应用不需要去考虑用户在哪一台服务器登录了,这就为应用的扩展提供了便利。

流程上是这样的:

    • 用户使用用户名密码来请求服务器
    • 服务器进行验证用户的信息
    • 服务器通过验证发送给用户一个token
    • 客户端存储token,并在每次请求时附送上这个token值
    • 服务端验证token值,并返回数据

JWT的构成

第一部分我们称它为头部(header),第二部分我们称其为载荷(payload, 类似于飞机上承载的物品),第三部分是签证(signature).

header

jwt的头部承载两部分信息:

    • 声明类型,这里是jwt
    • 声明加密的算法 通常直接使用 HMAC SHA256

完整的头部就像下面这样的JSON:

{
  ‘typ‘: ‘JWT‘,
  ‘alg‘: ‘HS256‘
}

然后将头部进行base64加密(该加密是可以对称解密的),构成了第一部分.

playload

载荷就是存放有效信息的地方。这个名字像是特指飞机上承载的货品,这些有效信息包含三个部分

    • 标准中注册的声明
    • 公共的声明
    • 私有的声明

标准中注册的声明 (建议但不强制使用) :

    • iss: jwt签发者
    • sub: jwt所面向的用户
    • aud: 接收jwt的一方
    • exp: jwt的过期时间,这个过期时间必须要大于签发时间
    • nbf: 定义在什么时间之前,该jwt都是不可用的.
    • iat: jwt的签发时间
    • jti: jwt的唯一身份标识,主要用来作为一次性token,从而回避重放攻击。

公共的声明 :
公共的声明可以添加任何的信息,一般添加用户的相关信息或其他业务需要的必要信息.但不建议添加敏感信息,因为该部分可以直接base64解码,可以看到里面的信息

signature

jwt的第三部分是一个签证信息,这个签证信息由三部分组成:

    • header (base64后的)
    • payload (base64后的)
    • secret

这个部分需要base64加密后的header和base64加密后的payload使用.连接组成的字符串,然后通过header中声明的加密方式进行加盐secret组合加密,然后就构成了jwt的第三部分。

将这三部分用 . 连接成一个完整的字符串,构成了最终的jwt。

注意:secret是保存在服务器端的,jwt的签发生成也是在服务器端的,secret就是用来进行jwt的签发和jwt的验证,所以,它就是你服务端的私钥,在任何场景都不应该流露出去。一旦客户端得知这个secret, 那就意味着客户端是可以自我签发jwt了。

2.如何将JWT 脱离出来生成与验证?

  在任意类库(建议放在公用类中)的NuGet包管理中添加: System.IdentityModel.Tokens.Jwt  然后添加  TokenManager 类

    /// <summary>
    /// token管理类
    /// </summary>
    public class TokenManager
    {
        //私有字段建议放到配置文件中
        /// <summary>
        /// 秘钥  4的倍数  长度大于等于24
        /// </summary>
        private static string _secret = "levy0102030405060708asdf";
        /// <summary>
        /// 发布者
        /// </summary>
        private static string _issuer = "levy";

        /// <summary>
        /// 生成token
        /// </summary>
        /// <param name="tokenStr">需要签名的数据  </param>
        /// <param name="expireHour">默认3天过期</param>
        /// <returns>返回token字符串</returns>
        public static string GenerateToken(string tokenStr, int expireHour = 3 * 24) //3天过期
        {
            var key1 = new SymmetricSecurityKey(Convert.FromBase64String(_secret));
            var cred = new SigningCredentials(key1, SecurityAlgorithms.HmacSha256);
            var claims = new[]
            {
                new Claim("sid",tokenStr),
                //new Claim(ClaimTypes.Name,name), //示例  可使用ClaimTypes中的类型
            };
            var token = new JwtSecurityToken(
                issuer: _issuer,//签发者
                notBefore: DateTime.Now,//token不能早于这个时间使用
                expires: DateTime.Now.AddHours(expireHour),//添加过期时间
                claims: claims,//签名数据
                signingCredentials: cred//签名
                );
            //解决一个不知什么问题的PII什么异常
            IdentityModelEventSource.ShowPII = true;
            return new JwtSecurityTokenHandler().WriteToken(token);
        }

        /// <summary>
        /// 得到Token中的验证消息
        /// </summary>
        /// <param name="token"></param>
        /// <param name="dateTime"></param>
        /// <returns></returns>
        public static string ValidateToken(string token, out DateTime dateTime)
        {
            dateTime = DateTime.Now;
            var principal = GetPrincipal(token, out dateTime);

            if (principal == null)
                return default(string);

            ClaimsIdentity identity = null;
            try
            {
                identity = (ClaimsIdentity)principal.Identity;
            }
            catch (NullReferenceException)
            {
                return null;
            }
            //identity.FindFirst(ClaimTypes.Name).Value;
            return identity.FindFirst("sid").Value;
        }

        /// <summary>
        /// 从Token中得到ClaimsPrincipal对象
        /// </summary>
        /// <param name="token"></param>
        /// <returns></returns>
        private static ClaimsPrincipal GetPrincipal(string token, out DateTime dateTime)
        {
            try
            {
                dateTime = DateTime.Now;
                var tokenHandler = new JwtSecurityTokenHandler();
                var jwtToken = (JwtSecurityToken)tokenHandler.ReadToken(token);

                if (jwtToken == null)
                    return null;

                var key = Convert.FromBase64String(_secret);

                var parameters = new TokenValidationParameters()
                {
                    RequireExpirationTime = true,
                    ValidateIssuer = true,//验证创建该令牌的发布者
                    ValidateLifetime = true,//检查令牌是否未过期,以及发行者的签名密钥是否有效
                    ValidateAudience = false,//确保令牌的接收者有权接收它
                    IssuerSigningKey = new SymmetricSecurityKey(key),
                    ValidIssuer = _issuer//验证创建该令牌的发布者
                };
                //验证token
                var principal = tokenHandler.ValidateToken(token, parameters, out var securityToken);
                //若开始时间大于当前时间 或结束时间小于当前时间 则返回空
                if (securityToken.ValidFrom.ToLocalTime() > DateTime.Now || securityToken.ValidTo.ToLocalTime() < DateTime.Now)
                {
                    dateTime = DateTime.Now;
                    return null;
                }
                dateTime = securityToken.ValidTo.ToLocalTime();//返回Token结束时间
                return principal;
            }
            catch (Exception e)
            {
                dateTime = DateTime.Now;
                LogHelper.Logger.Fatal(e, "Token验证失败");
                return null;
            }
        }
    }

  再到控制器中添加测试方法

        [HttpGet]
        [Route("testtoken")]
        public ActionResult TestToken()
        {
            var token = TokenManager.GenerateToken("测试token的生成");
            Response.Headers["token"] = token;
            Response.Headers["Access-Control-Expose-Headers"] = "token";//一定要添加这一句  不然前端是取不到token字段的值的!更别提存store了。
            return Succeed(token);
        }

在这里必须得提的地方是  若是前后端分离的项目,由于存在跨域问题,必须得在返回header中多添加一个字段 Access-Control-Expose-Headers 该字段对应的值为前端需要取得字段的集合,以英文逗号分隔。

原因:在跨域访问时,XMLHttpRequest对象的getResponseHeader()方法只能拿到一些最基本的响应头,Cache-Control、Content-Language、Content-Type、Expires、Last-Modified、Pragma,如果要访问其他头,则需要服务器设置本响应头。Access-Control-Expose-Headers 头让服务器把允许浏览器访问的头放入白名单。不然容易出现前后端开发人员撕逼哦~

测试结果截图:

能看到数据能返回出来,在调试中也能看到。接着拿这个去访问接口。博主这里只是示例,具体业务视情况而定。

接下来我们拿生成的token去访问验证下是否能成功,在验证token的时候我们可以顺带看下token是否即将过期,若快要过期了就取一个新的token。当然这里有一个问题就是之前的token还可以使用。这里可以用其它手段来规避。如缓存过期token判断等。

        [HttpPost]
        [Route("validtoken")]
        public ActionResult ValidToken([FromHeader]string token)
        {
            var str = TokenManager.ValidateToken(token, out DateTime date);
            if (!string.IsNullOrEmpty(str) || date > DateTime.Now)
            {
                //当token过期时间小于五小时,更新token并重新返回新的token
                if (date.AddHours(-5) > DateTime.Now) return Succeed($"Token字符串:{str},过期时间:{date}");
                var nToken = TokenManager.GenerateToken(str);
                Response.Headers["token"] = nToken;
                token = nToken;
                Response.Headers["Access-Control-Expose-Headers"] = "token";
            }
            else
            {
                return Fail(101, "未取得授权信息");
            }
            return Succeed($"Token字符串:{str},过期时间:{DateTime.Now.AddHours(3 * 24)}");
        }

  测试结果:

 3.问题与讨论~

  JWT也存在很多疑问的地方,比如 1.被盗取了怎么办?2.用户处于失控状态下?等等问题。

  建议:1.不在payload部分存放敏感信息,且尽可能使用https方式,防止被盗的可能性,且提醒用户有风险,不要在公共地方登陆。提供给用户token保存时间选择,若未选择长期保存则只存sessionStorage ,选了则存localStorage。

       2.后端用户信息一般存于缓存之中,一般用户使用时间不会太长,所以后端缓存设置时间短(如2小时),当后端缓存过期了就根据payload部分数据来取用户信息存缓存, 用户信息添加稳定状态值来判断是否可用。

       3.为解决2要使用payload部分的数据,为防止泄露,可进行AES 进行加密处理,当需要使用时取出在解密使用。

  以上属个人想法。有什么问题欢迎提出,共同讨论。

  部分文字描述参考于:

  https://www.jianshu.com/p/576dbf44b2ae

  

  

  在下一篇中将介绍如何在NetCore中如何使用 MemoryCache 和 Redis 来做缓存不常变动数据,提高响应速度~~

  有需要源码的在下方评论或私信~给我的SVN访客账户密码下载,代码未放在GitHub上。

原文地址:https://www.cnblogs.com/levywang/p/coreframe_7.html

时间: 2024-10-08 06:51:32

从零开始搭建前后端分离的NetCore2.2(EF Core CodeFirst+Autofac)+Vue的项目框架之七使用JWT生成Token(个人见解)的相关文章

从零开始搭建前后端分离的NetCore(EF Core CodeFirst+Au)+Vue的项目框架之二autofac解耦

在 上一篇 中将项目的基本骨架搭起来能正常跑通,这一篇将讲到,如何通过autofac将DbContext和model进行解耦,只用添加model,而不用在DbContext中添加DbSet. 在这里就不详细讲autofac是干什么用的了,简单说下autofac. 1.autofac可替换net core自带的DI IOC,用来扩展. 2.autofac可提供Aop,具体实现在博客园有很多示例. 3.autofac的几个生命周期用法:InstancePerDependency 每次都创建一个对象

利用grunt-contrib-connect和grunt-connect-proxy搭建前后端分离的开发环境

前后端分离这个词一点都不新鲜,完全的前后端分离在岗位协作方面,前端不写任何后台,后台不写任何页面,双方通过接口传递数据完成软件的各个功能实现.此种情况下,前后端的项目都独立开发和独立部署,在开发期间有2个问题不可避免:第一是前端调用后台接口时的跨域问题(因为前后端分开部署):第二是前端脱离后台服务后无法独立运行.本文总结最近一个项目的工作经验,介绍利用grunt-contrib-connect和grunt-connect-proxy搭建前后端分离的开发环境的实践过程,希望能对你有所帮助. 注:

从壹开始前后端分离 [ Vue2.0+.NET Core2.1] 十六 ║ Vue前篇:ES6初体验 &amp; 模块化

缘起 昨天说到了<从壹开始前后端分离 [ Vue2.0+.NET Core2.1] 十五 ║ Vue前篇:JS对象&字面量&this>,通过总体来看,好像大家对这一块不是很感兴趣,嗯~~这一块确实挺枯燥的,不能直接拿来代码跑一下那种,不过还是得说下去,继续加油吧!如果大家对昨天的小demo练习的话,相信现在已经对JS的面向对象写法很熟悉了,如果嵌套字面量定义函数,如何使用this关键字指向.今天呢,主要说一下ES6中的一些特性技巧,然后简单说一下模块化的问题,好啦,开始今天的讲

从壹开始前后端分离 [ Vue2.0+.NET Core2.1] 二十三║Vue实战:Vuex 其实很简单

系列教程一目录:.netcore+vue 前后端分离 系列教程二目录:DDD领域驱动设计 系列教程三目录:Nuxt.js TiBug系统 系列教程四目录:VueAdmin 后台管理系统 系列教程五目录:IdentityServer4 授权服务器 本文梯子 前言 零.今天要完成实战1中的红色部分 一.常见的 Vue 表单提交是如何设计的? 1.表单.按钮等在一个组件内 2.按钮在父组件.表单在单独的子组件内 二.通过 $emit 修改父组件数据 1.在原来代码里 About.vue 修改成 For

ASP.NET Core 实战:使用 ASP.NET Core Web API 和 Vue.js 搭建前后端分离项目

 一.前言 这几年前端的发展速度就像坐上了火箭,各种的框架一个接一个的出现,需要学习的东西越来越多,分工也越来越细,作为一个 .NET Web 程序猿,多了解了解行业的发展,让自己扩展出新的技能树,对自己的职业发展还是很有帮助的.毕竟,现在都快到9102年了,如果你还是只会 Web Form,或许还是能找到很多的工作机会,可是,这真的不再适应未来的发展了.如果你准备继续在 .NET 平台下进行开发,适时开始拥抱开源,拥抱 ASP.NET Core,即使,现在工作中可能用不到. 雪崩发生时,没有一

从壹开始前后端分离 [ Vue2.0+.NET Core2.1] 二十一║Vue实战:开发环境搭建【详细版】

系列教程一目录:.netcore+vue 前后端分离 系列教程二目录:DDD领域驱动设计 系列教程三目录:Nuxt.js TiBug系统 系列教程四目录:VueAdmin 后台管理系统 系列教程五目录:IdentityServer4 授权服务器 本文梯子 缘起 零.今天要完成左下角红色的部分 A.Vue 常见的IDE —— 我是开发工具,干活的都是我 1.VsCode 2.Webstorm 3.Atom B.安装Nodejs环境 —— 我是运行环境,没我不行 C.安装 npm / cnpm ——

node+mysql+vue 搭建前后端分离基础框架

话不多说直接上 1.安装node,通过express.生成node项目.搭建链接 http://www.expressjs.com.cn/starter/generator.html: 2安装vue 前端项目. 3.配置vue 跨域问题,找到vue里面config里的index文件.配置proxyTable 原文地址:https://www.cnblogs.com/chen527/p/11442588.html

关于使用vue搭建前后端分离的项目,部署过程遇到的问题

1.首先应该有三个端口号:app前端.网页前端.后台接口  后台接口有很多,但是会映射到zuul上,保证他的端口号是对外开放的,其他不被占用就行 2.pc前端访问后台路径 他的baseUrl是每一次请求的基础路径,需要写服务器后台对外开放的ip和端口号,tomcat部署前端服务也得需要另一个端口号 3.然后发现他的静态资源都访问不到,可以看我的另一篇博客  https://www.cnblogs.com/vv-lilu/p/11106894.html 4.使用vue-lic3搭建的手机端框架,静

[原创]基于VueJs的前后端分离框架搭建之完全攻略

首先请原谅本文标题取的有点大,但并非为了哗众取宠.本文取这个标题主要有3个原因,这也是写作本文的初衷: (1)目前国内几乎搜索不到全面讲解如何搭建前后端分离框架的文章,讲前后端分离框架思想的就更少了,而笔者希望在本文中能够全面.详细地阐述我们团队在前后端分离的摸索中所得到的搭建思路.最佳实践以及架构思想: (2)我们团队所搭建的前后端分离框架,并非只是将网上传播的知识碎片简单拼装,而是一开始就从全局出发,按照整个系统对前后端分离框架的最高期望进行设计,到目前为止,可以说我们的框架完全实现了对我们