JWT认证

JWT认证

 

 


1.什么是JWT Token

  JWT(Json Web Tokens) 是一个开放标准(RFC 7519),它定义了一种简洁,自包含,JSON 对象形式的安全传递信息的方法。JWT经常使用在 Web 应用或者移动应用上,Token是令牌的意思,表示只有拿着令牌才具备一些权限。JWT的声明(Claim)通常被用来在身份提供者和服务提供者间传递身份验证信息,也能够增长一些额外的其它业务逻辑所必须的声明信息。git

JWT的使用场景

一次性验证github

好比用户注册后须要发一封邮件让其激活帐户,一般邮件中须要有一个连接,这个连接须要具有如下的特性:可以标识用户,该连接具备时效性(一般只容许几小时以内激活),不能被篡改以激活其余可能的帐户…这种场景就和 jwt 的特性很是贴近,jwt 的 payload 中固定的参数:iss 签发者和 exp 过时时间正是为其作准备的。redis

restful api 的无状态认证算法

使用 jwt 来作 restful api 的身份认证也是值得推崇的一种使用方案。客户端和服务端共享 secret;过时时间由服务端校验,客户端定时刷新json

2.JWT的组成

使用JWT token认证前咱们先了解下JWT的组成部分。JWT通过加密处理与校验处理的字符串,形式 A.B.Capi

A由JWT头部信息header加密获得
B由JWT用到的身份验证信息json数据加密获得
C由A和B加密获得,是校验部分

怎么计算A?

header格式为:
{
    "typ": "JWT",
    "alg": "HS256" 
}

它就是一个json串,两个字段是必须的,不能多也不能少。alg字段指定了生成C的算法,默认值是HS256,将header用base64加密,获得A。
补充:Base64是一种表示二进制数据的方法。因为2的6次方等于64,因此每6个比特为一个单元,对应某一个可打印字符。三个字节有24个比特,对应于4个Base64单元,即3个字节须要用4个Base64可打印字符来表示。浏览器

怎样计算B?

  根据JWT claim set[用base64]加密获得的。claim set是一个json数据,是代表用户身份的数据,可自行指定字段很灵活,也有固定字段表示特定含义(但不必定要包含特定字段,只是推荐)。
一些字段的含义:缓存

复制代码
    {
     "iss" :"http://example.org", //非必须。issuer 请求实体,能够是发起请求的用户的信息,也但是jwt的签发者。   "iat" : 1356999524, //非必须。issued at。 token建立时间,unix时间戳格式   "exp" :"1548333419", //非必须。expire 指定token的生命周期。unix时间戳格式   "aud" : "http://example.com", //非必须。接收该JWT的一方。   "sub" : "jrocket@example.com", //非必须。该JWT所面向的用户   "nbf" : 1357000000, //非必须。not before。若是当前时间在nbf里的时间以前,则Token不被接受;通常都会留一些余地,好比几分钟。   "jti" :'222we', //非必须。JWT ID。针对当前token的惟一标识   "GivenName" : "Jonny", // 自定义字段   "Surname" : "Rocket", // 自定义字段   "Email" : "jrocket@example.com", // 自定义字段   "Role" : ["Manager", "Project Administrator"] // 自定义字段
  }
复制代码

自定义字段的key是一个string,value是一个json数据。将claim set经过Base64加密后获得B,学名payload(载荷)安全

怎样计算C?

A.B使用HS256加密(实际上是用header中指定的算法),固然加密过程当中还须要密钥(secret,自行指定的一个字符串)。加密获得C,学名signature,其实就是一个字符串。

3.JWT的工做过程

借鉴于:https://www.cnblogs.com/lonelyxmas/p/8024006.html

下面咱们从一个实例来看如何运用JWT机制实现认证:

1.登陆

  • 第一次认证:第一次登陆,用户从浏览器输入用户名/密码,提交后到服务器的登陆处理的Action层(Login Action);
  • Login Action调用认证服务进行用户名密码认证,若是认证经过,Login Action层调用用户信息服务获取用户信息(包括完整的用户信息及对应权限信息);
  • 返回用户信息后,Login Action从配置文件中获取Token签名生成的秘钥信息,进行Token的生成;
  • 生成Token的过程当中能够调用第三方的JWT Lib生成签名后的JWT数据;
  • 完成JWT数据签名后,将其设置到COOKIE对象中,并重定向到首页,完成登陆过程;

