JWT认证

2023-05-19,

1.什么是JWT Token

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

JWT的使用场景

一次性验证

比如用户注册后需要发一封邮件让其激活账户,通常邮件中需要有一个链接,这个链接需要具备以下的特性:能够标识用户,该链接具有时效性(通常只允许几小时之内激活),不能被篡改以激活其他可能的账户…这种场景就和 jwt 的特性非常贴近,jwt 的 payload 中固定的参数:iss 签发者和 exp 过期时间正是为其做准备的。

restful api 的无状态认证

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

2.JWT的组成

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

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", },
{ "testStr", "hello" },
{"testObj" ,new { name=""} },
{ "exp", DateTimeOffset.UtcNow.AddSeconds().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.https://www.cnblogs.com/lonelyxmas/p/8024083.html

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

JWT认证的相关教程结束。

《JWT认证.doc》

下载本文的Word格式文档,以方便收藏与打印。