Home JWT
Post
Cancel

JWT

什么是JWT

Json web token (JWT), 是为了在网络应用环境间传递声明而执行的一种基于JSON的开放标准(RFC 7519)。该Token被设计为紧凑且安全的,特别适用于分布式站点的单点登录(SSO)场景。JWT的声明一般被用来在身份提供者和服务提供者间传递被认证的用户身份信息,以便于从资源服务器获取资源,也可以增加一些额外的其它业务逻辑所必须的声明信息,该Token也可直接被用于认证,也可被加密。

JWT的组成

第一部分称为头部(header),第二部分称为载荷(payload),第三部分是签证(signature)。

  • header

    JWT的头部承载两部分信息:声明类型,这里是JWT;声明加密的算法,通常直接使用HMAC SHA256,完整的头部就像下面这样的JSON:

    1
    2
    3
    4
    
      {
          "typ": "JWT",
          "alg": "HS256"
      }
    

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

    1
    
      eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9
    
  • playload

    载荷就是存放有效信息的地方:标准中注册的声明、公共的声明、私有的声明。

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

      1
      2
      3
      4
      5
      6
      7
      
        iss: JWT签发者
        sub: JWT所面向的用户
        aud: 接收JWT的一方
        exp: JWT的过期时间,这个过期时间必须要大于签发时间
        nbf: 定义在什么时间之前,该JWT都是不可用的.
        iat: JWT的签发时间
        jti: JWT的唯一身份标识,主要用来作为一次性token,从而回避重放攻击。
      
    • 公共的声明

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

    • 私有的声明

      私有声明是提供者和消费者所共同定义的声明,一般不建议存放敏感信息,因为BASE64是对称解密的,意味着该部分信息可以归类为明文信息。这个指的就是自定义的声明。这些声明跟JWT标准规定的声明区别在于:JWT规定的声明,JWT的接收方在拿到JWT之后,都知道怎么对这些标准的声明进行验证;而私有的声明不会验证,除非明确告诉接收方要对这些声明进行验证以及规则才行。

    定义一个payload

    1
    2
    3
    4
    5
    
      {
          "sub": "1234567890",
          "name": "John Doe",
          "admin": true
      }
    

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

    1
    
      eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9
    
  • signature

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

    1
    2
    3
    
      header (BASE64后的)
      payload (BASE64后的)
      secret
    

    这个部分需要BASE64加密后的header和BASE64加密后的payload使用.连接组成的字符串,然后通过header中声明的加密方式进行组合加密,然后就构成了JWT的第三部分。

    1
    2
    3
    4
    
      // javascript
      var encodedString = base64UrlEncode(header) + '.' + base64UrlEncode(payload);
      var signature = HMACSHA256(encodedString, 'secret'); 
      // TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ
    

将这三部分用.连接成一个完整的字符串,构成了最终的JWT:

1
2
3
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM
0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV
9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ

注意:secret是保存在服务器端的,JWT的签发生成也是在服务器端的,secret就是用来进行JWT的签发和JWT的验证,所以,它就是你服务端的私钥,在任何场景都不应该流露出去。一旦客户端得知这个secret, 那就意味着客户端是可以自我签发JWT了。

JWT有什么好处

  • 支持跨域访问: Cookie是不允许垮域访问的,这一点对Token机制是不存在的,前提是传输的用户认证信息通过HTTP头传输。
  • 无状态(也称:服务端可扩展行): Token机制在服务端不需要存储Session信息,因为Token自身包含了所有登录用户的信息,只需要在客户端的Cookie或本地介质存储状态信息。
  • 更适用CDN: 可以通过内容分发网络请求你服务端的所有资料(如:JavaScript,HTML,图片等),而你的服务端只要提供API即可。
  • 去耦: 不需要绑定到一个特定的身份验证方案。Token可以在任何地方生成,只要在你的API被调用的时候,你可以进行Token生成调用即可。
  • 更适用于移动应用: 当你的客户端是一个原生平台(iOS,Android,Windows 8等)时,Cookie是不被支持的(你需要通过Cookie容器进行处理),这时采用Token认证机制就会简单得多。
  • CSRF: 因为不再依赖于Cookie,所以你就不需要考虑对CSRF(跨站请求伪造)的防范。一般建议使用:1、在HTTP请求中以参数的形式加入一个服务器端产生的token,或者,2、放入http请求头中也就是一次性给所有该类请求加上csrftoken这个HTTP头属性,并把token值放入其中。
  • 性能: 一次网络往返时间(通过数据库查询Session信息)总比做一次HMACSHA256计算的Token验证和解析要费时得多。
  • 不需要为登录页面做特殊处理: 如果你使用Protractor做功能测试的时候,不再需要为登录页面做特殊处理。
  • 基于标准化:你的API可以采用标准化的JSON Web Token (JWT)。这个标准已经存在多个后端库(.NET,Ruby,Java,Python,PHP)和多家公司的支持(如:FireBASE,Google,Microsoft)。

Java中的实现