2.请求认证

  基于Token的认证机制会在每一次请求中都带上完成签名的Token信息,这个Token信息可能在Cookie中,也可能在HTTP的Authorization头中;

  • 客户端(APP客户端或浏览器)经过GET或POST请求访问资源(页面或调用API);
  • 认证服务做为一个Middleware HOOK 对请求进行拦截,首先在cookie中查找Token信息,若是没有找到,则在HTTP Authorization Head中查找;
  • 若是找到Token信息,则根据配置文件中的签名加密秘钥,调用JWT Lib对Token信息进行解密和解码;
  • 完成解码并验证签名经过后,对Token中的exp、nbf、aud等信息进行验证;
  • 所有经过后,根据获取的用户的角色权限信息,进行对请求的资源的权限逻辑判断;
  • 若是权限逻辑判断经过则经过验证,开始执行功能代码;不然则返回HTTP 401;

3.JWT.Net的使用

使用JWT.Net前要首先经过Nuge(git地址:https://github.com/jwt-dotnet/jwt)t获取JWT.Net包,以下:

添加了JWT.Net包后就可使用JWT了,这里封装了一个简单的JWTHelper,代码以下:

复制代码
    public class JWTHelper
    {
        static IJwtAlgorithm algorithm = new HMACSHA256Algorithm();//HMACSHA256加密
        static IJsonSerializer serializer = new JsonNetSerializer();//序列化和反序列
        static IBase64UrlEncoder urlEncoder = new JwtBase64UrlEncoder();//Base64编解码
        static IDateTimeProvider provider = new UtcDateTimeProvider();//UTC时间获取
        #region /////生成JWT
        public static string GetJWT(string secret, Dictionary<string, object> payload)
        {
            IJwtEncoder encoder = new JwtEncoder(algorithm, serializer, urlEncoder);
            return encoder.Encode(payload, secret);
        }
        #endregion


        #region /////验证JWT
        public static bool ValidateJwt(string secret, string token, out string payload,out string message)
        {
            bool isValidted = false;
            payload = "";
            try
            {
                IJwtValidator validator = new JwtValidator(serializer, provider);//用于验证JWT的类
                IJwtDecoder decoder = new JwtDecoder(serializer, validator, urlEncoder);//用于解析JWT的类
                payload = decoder.Decode(token, secret, verify: true);//verify:true表示解析JWT时进行验证,该方法会自动调用validator的Validate()方法,不知足验证会抛出异常,所以咱们不用写验证的方法
                isValidted = true;
                message = "验证成功";
            }
            catch (TokenExpiredException)//若是当前时间大于负载中的过时时间(负荷中的exp),引起Token过时异常
            {

                message = "Token已通过期了!";
            }
            catch (SignatureVerificationException)//若是签名不匹配,引起签名验证异常
            {
                message = "Token签名不正确!";
            }
            return isValidted;
        } 
        #endregion
    }
复制代码

 咱们在一个控制台程序中简单展现一下JWT.Net的用法,代码以下:

复制代码
        static void Main(string[] args)
        {
            //服务端的秘钥,通常放在配置文件中
            const string secret = "GQDstcKsx0NHjPOuXOYg5MbeJ1XT0uFiwDVvVBrk";
            //负荷(payload)
            var payload = new Dictionary<string, object>
            {
                { "claim1", 0 },
                { "testStr", "hello" },
                {"testObj" ,new { name="111"} },
                { "exp", DateTimeOffset.UtcNow.AddSeconds(2).ToUnixTimeSeconds() }
            };

            Console.WriteLine("生成JWT--------------------------------------------------------------");
            Console.WriteLine();
            string token = JWTHelper.GetJWT(secret, payload);
            Console.WriteLine($"生成的JWT是:{token}");
            Console.WriteLine();
            Console.WriteLine();
            Console.WriteLine();

            Console.WriteLine("校验JWT--------------------------------------------------------------");
            Console.WriteLine();
            string message;//解析的消息
            string curPayload;//解析获取的负载
            if (JWTHelper.ValidateJwt(secret,token,out curPayload,out message))
            {
                Console.WriteLine($"解析获取的负载是:{curPayload}");
            }
            Console.WriteLine(message);
        }
    }
复制代码

运行结果如图所示:

4.一些须要注意的问题

这些问题摘自https://blog.csdn.net/qq_28165595/article/details/80214994

1.jwt token泄露了怎么办? 

  前面的文章下有很多人留言提到这个问题,我则认为这不是问题。传统的 session+cookie 方案,若是泄露了 sessionId,别人一样能够盗用你的身份。扬汤止沸不如釜底抽薪,不妨来追根溯源一下,什么场景会致使你的 jwt 泄露。 
遵循以下的实践能够尽量保护你的 jwt 不被泄露:使用 https 加密你的应用,返回 jwt 给客户端时设置 httpOnly=true 而且使用 cookie 而不是 LocalStorage 存储 jwt,这样能够防止 XSS 攻击和 CSRF 攻击

