赞
踩
OAuth2 中默认使用 Bearer Tokens (一般用 UUID 值)作为 token 的数据格式,但也支持升级使用 JSON Web Token(JWT) 来作为 token 的数据格式。实际来说,OAuth 规范中并无限制 Token 采取何种格式。今天我们就采用 JWT 来作为 Token,它的一个好处是自描述 Token,包含了用户信息而并不需要通过额外的接口获取用户信息。
所谓 JWT Token,本身是明文的,前端得到之后进行 Base64 解码,即可获取用户信息(JSON)。——此时你认为可以直接使用吗?——那岂可值得信任?放心,我们还有一个 signature 参数用于校验这段 Token 是否合法,还是伪造的。即使假设这是个假的 Token,调用业务逻辑时候传入到后端,我们根据签名就能知道这个 Token 真实性。
故所以,我们必须在服务端校验过后才能用于前端的显示。因为密钥是在后端的——验证 JWT 的完整性和真实性应该在服务器端进行,使用密钥进行签名验证。
网上关于 JWT 的文章很多,但无非都是库的使用方式介绍,再深一点就研究 JWT 原理。其实如果只是生成 JWT,Java 代码是很简单的,不需要依赖什么库。今天我们就发挥一下动手能力,自己写个 JWT Token 的生成器。实际网上写 Java JWT 的轮子不是很多,我看到的有 cn.hutool.jwt.JWT
和老外一个例子。
JWT 令牌由这三部分组成:
{"alg":"HS256","typ":"JWT"}
。我们用一个常量定义之:private static final String JWT_HEADER = "{\"alg\":\"HS256\",\"typ\":\"JWT\"}";
重点是 Payload。其中最关键的三个字段是:exp、sub、aud。当然可以添加任何的信息,一般添加用户的相关信息或其他业务需要的必要信息。但不建议添加敏感信息,因为该部分在客户端可解密。
我们定义个一个 Java Bean,说明这个 Payload 如何:
import lombok.Data; import java.util.List; /** * JWT 基础载荷 */ @Data public class Payload { /** * 主题 */ private String sub; /** * 受众 */ private List<String> aud; /** * 过期时间 */ private Long exp; /** * 签发人 */ private String iss; /** * 签发时间 */ private Long iat; // /** // * 编号,似乎不需要 // */ // private String jti; }
进而描绘出 JWT Token 的结构,如是 JWebToken
:
import com.ajaxjs.util.map.JsonHelper; import lombok.Data; /** * JWT Token */ @Data public class JWebToken { /** * 头部 */ public static final String JWT_HEADER = "{\"alg\":\"HS256\",\"typ\":\"JWT\"}"; /** * 头部的 Base64 编码 */ public static final String encodedHeader = Utils.encode(JWT_HEADER); /** * 载荷 */ private Payload payload; /** * 签名部分 */ private String signature; public JWebToken(Payload payload) { this.payload = payload; } /** * 头部 + Payload * * @return 头部 + Payload */ public String headerPayload() { String p = Utils.encode(JsonHelper.toJson(payload)); return encodedHeader + "." + p; } /** * 返回 Token 的字符串形式 * * @return Token */ @Override public String toString() { return headerPayload() + "." + signature; } }
结构清楚了,我们就试着创建 Token。
首先对 Header 和 Payload 分别 base64 编码,然后通过 HMACSHA256算法得到签名(Signature )部分,这样还可以防止数据被篡改。
String signature = HMACSHA256( base64UrlEncode(header) + "." + base64UrlEncode(payload), secret);
String jwtToken = base64(header) + "." + base64(payload) + "." + signature;
然后把 Header、Payload、Signature 三个部分拼成一个字符串,每个部分之间用"点"(.)分隔,就可以返回给用户。
具体执行过程参见下面测试代码:
JWebTokenMgr mgr = new JWebTokenMgr();
@Test
public void testMakeToken() {
JWebToken token = mgr.tokenFactory("user01", Collections.singletonList("admin"), Utils.setExpire(24));
System.out.println(token.toString());
}
这里出现了 JWebTokenMgr
,这是封装好的 JWT 管理器,一般情况下要对其初始化,传入最关键的密钥 secretKey
信息,还有其他颁发者等的信息。
/**
* JWT 管理器
*/
public class JWebTokenMgr {
private String secretKey = "Df87sD#$%#A";
private String issuer = "foo@bar.net";
public JWebTokenMgr(String secretKey, String issuer) {
this.secretKey = secretKey;
this.issuer = issuer;
}
public JWebTokenMgr() {
}
……
当然不传也行,就是默认的密钥(无安全性可言)。
mgr.tokenFactory()
分别传入了 sub、aud、exp 这三个 Payload 最基本的参数。最后执行 token.toString() 返回 Token 字符串。
JWebToken token = mgr.tokenFactory("user01", Collections.singletonList("admin"), Utils.setExpire(24));
System.out.println(token.toString());
当然你也可以传入 Payload 实例或其子类。
/**
* 创建 JWT Token
*
* @param payload Payload 实例或其子类
* @return JWT Token
*/
public JWebToken tokenFactory(Payload payload);
JWT 作为一个令牌(token),有些场合可能会放到 URL(比如 api.example.com/?token=xxx
)。Base64 有三个字符+
、/
和=
,在 URL 里面有特殊含义,所以要被替换掉:=
被省略、+
替换成-
,/
替换成_
。于是这个 Base64 算法是 Base64URL,跟 Base64 算法基本类似,但有一些小的不同。在 jdk8 之后提供了这样 Base64.getUrlEncoder().withoutPadding()
的 Base64URL 方式。
还是一位行家说得好:
先说签名验证。当接收方接收到一个 JWT 的时候,首先要对这个 JWT 的完整性进行验证,这个就是签名认证。它验证的方法其实很简单,只要把 header 做 base64 url 解码,就能知道 JWT 用的什么算法做的签名,然后用这个算法,再次用同样的逻辑对 header 和 payload 做一次签名,并比较这个签名是否与 JWT 本身包含的第三个部分的串是否完全相同,只要不同,就可以认为这个 JWT 是一个被篡改过的串,自然就属于验证失败了。接收方生成签名的时候必须使用跟 JWT 发送方相同的密钥,意味着要做好密钥的安全传递或共享。
话不多说,直接给代码:
/** * 解析 Token * * @param tokenStr JWT Token */ public JWebToken parse(String tokenStr) { String[] parts = tokenStr.split("\\."); if (parts.length != 3) throw new IllegalArgumentException("无效 Token 格式"); if (!JWebToken.encodedHeader.equals(parts[0])) throw new IllegalArgumentException("非法的 JWT Header: " + parts[0]); String json = Utils.decode(parts[1]); Payload payload = JsonHelper.parseMapAsBean(json, Payload.class); if (payload == null) throw new RuntimeException("Payload is Empty: "); if (payload.getExp() == null) throw new RuntimeException("Payload 不包含过期字段 exp:" + payload); JWebToken token = new JWebToken(payload); token.setSignature(parts[2]); return token; } /** * 校验是否合法的 Token * * @param token 待检验的 Token * @return 是否合法 */ public boolean isValid(JWebToken token) { String _token = signature(token); System.out.println(">>>" + token.getSignature()); System.out.println(":::" + _token); return token.getPayload().getExp() > Utils.now() //token not expired && token.getSignature().equals(_token); //signature matched }
JWT 我也是刚接触,如果有不对的地方敬请提出!
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。