写点什么

基于 jwt 的 token 验证、原理及流程

作者:TimeFriends
  • 2022 年 8 月 15 日
    北京
  • 本文字数:4539 字

    阅读完需:约 15 分钟

基于jwt的token验证、原理及流程

一、什么是 JWT


Json web token (JWT), 是为了在网络应用环境间传递声明而执行的一种基于 JSON 的开放标准((RFC 7519).


该 token 被设计为紧凑且安全的,特别适用于分布式站点的单点登录(SSO)场景。


JWT 的声明一般被用来在身份提供者和服务提供者间传递被认证的用户身份信息,以便于从资源服务器获取资源,也可以增加一些额外的其它业务逻辑所必须的声明信息,该 token 也可直接被用于认证,也可被加密。


二、JWT 的组成


1、JWT 生成编码后的样子


eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.UQmqAUhUrpDVV2ST7mZKyLTomVfg7sYkEjmdDI5XF8Q
复制代码


2、JWT 由三部分构成


第一部分我们称它为头部(header),第二部分我们称其为载荷(payload, 类似于飞机上承载的物品),第三部分是签证(signature).


header


jwt 的头部承载两部分信息:


  • 声明类型,这里是 jwt

  • 声明加密的算法 通常直接使用 HMAC SHA256


完整的头部就像下面这样的 JSON:


{  'typ': 'JWT',  'alg': 'HS256'}
复制代码


然后将头部进行 base64 加密(该加密是可以对称解密的),构成了第一部分


eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9
复制代码


playload


载荷就是存放有效信息的地方。这个名字像是特指飞机上承载的货品,这些有效信息包含三个部分


  • 标准中注册的声明

  • 公共的声明

  • 私有的声明


标准中注册的声明 (建议但不强制使用) :


  • iss: jwt 签发者

  • sub: jwt 所面向的用户

  • aud: 接收 jwt 的一方

  • exp: jwt 的过期时间,这个过期时间必须要大于签发时间

  • nbf: 定义在什么时间之前,该 jwt 都是不可用的.

  • iat: jwt 的签发时间

  • jti: jwt 的唯一身份标识,主要用来作为一次性 token,从而回避重放攻击。


公共的声明 :


公共的声明可以添加任何的信息,一般添加用户的相关信息或其他业务需要的必要信息.但不建议添加敏感信息,因为该部分在客户端可解密.


私有的声明 :


私有声明是提供者和消费者所共同定义的声明,一般不建议存放敏感信息,因为 base64 是对称解密的,意味着该部分信息可以归类为明文信息。


定义一个 payload:


{  "sub": "1234567890",  "name": "John Doe",  "admin": true}
复制代码


然后将其进行 base64 加密,得到 Jwt 的第二部分


eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9
复制代码


signature


jwt 的第三部分是一个签证信息,这个签证信息由三部分组成:


  • header (base64 后的)

  • payload (base64 后的)

  • secret


这个部分需要 base64 加密后的 header 和 base64 加密后的 payload 使用.连接组成的字符串(头部在前),然后通过 header 中声明的加密方式进行加盐 secret 组合加密,然后就构成了 jwt 的第三部分。


UQmqAUhUrpDVV2ST7mZKyLTomVfg7sYkEjmdDI5XF8Q
复制代码


密钥 secret 是保存在服务端的,服务端会根据这个密钥进行生成 token 和验证,所以需要保护好。


3、签名的目的


最后一步签名的过程,实际上是对头部以及载荷内容进行签名。一般而言,加密算法对于不同的输入产生的输出总是不一样的。对于两个不同的输入,产生同样的输出的概率极其地小(有可能比我成世界首富的概率还小)。所以,我们就把“不一样的输入产生不一样的输出”当做必然事件来看待吧。


所以,如果有人对头部以及载荷的内容解码之后进行修改,再进行编码的话,那么新的头部和载荷的签名和之前的签名就将是不一样的。而且,如果不知道服务器加密的时候用的密钥的话,得出来的签名也一定会是不一样的。


服务器应用在接受到 JWT 后,会首先对头部和载荷的内容用同一算法再次签名。那么服务器应用是怎么知道我们用的是哪一种算法呢?别忘了,我们在 JWT 的头部中已经用 alg 字段指明了我们的加密算法了。


如果服务器应用对头部和载荷再次以同样方法签名之后发现,自己计算出来的签名和接受到的签名不一样,那么就说明这个 Token 的内容被别人动过的,我们应该拒绝这个 Token,返回一个 HTTP 401 Unauthorized 响应。


注意:在 JWT 中,不应该在载荷里面加入任何敏感的数据,比如用户的密码。


4、如何应用


一般是在请求头里加入 Authorization,并加上 Bearer 标注:


fetch('api/user/1', {  headers: {    'Authorization': 'Bearer ' + token  }})
复制代码


服务端会验证 token,如果验证通过就会返回相应的资源。


5、安全相关


  • 不应该在 jwt 的 payload 部分存放敏感信息,因为该部分是客户端可解密的部分。

  • 保护好 secret 私钥,该私钥非常重要。

  • 如果可以,请使用 https 协议


6、对 Token 认证的五点认识


  • 一个 Token 就是一些信息的集合;

  • 在 Token 中包含足够多的信息,以便在后续请求中减少查询数据库的几率;

  • 服务端需要对 cookie 和 HTTP Authrorization Header 进行 Token 信息的检查;

  • 基于上一点,你可以用一套 token 认证代码来面对浏览器类客户端和非浏览器类客户端;

  • 因为 token 是被签名的,所以我们可以认为一个可以解码认证通过的 token 是由我们系统发放的,其中带的信息是合法有效的;


三、传统的 session 认证


我们知道,http 协议本身是一种无状态的协议,而这就意味着如果用户向我们的应用提供了用户名和密码来进行用户认证,那么下一次请求时,用户还要再一次进行用户认证才行,因为根据 http 协议,我们并不能知道是哪个用户发出的请求,所以为了让我们的应用能识别是哪个用户发出的请求,我们只能在服务器存储一份用户登录的信息,这份登录信息会在响应时传递给浏览器,告诉其保存为 cookie,以便下次请求时发送给我们的应用,这样我们的应用就能识别请求来自哪个用户了,这就是传统的基于 session 认证。


但是这种基于 session 的认证使应用本身很难得到扩展,随着不同客户端用户的增加,独立的服务器已无法承载更多的用户,而这时候基于 session 认证应用的问题就会暴露出来。


基于 session 认证所显露的问题


Session: 每个用户经过我们的应用认证之后,我们的应用都要在服务端做一次记录,以方便用户下次请求的鉴别,通常而言 session 都是保存在内存中,而随着认证用户的增多,服务端的开销会明显增大。


扩展性: 用户认证之后,服务端做认证记录,如果认证的记录被保存在内存中的话,这意味着用户下次请求还必须要请求在这台服务器上,这样才能拿到授权的资源,这样在分布式的应用上,相应的限制了负载均衡器的能力。这也意味着限制了应用的扩展能力。


CSRF: 因为是基于 cookie 来进行用户识别的, cookie 如果被截获,用户就会很容易受到跨站请求伪造的攻击。


基于 token 的鉴权机制


基于 token 的鉴权机制类似于 http 协议也是无状态的,它不需要在服务端去保留用户的认证信息或者会话信息。这就意味着基于 token 认证机制的应用不需要去考虑用户在哪一台服务器登录了,这就为应用的扩展提供了便利。


流程上是这样的:


  • 用户使用用户名密码来请求服务器

  • 服务器进行验证用户的信息

  • 服务器通过验证发送给用户一个 token

  • 客户端存储 token,并在每次请求时附送上这个 token 值

  • 服务端验证 token 值,并返回数据


这个 token 必须要在每次请求时传递给服务端,它应该保存在请求头里, 另外,服务端要支持 CORS(跨来源资源共享)策略,一般我们在服务端这么做就可以了 Access-Control-Allow-Origin:*。


四、token 的优点


  • 支持跨域访问: Cookie 是不允许垮域访问的,这一点对 Token 机制是不存在的,前提是传输的用户认证信息通过 HTTP 头传输。

  • 无状态(也称:服务端可扩展行):Token 机制在服务端不需要存储 session 信息,因为 Token 自身包含了所有登录用户的信息,只需要在客户端的 cookie 或本地介质存储状态信息。

  • 更适用 CDN: 可以通过内容分发网络请求你服务端的所有资料(如:javascript,HTML,图片等),而你的服务端只要提供 API 即可。

  • 去耦: 不需要绑定到一个特定的身份验证方案。Token 可以在任何地方生成,只要在你的 API 被调用的时候,你可以进行 Token 生成调用即可。

  • 更适用于移动应用: 当你的客户端是一个原生平台(iOS, Android,Windows 8 等)时,Cookie 是不被支持的(你需要通过 Cookie 容器进行处理),这时采用 Token 认证机制就会简单得多。

  • CSRF:因为不再依赖于 Cookie,所以你就不需要考虑对 CSRF(跨站请求伪造)的防范。

  • 性能: 一次网络往返时间(通过数据库查询 session 信息)总比做一次 HMACSHA256 计算 的 Token 验证和解析要费时得多。

  • 不需要为登录页面做特殊处理: 如果你使用 Protractor 做功能测试的时候,不再需要为登录页面做特殊处理。

  • 基于标准化:你的 API 可以采用标准化的 JSON Web Token (JWT). 这个标准已经存在多个后端库(.NET, Ruby, Java,Python, PHP)和多家公司的支持(如:Firebase,Google, Microsoft)。

  • 因为 json 的通用性,所以 JWT 是可以进行跨语言支持的,像 JAVA,JavaScript,NodeJS,PHP 等很多语言都可以使用。

  • 因为有了 payload 部分,所以 JWT 可以在自身存储一些其他业务逻辑所必要的非敏感信息。

  • 便于传输,jwt 的构成非常简单,字节占用很小,所以它是非常便于传输的。

  • 它不需要在服务端保存会话信息, 所以它易于应用的扩展。


五、JWT 的 JAVA 实现


Java 中对 JWT 的支持可以考虑使用 JJWT 开源库;JJWT 实现了 JWT, JWS, JWE 和 JWA RFC 规范;


下面将简单举例说明其使用:


1、生成 Token 码


import javax.crypto.spec.SecretKeySpec;import javax.xml.bind.DatatypeConverter;import java.security.Key;import io.jsonwebtoken.*;import java.util.Date;     //Sample method to construct a JWT private String createJWT(String id, String issuer, String subject, long ttlMillis) { //The JWT signature algorithm we will be using to sign the tokenSignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256; long nowMillis = System.currentTimeMillis();Date now = new Date(nowMillis); //We will sign our JWT with our ApiKey secretbyte[] apiKeySecretBytes = DatatypeConverter.parseBase64Binary(apiKey.getSecret());Key signingKey = new SecretKeySpec(apiKeySecretBytes, signatureAlgorithm.getJcaName());   //Let's set the JWT ClaimsJwtBuilder builder = Jwts.builder().setId(id)                                .setIssuedAt(now)                                .setSubject(subject)                                .setIssuer(issuer)                                .signWith(signatureAlgorithm, signingKey); //if it has been specified, let's add the expirationif (ttlMillis >= 0) {    long expMillis = nowMillis + ttlMillis;    Date exp = new Date(expMillis);    builder.setExpiration(exp);} //Builds the JWT and serializes it to a compact, URL-safe stringreturn builder.compact();}
复制代码


2、解码和验证 Token 码


import javax.xml.bind.DatatypeConverter;import io.jsonwebtoken.Jwts;import io.jsonwebtoken.Claims; //Sample method to validate and read the JWTprivate void parseJWT(String jwt) {//This line will throw an exception if it is not a signed JWS (as expected)Claims claims = Jwts.parser()           .setSigningKey(DatatypeConverter.parseBase64Binary(apiKey.getSecret()))   .parseClaimsJws(jwt).getBody();System.out.println("ID: " + claims.getId());System.out.println("Subject: " + claims.getSubject());System.out.println("Issuer: " + claims.getIssuer());System.out.println("Expiration: " + claims.getExpiration());}
复制代码


用户头像

TimeFriends

关注

加油! 年轻人! 2022.08.02 加入

这里没有天赋异禀,也没有天资聪颖,只有每天的陪伴。万物瞬息万变,但唯一不变的只有变化。抓住变化的根本,以时间为伍,以坚持为伴,做时间的朋友。

评论

发布
暂无评论
基于jwt的token验证、原理及流程_8月月更_TimeFriends_InfoQ写作社区