2. secret如何设计

  JWT惟一存储在服务端的只有一个 secret,我的认为这个 secret 应该设计成和用户相关的属性,而不是一个全部用户公用的统一值。这样能够有效的避免一些注销和修改密码时遇到的窘境。 
注销和修改密码 

  传统的 session+cookie 方案用户点击注销,服务端清空 session 便可,由于状态保存在服务端。但 jwt 的方案就比较难办了,由于 jwt 是无状态的,服务端经过计算来校验有效性。没有存储起来,因此即便客户端删除了 jwt,可是该 jwt 仍是在有效期内,只不过处于一个游离状态。分析下痛点:注销变得复杂的缘由在于 jwt 的无状态。我提供几个方案,视具体的业务来决定能不能接受。 

  1. 仅仅清空客户端的 cookie,这样用户访问时就不会携带 jwt,服务端就认为用户须要从新登陆。这是一个典型的假注销,对于用户表现出退出的行为,实际上这个时候携带对应的 jwt 依旧能够访问系统。 
  2. 清空或修改服务端的用户对应的 secret,这样在用户注销后,jwt 自己不变,可是因为 secret 不存在或改变,则没法完成校验。这也是为何将 secret 设计成和用户相关的缘由。 
  3. 借助第三方存储本身管理 jwt 的状态,能够以 jwt 为 key,实现去 redis 一类的缓存中间件中去校验存在性。方案设计并不难,可是引入 redis 以后,就把无状态的 jwt 硬生生变成了有状态了,违背了 jwt 的初衷。实际上这个方案和 session 都差很少了。 
修改密码则略微有些不一样,假设号被到了,修改密码(是用户密码,不是 jwt 的 secret)以后,盗号者在原 jwt 有效期以内依旧能够继续访问系统,因此仅仅清空 cookie 天然是不够的,这时,须要强制性的修改 secret。在个人实践中就是这样作的。 

3.续签问题

  续约问题能够说是我抵制使用 jwt 来代替传统 session 的最大缘由,由于 jwt 的设计中我就没有发现它将续签认为是自身的一个特性。传统的 cookie 续签方案通常都是框架自带的,session 有效期 30 分钟,30 分钟内若是有访问,session 有效期被刷新至 30 分钟。而 jwt 自己的 payload 之中也有一个 exp 过时时间参数,来表明一个 jwt 的时效性,而 jwt 想延期这个 exp 就有点身不禁己了,由于 payload 是参与签名的,一旦过时时间被修改,整个 jwt 串就变了,jwt 的特性自然不支持续签! 
若是你必定要使用 jwt 作会话管理(payload 中存储会话信息),也不是没有解决方案,但我的认为都不是很使人满意 
1.每次请求刷新JWT
  JWT修改 payload 中的 exp 后整个 jwt 串就会发生改变,那…就让它变好了,每次请求都返回一个新的 jwt 给客户端。太暴力了,不用我赘述这样作是多么的不优雅,以及带来的性能问题。但,至少这是最简单的解决方案。 
2.只要快要过时的时候刷新JWT
  一个上述方案的改造点是,只在最后的几分钟返回给客户端一个新的 jwt。这样作,触发刷新 jwt 基本就要看运气了,若是用户恰巧在最后几分钟访问了服务器,触发了刷新,万事大吉;若是用户连续操做了 27 分钟,只有最后的 3 分钟没有操做,致使未刷新 jwt,无疑会令用户抓狂。 
3.完善 refreshToken 
  借鉴 oauth2 的设计,返回给客户端一个 refreshToken,容许客户端主动刷新 jwt。通常而言,jwt 的过时时间能够设置为数小时,而 refreshToken 的过时时间设置为数天。我认为该方案并可行性是存在的,可是为了解决 jwt 的续签把整个流程改变了,为何不考虑下 oauth2 的 password 模式和 client 模式呢? 
4.使用 redis 记录独立的过时时间 
  为了解决续签问题,在 redis 中单独给每一个 jwt 设置了过时时间,每次访问时刷新 jwt 的过时时间,若 jwt 不存在于 redis 中则认为过时。 
一样改变了 jwt 的流程,不过嘛,世间安得两全法。我只能奉劝各位还未使用 jwt 作会话管理的朋友,尽可能仍是选用传统的 session+cookie 方案,有不少成熟的分布式 session 框架和安全框架供你开箱即用。

 

参考文章:

1.https://blog.csdn.net/mn_kw/article/details/80522565

2.http://www.javashuo.com/article/p-cgntecus-dx.html

3.https://blog.csdn.net/qq_28165595/article/details/80214994

做者: 捞月亮的猴子
         
欢迎转载,但请标明出处。若是本文对您有些许帮助,点击一下推荐吧,Thanks♪(・ω・)ノ