解析结果如下:
当前解析的JWT:eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJ0aHJvd3giLCJqaWQiOjEwMDg3LCJleHAiOjE2MTMyMjc0NjgxNjh9.7skduDGxV-BP2p_CXyr3Na7WBvENNl--Pm4HQ8cJuEs 解析结果如下: HEADER:PartContent(part=HEADER, rawContent=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9, pairs={typ=JWT, alg=HS256}) PAYLOAD:PartContent(part=PAYLOAD, rawContent=eyJpc3MiOiJ0aHJvd3giLCJqaWQiOjEwMDg3LCJleHAiOjE2MTMyMjc0NjgxNjh9, pairs={iss=throwx, jid=10087, exp=1613227468168}) SIGNATURE:PartContent(part=SIGNATURE, rawContent=7skduDGxV-BP2p_CXyr3Na7WBvENNl--Pm4HQ8cJuEs, pairs=null)验证JWT建立在解析JWT完成的基础之上,需要对解析出来的头部参数和有效载做一次MAC签名,与解析出来的签名做校对。另外,可以自定义校验具体的Claim项,如过期时间和发行者等。一般校验失败会针对不同的情况定制不同的运行时异常便于区分场景,这里为了方便统一抛出IllegalStateException:
public void verify(String jwt) throws Exception { System.out.println("当前校验的JWT:" + jwt); Map<Part, PartContent> parseResult = parse(jwt); PartContent headerContent = parseResult.get(Part.HEADER); PartContent payloadContent = parseResult.get(Part.PAYLOAD); PartContent signatureContent = parseResult.get(Part.SIGNATURE); String signature = generateSignaturePart(headerContent.getRawContent(), payloadContent.getRawContent()); if (!Objects.equals(signature, signatureContent.getRawContent())) { throw new IllegalStateException("签名校验异常"); } String iss = payloadContent.getPairs().get("iss").toString(); // iss校验 if (!Objects.equals(iss, "throwx")) { throw new IllegalStateException("ISS校验异常"); } long exp = Long.parseLong(payloadContent.getPairs().get("exp").toString()); // exp校验,有效期14天 if (System.currentTimeMillis() - exp > 24 * 3600 * 1000 * 14) { throw new IllegalStateException("exp校验异常,JWT已经过期"); } // 省略其他校验项 System.out.println("JWT校验通过"); }类似地,用上面生成过的JWT进行验证,结果如下:
当前校验的JWT:eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJ0aHJvd3giLCJqaWQiOjEwMDg3LCJleHAiOjE2MTMyMjc0NjgxNjh9.7skduDGxV-BP2p_CXyr3Na7WBvENNl--Pm4HQ8cJuEs 当前解析的JWT:eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJ0aHJvd3giLCJqaWQiOjEwMDg3LCJleHAiOjE2MTMyMjc0NjgxNjh9.7skduDGxV-BP2p_CXyr3Na7WBvENNl--Pm4HQ8cJuEs 23:33:00.174 [main] INFO club.throwable.jwt.JsonWebToken - 生成的Signature部分为:7skduDGxV-BP2p_CXyr3Na7WBvENNl--Pm4HQ8cJuEs JWT校验通过上面的代码存在硬编码问题,只是为了用最简单的JWS实现方式重新实现了JWT的生成、解析和校验过程,算法也使用了复杂程度和安全性极低的HS256,所以在生产中并不推荐花大量时间去实现JWS,可以选用现成的JWT类库,如auth0和jjwt。
JWT的使用场景和实战JWT本质是一个令牌,更多场景下是作为会话ID(session_id)使用,作用是'维持会话的粘性'和携带认证信息(如果用JWT术语,应该是安全地传递Claims)。笔者记得很久以前使用的一种Session ID解决方案是由服务端生成和持久化Session ID,返回的Session ID需要写入用户的Cookie,然后用户每次请求必须携带Cookie,Session ID会映射用户的一些认证信息,这一切都是由服务端管理,一个很常见的例子就是Tomcat容器中出现的J(ava)SESSIONID。与之前的方案不同,JWT是一种无状态的令牌,它并不需要由服务端保存,携带的数据或者会话的数据都不需要持久化,使用JWT只需要关注Claims的完整性和合法性即可,生成JWT时候所有有效数据已经通过编码存储在JWT字符串中。正因JWT是无状态的,一旦颁发后得到JWT的客户端都可以通过它与服务端交互,JWT一旦泄露有可能造成严重安全问题,因此实践的时候一般需要做几点:
JWT需要设置有效期,也就是exp这个Claim必须启用和校验
JWT需要建立黑名单,一般使用jti这个Claim即可,技术上可以使用布隆过滤器加数据库的组合(数量少的情况下简单操作甚至可以用Redis的SET数据类型)
JWS的签名算法尽可能使用安全性高的算法,如RSXXX
Claims尽可能不要写入敏感信息
高风险场景如支付操作等不能仅仅依赖JWT认证,需要进行短信、指纹等二次认证
PS:身边有不少同事所在的项目会把JWT持久化,其实这违背了JWT的设计理念,把JWT当成传统的会话ID使用了