最近在做网页登录权限认证的时候,由于我使用的框架是 SpringBoot ,在网上参考了大量资料后,我发现 SpringSecurity + JWT 出现频率很高,于是我去了解了一下 JWT,以下是我做的一些记录。
1 什么是 JWT
JSON Web Token(JWT)是一个开放标准(RFC 7519),它定义了一种紧凑且自包含的方式,用 JSON 对象在各方之间安全地传输信息。此信息是经过数字签名的,因此可以验证和信任。
以上是来自 https://jwt.io/ 的一段介绍,从中我们得出 JWT 是一个 Token(令牌),用于 web 中数据的安全传输。
利用 Token 进行登录验证的步骤:
- 用户输入账号密码点击登录
- 后台收到账号密码,验证是否合法用户
- 后台验证是合法用户,生成一个
Token
返回给用户 - 用户收到该 Token 并将其保存在每次请求的请求头中
- 后台每次收到请求都去查询请求头中是否含有正确的 Token,只有 Token 验证通过才会返回请求的资源。
这种基于 Token 的认证方式相比较于基于传统的 cookie 和 session 方式更加节约资源,并且对移动端和分布式系统支持更加友好,其优点有:
- 支持跨域访问:cookie 是不支持跨域的,而 Token 可以放在请求头中传输
- 无状态:Token 自身包含了用户登录的信息,无需在服务器端存储 session
- 移动端支持更好:当客户端不是浏览器时,cookie 不被支持,采用 Token 无疑更好
- 无需考虑 CRSF:不使用 cookie,也就无需考虑 CRSF 的防御
而 JWT 就是上述 Token 的一种具体实现方式,其本质就是一个字符串, 是将用户信息存储到 JSON 中然后经过编码得到的字符串
2 JWT 的结构
JWT 由三部分组成,分别是 Header(头部)、Payload(有效载荷)、Signature(签名),用点(.)将三部分隔开便是 JWT 的结构,形如xxxxx.yyyyyy.zzzzz
的字符串
2.1 Header
JWT Header 由两部分组成,是一个描述 JWT 元数据的JSON对象,alg 属性表示签名使用的算法,默认为 HMAC SHA256(写为HS256);typ 属性表示 Token 的类型,统一写为 JWT。最后,使用 Base64 URL 算法将上述 JSON 对象转换为字符串保存
{
"alg": "HS256",
"typ": "JWT"
}
2.2 Payload
payload 是 JWT 的主体部分,保存实体(通常是用户)信息,每一个字段就是一个 claim(声明),JWT 为我们提供了一些默认字段
iss:发行人
exp:到期时间
sub:主题
aud:用户
nbf:在此之前不可用
iat:发布时间
jti:JWT ID用于标识该JWT
我们也可以自定义私有字段,比如用来保存用户信息
{
"sub": "1234567890",
"name": "John Doe",
"admin": true
}
payload 字段会使用 BaseUrl 编码成字符串组成 JWT 的第二个部分
2.3 Signature
签名部分是对上面两部分数据签名,需要使用 base64 编码后的 header 和 payload 数据,通过指定的算法生成哈希,以确保数据不会被篡改。首先,需要指定一个密钥(secret)。该密码仅仅为保存在服务器中,并且不能向用户公开。然后,使用header中指定的签名算法(默认情况下为 HMAC SHA256)根据以下公式生成签名
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
secret)
2.4 注意事项
- Header 和 Payload 只是简单的利用 Base64 编码,是可逆的,因此不要在 Payload 中存储敏感信息
- Signature 使用的是不可逆的加密算法,无法解码出原文,它的作用是校验 Token 有没有被篡改。该算法需要我们自己指定一个密钥,这个密钥存储在服务端,不能泄露
- 尽量避免在 JWT 中存储大量信息,因为一些服务器接收的 HTTP 请求头最大不超过 8KB
3 如何使用 JWT
在官网中选择开发语言,可以看到官方给我们推荐的库,这里以 Java 为例
比较推荐使用 java-jwt
以及jjwt-root
(star 最多,用户量大),去官方 GitHub 仓库学习使用方法
Java 中使用 JWT
首先利用你喜欢的方式引入依赖,我这里利用 Maven
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>4.4.0</version>
</dependency>
创建 JWT
public class JWTTest {
@Test
public void testGreateToken(){
// 指定token过期时间为10秒
Calendar calendar = Calendar.getInstance();
calendar.add(Calendar.SECOND, 10);
String token = JWT.create()
.withHeader(new HashMap<>()) // Header
.withClaim("userId", 1001) // Payload
.withClaim("userName", "user")
.withExpiresAt(calendar.getTime()) // 过期时间
.sign(Algorithm.HMAC256("YOUR_SECRETKEY")); // 签名用的secret
System.out.println(token);
}
}
解析 JWT
@Test
public void testResolveToken(){
// 创建解析对象,使用的算法和secret要与创建token时保持一致
JWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC256("YOUR_SECRETKEY")).build();
// 解析指定的token
DecodedJWT decodedJWT = jwtVerifier.verify(token);
// 获取解析后的token中的payload信息
Claim userId = decodedJWT.getClaim("userId");
Claim userName = decodedJWT.getClaim("userName");
System.out.println(userId.asInt());
System.out.println(userName.asString());
// 输出超时时间
System.out.println(decodedJWT.getExpiresAt());
}
可以将上述方法封装成 JWT 工具类在项目中使用
此外,除了对称加密方式(加密和解密用同样的密钥),还可以选择非对称加密,即指定两把密钥(公钥和私钥),用其中一把密钥加密必须用另外一把密钥解密
实际开发中需要提升 JWT 的安全性:
- JWT 是在请求头中传递的,为了避免网络劫持推荐使用 HTTPS
- JWT 可以使用暴力枚举来破解,因此可以定期更换密钥,不要使用过于简单的密钥