在Java的实现中可以有两种方式,一种是不借助第三方jar,自己生成token,另一种的借助第三方jar,传入自己需要的负荷信息,生成token。

  • 自己生成Token

    header和poyload的组成都是JSON字符串,所以先创建头部的JSON,然后用BASE64编码org.apache.axis.encoding.Base64。然后再创建负荷的JSON,然后也同样用BASE64编码,这样就生成了两个字符串,然后用.拼接到一起就形成了现在的形式eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJmcm9tX3VzZXIiOiJCIiwidGFyZ2V0X3VzZXIiOiJBIn0。接下来对上边拼接成的字符串进行HS256的算法加密生成sign签名,这里需要自己手动去写一个类,然后提供一个静态方法供外界的调用,类的实现代码如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    
     import javax.crypto.Mac;  
     import javax.crypto.spec.SecretKeySpec;  
     import org.apache.commons.codec.binary.Base64;  
    
     public class HS256 {  
         public static String returnSign(String message) {  
             String hash = "";  
             // 别人篡改数据,但是签名的密匙是在服务器存储,
             // 密匙不同,生成的sign也不同。  
             // 所以根据sign的不同就可以知道是否篡改了数据。  
             String secret = "mystar"; // 密匙  
             try {  
                 Mac sha256_HMAC = Mac.getInstance("HmacSHA256");  
                 SecretKeySpec secret_key = new SecretKeySpec(secret.getBytes(), 
                                          "HmacSHA256");  
                 sha256_HMAC.init(secret_key);  
                 hash = Base64.encodeBase64String(sha256_HMAC
                                             .doFinal(message.getBytes()));  
                 System.out.println(message + "#####" + hash);  
             } catch (Exception e) {  
                 System.out.println("Error");  
             }  
             return hash;  
         }  
     }  
    

    这样Token的三部分就生成了,然后当做参数传到前台,用Cookie存储就可以在同一客户端调用了。当从客户端带过来Token参数的时候,直接对头部和负荷再次调用加密算法,看生成的新的签名和之前的签名是否一致,判断数据是否被篡改。

  • 借用第三方的jar(jjwt-0.7.0.jar)生成token

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    
      public static String createJWT(String py) {
          System.out.println("负荷的值:" + py);
          SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;
          // 此处就是服务器定义的自己的秘钥
          String authJJm = PropertiesUtil.getValue("authJm");
          byte[] apiKeySecretBytes = DatatypeConverter.parseBase64Binary(authJm);
          Key signingKey = new SecretKeySpec(apiKeySecretBytes, 
                                      signatureAlgorithm.getJcaName());
          JwtBuilder builder = Jwts.builder()
              .setHeaderParam("typ", "JWT")
              .setHeaderParam("alg", "HS256")
              .setPayload(py) // 负荷
              .signWith(signatureAlgorithm, signingKey); // 部分签名,用HS256加密
          return builder.compact();
      }
    

    调用这个方法会自动对header和poyload进行BASE64的编码,用的是jar包自带的io.jsonwebtoken.impl.Base64Codec,跟自己生成Token时,用的BASE64的jar不一样,自己在此列出来:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    
      public static void main(String[] args) {    
          JSONObject json_header = new JSONObject();  
          json_header.put("typ", "JWT"); // 类型  
          json_header.put("alg", "HS256"); // 加密算法  
          String header = Base64Codec.BASE64URL.encode(json_header.toString()  
          .getBytes());  
          String aa = Base64Codec.BASE64URL.decodeToString(header);  
          System.out.println(header + "--" + aa);  
      }  
    

    接着上边createJWT()方法说,只要把自己定义的负荷JSON串当做参数传入就行,并且签名也会对应的生成,返回一个完整的Token。在测试的过程中,发现即使自己不定义Token的头部,也会自动生成header,只是里边没有typ这样的参数定义,只有HS256,这里源码里边,默认了alg的value。这样执行完,把生成的Token就当做参数传到前台,存储在Cookie里边。然后再说一下,前台带过来Token参数时候,怎么处理,看代码:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    
      public static Claims parseJWT(String jwt) {
          // 秘钥,跟生成Token时对应一致
          String authJm = PropertiesUtil.getValue("authJm"); 
          if (jwt.split("\\.").length == 3) {
              String head = jwt.split("\\.")[0]; // 头部
              String payload = jwt.split("\\.")[1]; // 负荷
              // System.out.println(Base64Codec.BASE64URL.decodeToString(payload));
              String sign = jwt.split("\\.")[2]; // 签名
              JwsHeader claims1 = Jwts.parser().setSigningKey(DatatypeConverter
                              .parseBase64Binary(authJm))
                              .parseClaimsJws(jwt).getHeader(); // 头部信息
              System.out.println("头部:" + claims1.toString());
              Claims claims = Jwts.parser().setSigningKey(DatatypeConverter
                              .parseBase64Binary(authJm))
                              .parseClaimsJws(jwt).getBody(); // 负荷信息
              // 传入负荷,再次调用返回签名,看是否一致
              String sign_new = createJWT(JSONObject.toJSONString(claims))
                                              .split("\\.")[2]; 
              if (sign_new.equals(sign)) {
                  System.out.println("匹配一致,数据没有篡改");
              }
              return claims;
          } else {
              return null;
          }
      }
    

    这个过程的原理跟自己生成Token判断原理一样,都是重新生成sign,只是一个是调用自己的方法,另外一个是调用第三方的方法。

Token过期时间

这个相对来说不是太复杂,可以在负荷里边多带一个参数,把过期时间放进去,其实里边有一个exp标签名就是存储过期时间字段的,存储的是时间戳。

1
2
3
4
5
// 存储
long nowMillis = System.currentTimeMillis();
long expMillis = nowMillis + 1000*2; // 设置Token 2秒过期
// 获取
Date aa = new Date(Long.parseLong(claims.get("aa").toString()));

可以存储一个生成Token时间和Token过期时间,然后服务器接收到的时候,可以根据当前的时间去判断。当前时间大于Token生成时间并且小于Token过期时间的情况下,继续走接下来的业务操作。

参考

基于Token的WEB后台认证机制
【JWT】JWT+HA256加密 Token验证
JSON Web Token - 在Web应用间安全地传递信息

This post is licensed under CC BY 4.0 by the author.