SpringBoot 整合 JWT 实现 Token 认证
一、前言
HTTP 是一个无状态的协议,因此服务器无法识别2次请求是否来自同一个客户端。但在 Web 应用中,用户的认证和鉴权又是非常重要的一环,实践中产生了多种可用的方案,基于 Session 的会话管理即是其中一种。
在 Web 应用发展的初期,大部分 Web 应用采用基于 Session 的会话管理方式,其逻辑如下:
- 客户端使用用户名、密码进行认证
- 服务端生成 Session 并存储,将 SessionID 通过 Cookie 返回给客户端
- 客户端访问需要认证的接口时在 Cookie 中携带 SessionID
- 服务端通过 SessionID 查找 Session 并进行鉴权,通过则返回给客户端需要的数据
Cookie
Cookie 是客户端保存用户信息的一种机制,用来记录用户的一些信息,也是实现 Session 的一种方式。Cookie 存储的数据量有限,且都是保存在客户端浏览器中。不同的浏览器有不同的存储大小,但一般不超过 4KB。因此使用 Cookie 实际上只能存储一小段的文本信息。
例如:登录网站,今输入用户名密码登录了,第二天再打开很多情况下就直接打开了。这个时候用到的一个机制就是 Cookie。
Session
Session 是另一种记录客户状态的机制,它是在服务端保存的一个数据结构(主要存储的的 SessionID 和 Session 内容,同时也包含了很多自定义的内容如:用户基础信息、权限信息、用户机构信息、固定变量等),这个数据可以保存在集群、数据库、文件中,用于跟踪用户的状态。
客户端浏览器访问服务器的时候,服务器把客户端信息以某种形式记录在服务器上。这就是 Session。客户端浏览器再次访问时只需要从该 Session 中查找该客户的状态就可以了。
用户第一次登录后,浏览器会将用户信息发送给服务器,服务器会为该用户创建一个 SessionId,并在响应内容(Cookie)中将该 SessionId 一并返回给浏览器,浏览器将这些数据保存在本地。当用户再次发送请求时,浏览器会自动的把上次请求存储的 Cookie 数据自动的携带给服务器。
服务器接收到请求信息后,会通过浏览器请求的数据中的 SessionId 判断当前是哪个用户,然后根据 SessionId 在 Session 库中获取用户的 Session 数据返回给浏览器。
例如:购物车,添加了商品之后客户端处可以知道添加了哪些商品,而服务器端如何判别呢,所以也需要存储一些信息就用到了 Session。
如果说 Cookie 机制是通过检查客户身上的“通行证”来确定客户身份的话,那么 Session 机制就是通过检查服务器上的“客户明细表”来确认客户身份。Session 相当于程序在服务器上建立的一份客户档案,客户来访的时候只需要查询客户档案表就可以了。
Session 生成后,只要用户继续访问,服务器就会更新 Session 的最后访问时间,并维护该 Session。为防止内存溢出,服务器会把长时间内没有活跃的 Session 从内存删除。这个时间就是 Session 的超时时间。如果超过了超时时间没访问过服务器,Session 就自动失效了。
基于 Session 的认证方式存在如下问题:
- 服务端需要存储 Session,由于 Session 经常需要快速查找,通常将其存储在内存或内存数据库中,当同时在线用户较多时会占用大量的服务器资源;
- 在分布式架构下,当前访问的节点可能不是创建 Session 的节点,导致无法验证,因此需要考虑在多个节点间同步 Session 数据;
- 由于客户端使用 Cookie 存储 SessionID,在跨域场景下需要进行兼容性处理,同时这种方式也难以防范 CSRF 攻击;
- 不支持 Android,IOS,小程序等移动端;
鉴于基于 Session 的会话管理方式存在上述多个缺点,无状态的基于 Token 的会话管理方式诞生了,所谓无状态,就是服务端不再存储信息,甚至是不再存储 Session,其处理逻辑如下:
- 客户端使用用户名、密码进行认证
- 服务端验证用户名密码,通过后生成 Token 返回给客户端
- 客户端保存 Token,访问需要认证的接口时在 URL 参数或 HTTP Header 中加入 Token
- 服务端通过解码 Token 进行鉴权,认证通过则返回给客户端需要的数据
基于 Token 的会话管理方式有效的解决了基于 Session 的会话管理方式带来的问题:
- 服务端不需要存储和用户鉴权有关的信息,鉴权信息会被加密到 Token 中,服务端只需要读取 Token 中包含的鉴权信息即可
- 避免了共享 Session 导致的不易扩展问题
- 不需要依赖 Cookie,有效避免 Cookie 带来的 CSRF 攻击问题
- 使用 CORS 可以快速解决跨域问题
- 支持 Android,IOS,小程序等不支持 Cookies 的移动端
二、什么是 JWT
JWT,全称 JSON Web Token,是一个开放标准(RFC 7519),它以一种紧凑的、自包含的方式在各方之间安全的传输信息。其官方定义如下:
JWT 官方定义三、JWT 原理
JWT 认证原理:服务器生成一个 JWT 后会将它以 Authorization : Bearer JWT 键值对的形式存放在 cookies 里面发送到客户端,客户端再次访问受 JWT 保护的资源时,服务器会获取到 cookies 中存放的 JWT 信息,服务端程序首先对 Header 进行反编码获取到加密算法,再通过存放在服务器上的密匙对 Header.Payload 这个字符串进行加密,然后比对 JWT 中的 Signature 和实际加密出来的结果是否一致,如果一致那么说明该 JWT 合法有效,认证通过,否则认证失败。
JWT格式:Header.Payload.Signature
Header
{
"typ":"JWT",
"alg":"HMAC256"
}
Header 是由上面这种格式的 Json 通过 Base64 编码生成的字符串,它描述了编码对象是一个 JWT 且使用 HMAC256 算法进行加密,当然也可以选用其他加密算法。
JWT 官方类库支持下列所有加密算法:
JWS | Algorithm | Description |
---|---|---|
HS256 | HMAC256 | HMAC with SHA-256 |
HS384 | HMAC384 | HMAC with SHA-384 |
HS512 | HMAC512 | HMAC with SHA-512 |
RS256 | RSA256 | RSASSA-PKCS1-v1_5 with SHA-256 |
RS384 | RSA384 | RSASSA-PKCS1-v1_5 with SHA-384 |
RS512 | RSA512 | RSASSA-PKCS1-v1_5 with SHA-512 |
ES256 | ECDSA256 | ECDSA with curve P-256 and SHA-256 |
ES384 | ECDSA384 | ECDSA with curve P-384 and SHA-384 |
ES512 | ECDSA512 | ECDSA with curve P-521 and SHA-512 |
Claim => Payload
Claim 也是一个 Json。Claim 中存放的内容是 JWT 自身的标准属性,所有的标准属性都是可选的,可自行添加的,比如 JWT 的签发者、JWT 的接收者、JWT 的有效时间等;同时 Claim 中也可以存放一些自定义的属性,这个自定义的属性可以是在用户认证中用于标明用户身份的属性,如用户对应的数据库记录 ID(为了安全起见,不可以将用户名及密码这类敏感的信息存放在 Claim 中)。Claim 经 Base64转码之后生成的一串字符串称作Payload。 Claim 的内容可以是:
{
loginUser: 'muyao',
userId: '10000000',
exp: 1544602234
}
Signature
将 Header 和 Claim 这两个 Json 分别使用 Base64 方式进行编码,生成字符串 Header 和 Payload,然后将Header 和 Payload 以 Header.Payload 的格式拼接在一起形成一个字符串,再使用 Header 中定义好的加密算法和一个密匙(这个密匙存放在服务器上,用于进行验证)对这个字符串进行加密,获得一个新的字符串,这个字符串就是 Signature。
四、SpringBoot 整合 JWT 实现 Token 认证
1. pom.xml 添加 maven 依赖
<properties>
<jwt.version>3.8.1</jwt.version>
</properties>
<!-- https://mvnrepository.com/artifact/com.auth0/java-jwt -->
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>${jwt.version}</version>
</dependency>
2. 实现签名方法和认证方法
package com.muyao;
import java.sql.Date;
import java.util.HashMap;
import java.util.Map;
import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTCreator;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.exceptions.JWTCreationException;
import com.auth0.jwt.exceptions.JWTVerificationException;
import com.auth0.jwt.interfaces.DecodedJWT;
import com.auth0.jwt.interfaces.JWTVerifier;
public class JwtUtils {
/** 过期时间,缺省15分钟 */
private long EXPIRE_TIME = 15 * 60 * 1000;
/** token 私钥,缺省 galaxy-all */
private String TOKEN_SECRET = "Galaxy-All";
/** header */
private Map<String, Object> header = new HashMap<>();
/** 签名算法实例 */
private Algorithm algorithm;
/** token 认证器 */
private JWTVerifier verifier;
public JwtUtils() {
JwtInit();
}
public JwtUtils(long expireTime, String tokenSecret) {
this.EXPIRE_TIME = expireTime;
this.TOKEN_SECRET = tokenSecret;
JwtInit();
}
// 签名算法和认证器初始化
private void JwtInit() {
this.algorithm = Algorithm.HMAC256(this.TOKEN_SECRET);
this.verifier = JWT.require(this.algorithm).build();
this.header.put("typ", "JWT");
this.header.put("alg", "HS256");
}
/**
* 签名方法:采用 HMAC256算法,附带 claims 信息生成签名
*
* @param claims
* @return
*/
public String sign(Map<String, String> claims) throws Exception {
// 计算 token 过期时间
Date date = new Date(System.currentTimeMillis() + this.EXPIRE_TIME);
try {
JWTCreator.Builder jwt = JWT.create().withHeader(this.header).withExpiresAt(date);
for (Map.Entry<String, String> entry : claims.entrySet()) {
jwt.withClaim(entry.getKey(), entry.getValue());
}
return jwt.sign(this.algorithm);
} catch (JWTCreationException exception) {
exception.printStackTrace();
throw new Exception(String.format("生成签名异常【%s】!", exception.getMessage()));
}
}
/**
* 认证方法类
* @param token
* @return
*/
public boolean verify(String token) {
try {
DecodedJWT jwt = verifier.verify(token);
return true;
} catch (JWTVerificationException exception) {
return false;
}
}
}
五、JWT 认证方式存在的问题
- token 不能撤销:JWT 没有过期或者失效时,客户端重置密码,JWT 依然可以使用;
- 不支持 refresh token,JWT 过期后需要执行登录授权的完整流程;
- 无法知道用户签发了几个 JWT
续篇将针对上述问题给出解决方案。