1.JWT官网介绍
What is JSON Web Token?
JSON Web令牌(JWT)是一个开放标准(RFC 7519),它定义了一种紧凑的、自包含的方式,用于在各方之间作为JSON对象安全地传输信息。该信息可以被验证和信任,因为它是数字签名的。JWTS可以使用密钥(使用HMAC算法)或公钥/私钥对使用RSA或ECDSA来签名。虽然JWTS可以加密,但也提供保密各方之间,我们将重点放在签名令牌。签名的令牌可以验证其中包含的声明的完整性,而加密的令牌则向其他方隐藏这些声明。当令牌使用公钥/私钥对签名时,签名还证明只有持有私钥的一方是对其签名的一方。
说白了,JWT可以算是一种生成Token的加密算法,我们可以将信息存储在令牌(Token)当中,并且,持有密钥的一方才可以进行验证Token的有效性。
2.为什么要使用JWT?
2.1 传统的Session认证
由于Http协议是无状态的,所以服务端无法识别每一次请求是从哪个用户发来的。
所以在传统的开发当中,都会采取以下类似的操作,假设我们要对用户进行鉴权操作,大概有如下:
1.用户在客户端输入账号密码进行登陆
2.服务端对账号密码进行校验,校验成功则将用户信息存储在服务端,并生成一串session_id返回给客户端存储在Cookie当中
3.用户再次访问或操作时,只需将Cookie中的session_id发送给服务端即可完成鉴权。
2.2 Session认证的缺点
1.当大量用户信息存储在服务端时,会使服务器开销增大,若采用集群或分布式的情况,还要考虑到Session的共享机制。
2.存在跨站请求伪造攻击的风险(CSRF)
跨站请求伪造:假设你在A系统进行转账,之后你去访问存在风险的B系统,B系统可以将你的session_id进行转发,从而盗取你的血汗钱。服务端通过session_id对账户进行识别,一旦session_id泄露,会造成不可预估的风险。
2.3 JWT出现了
JWT的鉴权机制类似于Http协议,也是属于无状态的。我们可以将用户的非敏感信息存储在Token中,在每次请求的过程中,携带在Header并发送给服务端,服务端使用密钥进行解析,从而可以得到用户的相关信息,这时候就避免了过多的信息存储在Session中,降低服务端的开销。
2.4 JWT组成
JWT由三部分组成:Header、Payload、Signature
JWT都长这样:liang.zai.bo
liang代表Header,zai代表Payload,bo代表Signature
头部(Header): 头部用于描述Json Web Token的基本信息,如类型、签名算法。将以下JSON格式的字符串进行Base64编码,就得到了Header。
{
"alg":"HS256",
"typ":"JWT"
}
载荷(Payload):载荷是JWT的主体内容部分,里面存放着一些非敏感信息,官网定义了以下五个字段:iss、sub、aud、exp、iat
iss:JWT的签发者
sub:JWT的主体部分
aud:接收JWT的一方
exp:该JWT的过期时间,格式为Unix时间戳(一定要注意!不然就是个坑)
iat:JWT的签发时间
将以下的JSON字符串进行Base64编码,即可得到Payload。
{
"sub":"liangzaibo",
"exp":"666666666666666666666",
"username":"liangzaibo",// 我们还可以自定义字段
}
签名(Signature):将Header和Payload进行Base64编码以及提供的密钥,采用Header指定的签名算法进行加密,即HS256(Base64Encode(Header) + “.” + Base64Encode(Payload),SECRET)
2.5 JWT实践
public class Main {
public static void main(String[] args) throws ParseException {
// header 可有可无,此处为了方便演示,默认为{"alg":"HS256"}
Map<String, Object> header = new HashMap<String, Object>();
header.put("alg", "HS256");
header.put("typ", "JWT");
// claims代表Payload的主要内容,可以自定义字段
Map<String, Object> cliams = new HashMap<String, Object>();
cliams.put("username", "liangzaibo");
String token = generateJwtToken(header, cliams);
System.out.println(token);
//eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.
//eyJleHAiOjE2MTAyMDgwMDAsImlhdCI6MTYwOTQ5ODk3OSwidXNlcm5hbWUiOiJsaWFuZ3phaWJvIn0.
//vglPuCoRdTQbVq68TykiDsiev-p_Rr61WQvJHwqOLWM
Claims claims = parseJwtToken(token);
System.out.println(claims);
// {exp=1610208000, iat=1609498979, username=liangzaibo}
}
/**
* 生成JWT令牌
*/
public static String generateJwtToken(Map<String,Object> header, Map<String,Object> claims) throws ParseException {
return Jwts.builder()
.setHeader(header)
.setClaims(claims)
.signWith(SignatureAlgorithm.HS256, "SECRET") //加密算法以及密钥
.setExpiration(new SimpleDateFormat("yyyy-MM-dd").parse("2021-01-10")) // JWT过期时间
.setIssuedAt(new Date()) // 签发时间
.compact();
}
/**
* 解析JWT
*/
public static Claims parseJwtToken(String token){
return Jwts.parser()
.setSigningKey("SECRET")
.parseClaimsJws(token)
.getBody();
}
2.6 注意事项
- 不得将敏感信息存储在JWT中,因为Header、Payload只是进行了一次Base64编码,所以只要对Header、Payload进行一次Base64解码即可得到内部信息。
- JWT是支持跨语言的,但是我遇到一个项目,开发人员设置JWT的过期时间不是时间戳格式,导致最后我无法进行同步解析。所以一定要注意,存储JWT的exp过期时间一定要为时间戳。否则最后是无法解析的。
- 生成JWT时,过期时间不易设置过长,因为JWT一旦签发,就无法撤销,必须等待其到达过期时间才失效。
- 并不是所有场合都适用JWT,但业务不需要进行加密解密时,使用JWT就是累赘。