# JWT
# 介绍
JWT 是 JSON Web Token 的缩写,即 JSON Web 令牌,是一种自包含令牌 (自己能包含重要信息),是为了在网络引用环境间传递声明而执行的一种基于 JSON 的开发标准
JWT 的声明一般被用来在身份提供者和服务提供者间传递被认证的用户身份信息,以便于从资源服务器获取资源。比如用在用户登录上。
JWT 最重要的作用就是对 token 的信息的防伪作用。
# JWT 令牌的组成
一个 JWT 由三个部分组成:JWT 头,有效载荷,签名哈希
最后由这三者组合进行 base64url 编码得到 JWT
典型的,一个 JWT 看起来如下图:该对象为一个很长的字符串,字符之间通过 “.” 分隔符分为三个子串。
https://jwt.io/
# JWT 头
JWT 头部分是一个描述 JWT 元数据的 JSON 对象,通常如下所示:
{ | |
"alg": "HS256", | |
"typ": "JWT" | |
} |
在上面的代码中,alg 属性表示签名使用的算法,默认为 HMAC SHA256 (写为 HS256)
typ 属性表示令牌的类型,JWT 令牌统一写为 JWT。
最后,使用 Base64URL 算法将上述 JSON 对象转换为字符串保存。
# 有效载荷
有效载荷部分,是 JWT 的主体内容部分,也是一个 JSON 对象,包含需要传递的数据,JWT 指定七个默认字段供选择。
iss:jwt 签发者
sub:主题
aud:接收 jwt 的一方
exp:jwt 的过期时间,这个过期事件必须要大于签发时间
nbf:定义在什么时间之前,该 jwt 都是不可用的。
jat:jwt 的签发时间
jti:jwt 的唯一身份标识,主要用来作为一次性 token,从而回避重放攻击。
除以上默认字段外,我们还可以自定义私有字段,如下例:
{ | |
"name": "Helen", | |
"role": "editor", | |
"avatar": "helen.jpg" | |
} |
请注意,默认情况下 JWT 是未加密的,任何人都可以解读其内容,因此不要构建隐私信息字段,存放保密信息,以防止信息泄露
JSON 对象也是用 Base64URL 算法转换为字符串保存。
# 签名哈希
签名哈希部分是对上面两部分数据签名,通过指定的算法生成哈希,以确保数据不会被篡改。
首先,需要指定一个密码 (secret)。改密码仅仅为保存在服务器中,并且不能向用户公开。然后,使用标头中指定的签名算法 (默认情况下为 HMAC SHA256) 根据以下公式生成签名。
HMACSHA256(base64urlEncode(header) + "." + base64UrlEncode(claims).secret) ==> 签名 hash
在计算出签名哈希后,JWT 头,有效载荷和签名哈希的三个部分组合成一个字符串,每个部分用 “.” 分隔,就构成整个 JWT 对象。
# Base64URL 算法
如前所述,JWT 头和有效载荷序列化的算法都用到了 Base64URL。该算法和常见 Base64 算法类似,稍有差别。
作为令牌的 JWT 可以放在 URL 中 (例如 api.example/?token=xxx)。Base64 中用的三个字符是 “+” , “/” 和 “=” ,由于在 URL 中有特殊含义,因此 Base64URL 中对他们做了替换: “=” 去掉,“+” 用 “-” 替换,“/” 用 “_” 替换,这就是 Base64URL 算法
# JWT 工具类代码
具体可以根据自己的
import io.jsonwebtoken.*; | |
import javax.crypto.SecretKey; | |
import javax.crypto.spec.SecretKeySpec; | |
import java.util.Base64; | |
import java.util.Date; | |
import java.util.UUID; | |
public class JWTHelper { | |
// 设置有效期 一个小时 | |
public static final Long JWT_TTL = 60 * 60 * 1000L; | |
// 设置密钥 设置长度不能大于 11 | |
public static final String JWT_KEY = "liusangbaoyo"; | |
public static String getUUID() { | |
return UUID.randomUUID().toString().replaceAll("-", ""); | |
} | |
/** | |
* 生成 JWT | |
* | |
* @param subject token 中要存放的数据 (JSON 格式) | |
* @return | |
*/ | |
public static String createJwt(String subject) { | |
return getJwtBuilder(subject, null, getUUID(), null, null).compact(); | |
} | |
public static String createJwt(Long id, String name) { | |
return getJwtBuilder(null, null, getUUID(), id, name).compact(); | |
} | |
public static String createJwt(String subject, Long ttlMillis) { | |
// 设置过期时间 | |
return getJwtBuilder(subject, ttlMillis, getUUID(), null, null).compact(); | |
} | |
private static JwtBuilder getJwtBuilder(String subject, Long ttlMillis, String uuid, Long id, String name) { | |
SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256; | |
SecretKey secretKey = generalKey(); | |
long nowMillis = System.currentTimeMillis(); | |
Date now = new Date(nowMillis); | |
if (ttlMillis == null) { | |
ttlMillis = JWTHelper.JWT_TTL; | |
} | |
long expMills = nowMillis + ttlMillis; | |
Date expDate = new Date(expMills); | |
return Jwts.builder() | |
.setId(uuid) // 唯一的 ID | |
.setSubject(subject) // 主题,可以是 JSON 数据 | |
.claim("userId", id) | |
.claim("username", name) | |
.setIssuer("dkx") // 签发者 | |
.setIssuedAt(now) // 签发时间 | |
.signWith(signatureAlgorithm, secretKey) // 使用 HS256 对称加密算法签名,第二个参数为密钥 | |
.setExpiration(expDate); | |
} | |
public static String createJwt(String id, String subject, Long ttlMillis) { | |
return getJwtBuilder(subject, ttlMillis, id, null, null).compact(); | |
} | |
private static SecretKey generalKey() { | |
byte[] encodedKey = Base64.getDecoder().decode(JWTHelper.JWT_KEY); | |
return new SecretKeySpec(encodedKey, 0, encodedKey.length, "AES"); | |
} | |
public static Claims parseJwt(String jwt) { | |
Claims claims; | |
SecretKey secretKey = generalKey(); | |
try { | |
return Jwts.parser() | |
.setSigningKey(secretKey) | |
.parseClaimsJws(jwt) | |
.getBody(); | |
} catch (ExpiredJwtException e) { | |
claims = e.getClaims(); | |
} | |
return claims; | |
} | |
} |
# JWT 中的 Claims
Claims 有索赔,声称,要求或者权利要求的含义,但是笔者觉得任一个翻译都不怎么合乎语义,这里保留 Claims 关键字直接作为命名。JWT 的核心作用就是保护 Claims 的完整性 (或者数据加密) ,保证 JWT 传输的过程中 Claims 不被篡改 (或者不被破解)。Claims 在 JWT 原始内容中是一个 JSON 格式的字符串,其中单个 Claims 是 k-v 结构,作为 JsonNode 中的一个 find-value,这里列出了常用的规范中预定义好的 Claim;
简称 | 全称 | 含义 |
---|---|---|
iss | Issuer | 发行方 |
sub | Subject | 主体 |
aud | Audience | (接收) 目标方 |
exp | Expiration Time | 过期时间 |
nbf | Not Before | 早于该定义的时间的 JWT 不能被接受处理 |
iat | Issued At | JWT 发行时的时间戳 |
jti | JWT ID | JWT 的唯一标识 |
这些预定义的 Claim 并不要求强制使用,何时选用何种 Claim 完全由使用者决定,而为了使 JWT 更加紧凑,这些 Claim 都是用了简短的命名方式去定义。在不和内建的 Claim 冲突的前提下,使用者可以自定义新的公共 Claim,如:
简称 | 全称 | 含义 |
---|---|---|
cid | Customer ID | 客户 ID |
rid | Role ID | 角色 ID |
一定要注意,在 JWT 实现中,Claims 会作为 payload 部分进行 Base64 编码,明文会直接暴漏,敏感信息一般不应该设计为一个自定义 Claim。
<span alt='solid'> 取出上述 JWT 工具类中设置的 userId 与 username 的值 </span>.
@Test | |
public void test() { | |
String jwt = JWTHelper.createJwt(1233L, "jiashujv"); | |
System.out.println(jwt); | |
Claims claims = JWTHelper.parseJwt(jwt); | |
Integer id = claims.get("userId", Integer.class); | |
String username = claims.get("username", String.class); | |
System.out.println("id: "+id+", name: "+username); | |
} |