用大白话讲明白JWT
JWT,是JSON Web Token的缩写,目前最流行的跨域认证解决方案。
为什么需要JWT?
一般的互联网用户认证流程如下:
- 用户向服务器发送用户名和密码。
- 服务器验证通过后,在当前对话(session)里面保存相关数据,比如用户角色、登录时间等等。
- 服务器向用户返回一个 session_id,写入用户的 Cookie。
- 用户随后的每一次请求,都会通过 Cookie,将 session_id 传回服务器。
- 服务器收到 session_id,找到前期保存的数据,由此得知用户的身份。
这就是经典的cookie-session认证模式,cookie保存在客户端,持有session-id,而session保存在服务端,持有用户信息和权限、过期时间等信息。
这种模式简洁易用,还能保证信息安全。
但是随着架构更新成服务器集群和跨域的服务导向架构,就要求实现session共享,否则不能实现session共享。一般来说,能够通过中间件实现session共享,比如redis。
举例来说,某系统有两个后端服务,都是集群部署,分别为服务A和服务B,用户在A服务登录之后,携带session-id请求B,要求用户能够通过B的登录验证,并且能获取用户信息。
大家都知道,session默认是存储的服务端的硬盘中,上面的场景中,用户登录的session应该会存储在服务集群A的某个节点上,那么用户访问任何其他的节点或服务都要重新登录。
最常见的解决方案就是利用中间件存储session。如上图中,若将session存储在redis中,那么用户登录之后,每个服务节点都能通过session-id查询到session,实现单点登录。
那么这种cookie-session模式有什么缺点呢?
- 客户端禁用cookie就会失效
- 跨域可能会导致cookie丢失
- 必须要有统一的中间件
- 中间件如果挂了,登录就会失效
优化方案
- 弃用cookie,直接将用户token存放在request header中,用户信息以token为key存放在redis
- 服务器索性不保存session数据了,直接将用户token和用户信息全都存在request header中,JWT就是此方案的代表
JWT的原理
怎么实现将用户信息存放在客户端,还能实现用户登录认证和权限认证?
我们可以将用户信息和用户权限和用户认证时间放一起生成一个json,如下
{
"姓名": "张三",
"角色": "管理员",
"到期时间": "2024年12月1日0点0分"
}
然后如果将这个字符串放在请求头中,那么服务端每次接收到请求都能知道用户信息和是否已登录等。
这样做的好处是服务器是无状态的,扩展性非常好。
但是请求头必须经过编码和网络传输,需要考虑乱码和URLEncode等问题。
所以我们要将上面的字符串使用Base64URL进行编码,规避这些问题。
新问题又来了,用户信息在客户端被篡改了怎么办?
我们再加一道签名(一种信息安全技术,此处不展开),防止信息篡改。
最终,上面的JSON经过这样魔改之后,就是所谓的JWT!它最终长这样:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyLlp5PlkI0iOiLlvKDkuIkiLCLop5LoibIiOiLnrqHnkIblkZgiLCLliLDmnJ_ml7bpl7QiOiIyMDI05bm0MTLmnIgx5pelMOeCuTDliIYifQ.fvBo5i2wXeYufnUQGLaJvtY6GdcqRhTgA5kc5S8HqLU
总结一下,JWT就是一串经过签名和编码的json对象,用户登录后由服务端生成,一般通过request header传输,轻松实现在不同平台和系统间信息交换和认证。
JWT官网: [https://jwt.io/]
JWT结构
JWT 有三个部分,依次如下。
Header(头部)
Payload(负载)
Signature(签名)
用英文点隔开,写成一行,就是下面的样子。
Header.Payload.Signature
下面依次介绍这三个部分。
Header
Header 部分是一个 JSON 对象,描述 JWT 的元数据,通常是下面的样子。
{
"alg": "HS256",
"typ": "JWT"
}
上面代码中,alg属性表示签名的算法(algorithm),默认是 HMAC SHA256(写成 HS256);typ属性表示这个令牌(token)的类型(type),JWT 令牌统一写为JWT。
最后,将上面的 JSON 对象使用 Base64URL 算法(详见后文)转成字符串。
Payload
Payload 部分也是一个 JSON 对象,用来存放实际需要传递的数据。JWT 规定了7个官方字段,供选用。
- iss (issuer):签发人
- exp (expiration time):过期时间
- sub (subject):主题
- aud (audience):受众
- nbf (Not Before):生效时间
- iat (Issued At):签发时间
- jti (JWT ID):编号
除了官方字段,你还可以在这个部分定义私有字段,下面就是一个例子。
{
"姓名": "张三",
"角色": "管理员",
"到期时间": "2024年12月1日0点0分"
}
注意,JWT 默认是不加密的,任何人都可以读到,所以不要把敏感信息放在这个部分。但是JWT的扩展性很好,我们可以先加密,再放入payload中。
在实际项目中,我一般将敏感信息使用DES加密,再放入JWT。
这个 JSON 对象也要使用 Base64URL 算法转成字符串。
Signature
Signature 部分是对前两部分的签名,防止数据篡改。
简单聊一下签名,前面说过,jwt存放在客户端就会有被篡改的风险,尤其是用户权限部分。
报文签名就是防止信息篡改的主流方案,大致原理如下:
- 加签:生成jwt的时候,将整个信息串通过一个秘钥计算加密,得到一个签名。秘钥仅保存在服务端。
- 验签:等到下次服务端再接收到这个jwt的时候,再次拿信息串加密算出签名,和原来的签名进行比对,就能知道信息有没有被篡改了。
算出签名以后,把 Header、Payload、Signature 三个部分拼成一个字符串,每个部分之间用"点"(.)分隔,就可以返回给用户。
Base64URL
前面提到,Header 和 Payload 串型化的算法是 Base64URL。这个算法跟 Base64 算法基本类似,但有一些小的不同。
JWT 作为一个令牌(token),有些场合可能会放到 URL(比如 api.example.com/?token=xxx)。Base64 有三个字符+、/和=,在 URL 里面有特殊含义,所以要被替换掉:=被省略、+替换成-,/替换成_ 。这就是 Base64URL 算法。
JWT的缺点
- JWT 默认是不加密,需要我们手动加密
- JWT无法作废,一旦 JWT 签发了,在到期之前就会始终有效,除非服务器部署额外的逻辑。
- JWT无法续签,因为JWT防篡改的特点,导致我们服务自己也没办法修改jwt的有效期,jwt一旦过期就无法续签
- 放置在客户端还是会有被盗用的风险,建议有效时间尽量设置短一点
JWT的使用
几乎所有的编程语言都有jwt的公共组件库,jwt的官网为我们介绍了一些常用的组件:https://jwt.io/libraries
这里以Java的依赖com.auth0 / java-jwt为例:
引入依赖
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.18.2</version>
</dependency>
生成JWT
public static String genJwt() {
Map<String, Object> headMap = Maps.newHashMap();
headMap.put("alg", "HS256");
headMap.put("type", "JWT");
return JWT.create()
// Header
.withHeader(headMap)
// Payload
.withClaim("tgc", "tgcVal")
.withClaim("token", "Token")
.withClaim("name", "zhangsan")
.withExpiresAt(new Date(System.currentTimeMillis() + 1000*60*60*24))
// 签名用的secret
.sign(Algorithm.HMAC256("secret"));
}
验证JWT
JWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC256("secret")).build();
public static boolean verifyToken(String token) {
try {
Map<String, Claim> map = jwtVerifier.verify(token).getClaims();
return true;
} catch (JWTVerificationException e) {
return false;
}
}