开发者

Spring框架整合Java Web Token问题

开发者 https://www.devze.com 2024-09-06 10:18 出处:网络 作者: Sams-ara
目录Java Web TokenJWT组成荷载头部签名Spring使用JWTMaven配置方式JWT算法(了解)使用方法创建JWT时间验证信息解析自定义字段实例总结Java Web Token
目录
  • Java Web Token
    • JWT组成
    • 荷载
    • 头部
    • 签名
  • Spring使用JWT
    • Maven配置方式
    • JWT算法(了解)
    • 使用方法
    • 创建JWT
    • 时间验证
    • 信息解析
    • 自定义字段
  • 实例
    • 总结

      Java Web Token

      jsON Web Token(JWT)是一个非常轻巧的规范。

      这个规范允许我们使用JWT在用户和服务器之间传递安全可靠的信息。

      JWT组成

      一个JWT实际上就是一个字符串,它由三部分组成,头部、载荷与签名。

      荷载

      {
      "iss": "John Wu JWT",
      "iat": 1441593502,
      "exp": 1441594722,
      "aud": "www.example.com",
      "sub": "jrocket@example.com",
      "from_user": "B",
      "target_user": "A"
      }

      这里面的前五个字段都是由JWT的标准所定义的。

      • iss: 该JWT的签发者
      • sub: 该JWT所面向的用户
      • aud: 接收该JWT的一方
      • exp(expires): 什么时候过期,这里是一个Unix时间戳
      • iat(issued at): 在什么时候签发的

      这些定义都可以在标准中找到。

      将上面的JSON对象进行[base64编码]可以得到下面的字符串。

      这个字符串我们将它称作JWT的Payload(载荷)。

      eyJpc3MiOiJKb2huIFd1IEpXVCIsImlhdCI6MTQ0MTU5MzUwMiwiZXhwIjoxNDQxNTk0NzIyLCJhdwQiOiJ3d3cuZXhhbXBsZS5jb20iLCJzdWIiOiJqcm9ja2V0QGV4YW1wbGUuY29tIiwiZnJvbV91c2VyIjoiQiIsInRhcmdldF91c2VyIjoiQSJ9

      头部

      JWT还需要一个头部,头部用于描述关于该JWT的最基本的信息,例如其类型以及签名所用的算法等。

      这也可以被表示成一个JSON对象。

      {
      “typ”: “JWT”,
      “alg”: “HS256”
      }

      在这里,我们说明了这是一个JWT,并且我们所用的签名算法(后面会提到)是HS256算法。(算法根据实际情况而变)

      eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9

      签名

      签名就是将荷载和头部编码用.号连接在一起就形成了

      eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJmcm9tX3VzZXIiOiJCIiwidGFyZ2V0X3VzZXIiOiJBIn0

      我们将上面拼接完的字符串用HS256算法进行加密。

      在加密的时候,我们还需要提供一个密钥(secret)。

      如果我们用mystar作为密钥的话,那么就可以得到我们加密后的内容

      rSWamyAYwuHCo7IFAgd1oRpSP7nzL7BF5t7ItqpKViM

      最后将这一部分签名也拼接在被签名的字符串后面,我们就得到了完整的JWT

      eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJmcm9tX3VzZXIiOiJCIiwidGFyZ2V0X3VzZXIiOiJBIn0.rSWamyAYwuHCo7IFAgd1oRpSP7nzL7BF5t7ItqpKViM

      签名的目的:

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

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

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

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

      我们可以看到,JWT适合用于向Web应用传递一些非敏感信息。例如在上面提到的完成加好友的操作,还有诸如下订单的操作等等。

      其实JWT还经常用于设计用户认证和授权系统,甚至实现Web应用的单点登录。

      Spring使用JWT

      Maven配置方式

      <dependency>
          <groupId>com.auth0</groupId>
          <artifactId>java-jwt</artifactId>
          <version>3.2.0</version>
      </dependency>

      JWT算法(了解)

      JWS算法介绍
      HS256HMAC256HMAC with SHA-256
      HS384HMAC384HMAC with SHA-384
      HS512HMAC512HMAC with SHA-512
      RS256RSA256RSASSA-PKCS1-v1_5 with SHA-256
      RS384RSA384RSASSA-PKCS1-v1_5 with SHA-384
      RS512RSA512RSASSA-PKCS1-v1_5 with SHA-512
      ES256ECDSA256ECDSA with curve P-256 and SHA-256
      ES384ECDSA384ECDSA with curve P-384 and SHA-384
      ES512ECDSA512ECDSA with curve P-521 and SHA-512

      使用方法

      选择算法:

      算法定义了一个令牌是如何被签名和验证的。它可以用HMAC算法的原始值来实例化,也可以在RSA和ECDSA算法的情况下对密钥对或密钥提供程序进行实例化。创建后,该实例可用于令牌签名和验证操作。

      在使用RSA或ECDSA算法时,只需要签署JWTs,就可以通过传递null值来避免指定公钥。当您需要验证JWTs时,也可以使用私钥进行操作

      使用静态的字符密文或者key来获取算法器:

      //HMAC
      Algorithm algorithmHS = Algorithm.HMAC256("secret");
      
      //RSA
      RSAPublicKey publicKey = //Get the key instance
      RSAPrivateKey privateKey = //Get the key instance
      Algorithm algorithmRS = Algorithm.RSA256(publicKey, privateKey);

      使用一个key提供者来获取算法:

      通过使用KeyProvider,您可以在运行时更改密钥,用于验证令牌签名或为RSA或ECDSA算法签署一个新的令牌。

      这是通过实现RSAKeyProvider或ECDSAKeyProvider方法实现的:

      • getPublicKeyById(String kid): 它在令牌签名验证中调用,它应该返回用于验证令牌的密钥。如果使用了关键的轮换,例如JWK,它可以使用id来获取正确的轮换键(或者只是一直返回相同的键)。
      • getPrivateKey(): 在令牌编程客栈签名期间调用它,它应该返回用于签署JWT的密钥。
      • getPrivateKeyId():在令牌签名期间调用它,它应该返回标识由getPrivateKey()返回的键的id的id。这个值比JWTCreator.Builder和keyid(String)方法中的值更受欢迎。如果您不需要设置孩子的值,就避免使用KeyProvider实例化算法。

      创建JWT

      try {
      Algorithm algorithm = Algorithm.HMAC256("secret");
      String token = JWT.create()
          .withIssuer("auth0")
          .sign(algorithm);
      } catch (UnsupportedEncodingException exception){
          //UTF-8 encoding not supported
      } catch (JWTCreationException exception){
          //Invalid Signing configuration / Couldn't convert Claims.
      }

      如果Claim不能转换为JSON,或者在签名过程中使用的密钥无效,那么将会抛出JWTCreationException异常。

      验证令牌

      首先需要通过调用jwt.require()和传递算法实例来创建一个JWTVerifier实例。

      如果您要求令牌具有特定的Claim值,请使用构建器来定义它们。

      方法build()返回的实例是可重用的,因此您可以定义一次,并使用它来验证不同的标记。

      最后调用verifier.verify()来验证token

      String token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXUyJ9.eyJpc3MiOiJhdXRoMCJ9.AbIJTDMFc7yUa5MhvcP03nJPyCPzZtQcGEp-zWfOkEE";
      try {
          Algorithm algorithm = Algorithm.HMAC256("secret");
          JWTVerifier verifier = JWT.require(algorithm)
              .withIssuer("auth0")
              .build(); //Reusable verifier instance
          DecodedJWT jwt = verifier.verify(token);
      } catch (UnsupportedEncodingException exception){
          //UTF-8 encoding not supported
      } catch (JWTVerificationException exception){
          //Invalid signature/claims
      }

      时间验证

      当验证一个令牌时,时间验证会自动发生,导致在值无效时抛出一个JWTVerificationException。如果前面的任何一个字段都丢失了,那么在这个验证中就不会考虑这些字段。

      要指定令牌仍然被认为有效的余地窗口,在JWTVerifier builder中使用accept回旋()方法,并传递一个正值的秒值。这适用于上面列出的每一项。

      JWTVerifier verifier = JWT.require(algorithm)
      .acceptLeeway(1) // 1 sec for nbf, iat and exp
      .build();

      您还可以为给定的日期声明指定一个自定义值,并为该声明覆盖缺省值。

      JWTVerifier verifier = JWT.require(algorithm)
      .acceptLeeway(1)   //1 sec for nbf and iat
      .acceptExpiresAt(5)   //5 secs for exp
      .build();

      信息解析

      Algorithm (“alg”)

      返回jwt的算法值或,如果没有定义则返回null

      String algorithm = jwt.getAlgorithm();

      如果您需要在您的lib/app中测试此行为,将验证实例转换为basever可视化,以获得verific.build()方法的可见性,该方法可以接受定制的时钟。

      例如:

      BaseVerification verification = (BaseVerification) JWT.require(algorithm)
      .acceptLeeway(1)
      .acceptExpiresAt(5);
      Clock clock = new CustomClock(); //Must implement Clock interface
      JWTVerifier verifier = verification.build(clock);

      Type (“typ”)

      返回jwt的类型值,如果没有定义则返回null(多数情况类型值为jwt)

      String type = jwt.getType();

      Content Type (“cty”)

      返回内容的类型,如果没有定义则返回null

      String contentType = jwt.getContentType();

      Key Id (“kid”)

      返回key的id值,如果没有定义则返回null

      String keyId = jwt.getKeyId();

      自定义字段

      在令牌的头部中定义的附加声明可以通过调用getHeaderClaim() 获取,即使无法找到,也会返回。您可以通过调用claim.isNull()来检查声明的值是否为null。

      Claim claim = jwt.getHeaderClaim("owner");

      当使用jwt.create()创建一个令牌时,您可以通过调用withHeader()来指定头声明,并同时传递声明的映射。

      Map<String, Object> headerClaims = new HashMap();
      headerClaims.put("owner", "auth0");
      String token = JWT.create()
          .withHeader(headerClaims)
          .sign(algorithm);

      提示:在签名过程之后,alg和typ值将始终包含在Header中。

      JWT的负载(Payload)声明

      Issuer ("iss")

      返回签发者的名称值,如果没有在负载中定义则返回null

      String issuer = jwt.getIssuer();
      Subject ("sub")

      返回jwt所面向的用户的值,如果没有在负载中定义则返回null

      String subject = jwt.getSubject();
      Audience ("aud")

      返回该jwt由谁接收,如果没有在负载中定义则返回null

      List<String> audience = jwt.getAudience();
      Expiration Time ("exp")

      返回该jwt的过期时间,如果在负载中没有定义则返回null

      Date expiresAt = jwt.getExpiresAt();
      Not Before ("nbf")

      Returns the Not Before value or null if it’s not defined in the Payload.

      Date notBefore = jwt.getNotBefore();
      Issued At ("iat")

      返回在什么时候签发的,如果在负载中没有定义则返回null

      Date issuedAt = jwt.getIssuedAt();
      JWT ID ("jti")

      返回该jwt的唯一标志,如果在负载中没有定义则返回null

      String id = jwt.getId();

      自定义声明

      在令牌有效负载中定义的附加声明可以通过调用getClaims()或 getClaim()和传递声明名来获得。即使无法找到声明,也将会有返回值。您可以通过调用claim.isNull()来检查声明的值是否为null。

      Map<String, Claim> claims = jwt.getClaims();    //Key is the Claim name
      Claim claim = claims.get("isAdmin");

      或者:

      Claim claim = jwt.getClaim("isAdmin");

      当使用jwt.create()创建一个令牌时,您可以通过调用withClaim()来指定自定义声明,并同时传递名称和值。

      String token = JWT.create()
          .withClaim("name", 123)
          .withArrayClaim("array", new Integer[]{1, 2, 3})
          .sign(algorithm);

      您还可以通过调用withClaim()来验证jwt.require()的自定义声明,并传递该名称和所需的值。

      JWTVerifier verifier = JWT.require(algorithm)
          .withClaim("name", 123)
          .withArrayClaim("array", 1, 2, 3)
          .build();
      DecodedJWT jwt = verifier.verify("my.jwt.token");    

      实例

      package course.utils;
      
      import com.auth0.jwt.JWT;
      import com.auth0.jwt.JWTVerifier;
      import com.auth0.jwt.algorithms.Algorithm;
      import com.auth0.jwt.exceptions.JWTVerificationException;
      import com.auth0.jwt.interfaces.DecodedJWT;
      import com.google.common.collect.Maps;
      import course.pojo.User;
      
      import java.io.UnsupportedEncodingException;
      import java.util.Date;
      import java.util.Map;
      
      public class JwtUtils {
      
          //创建token
          public static String creatToken(User user) throws IllegalArgumentException, UnsupportedEncodingException{
              Algorithm algorithm = Algorithm.HMAC256("secret");
              String username = user.getUsername();
              Map<String, Object> map = Maps.newHashMap();
              map.put("alg", "HS256");
              map.put("typ", "JWT");
              String token = JWT.create().withHeader(map)
                      .withClaim("username", username)
                      .withExpiresAt(new Date(System.currentTimeMillis()+360000))
                      .sign(algorithm);
              return token;
          }
      
          //验证jwt
          public static DecodedJWT verifyJwt(String token){
              DecodedJWT decodedJWT = null;
              try{
                  Algorithm algorithm = Algorithm.HMAC256("secret");
                  JWTVerifier jwtVerifier = JWT.require(algorithm).build();
                  decodedJWT = jwtVerifier.verify(token);
              }catch(IllegalArgumentException e){
                  e.printStackTrace();
              }catch (UnsupportedEncodingException e){
                  e.printStackTrace();
              }catch(JWTVerificationException e) {
                  e.printStackTrace();
              }
              return decodedJWT;
          }
      
          public static void main(String[] args) throws UnsupportedEncodingException{
      //        String username = "root";
      //        Integer id =1;
      //        System.out.println(creatToken(username,id));
      //        String token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE1MDgxMzgxNDAsInVzZXJJZCI6MSwidXNlcm5hbphpWUiOiJyb290In0.OeRdHJZKmxFBqIN-A-uSNQK8JyKdzX-wcFR883oMqFA";
      //        System.out.println(verifyJwt("eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE1MDgxMzgxNDAsInVzZXJJZCI6MSwidXNlcm5hbWUiOiJyb290In0.OeRdHJZKmxFBqIN-A-uSNQK8JyKdzX-wcFR883oMqFA"));
      //        System.out.println(verifyJwt(token).getClaims().get("username").asString());
          }
      }

      总结

      以上为编程客栈个人经验,希望能给大家一个参考,也希望大家多多支持编程客栈(www.devze.com)。

      0

      精彩评论

      暂无评论...
      验证码 换一张
      取 消

      关注公众号