JJWT原理及使用

地址

JWT官网:https://jwt.io/

JJWT github:https://github.com/jwtk/jjwt#features-unsupported

什么是jwt

JSON Web令牌是一种开放的、行业标准RFC 7519方法,用于在双方之间安全地表示声明

JWT.IO允许您解码、验证和生成JWT。

Java JWT:

适用于Java和Android的JSON Web令牌

JJWT旨在成为最容易使用和理解的库,用于在JVM和Android上创建和验证JSON Web令牌(JWT)。

JJWT是纯Java实现,完全基于JWTJWSJWEJWKJWA RFC规范以及Apache 2.0许可条款下的开源

该依赖由Okta的高级建筑师Les Hazlewood创建, 由一个贡献者社区支持和维护。

Okta是一个面向开发人员的完整身份验证和用户管理API。

我们还添加了一些不属于规范的便利扩展,例如JWT压缩和声明实施。

使用步骤

引入依赖

<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-api</artifactId>
    <version>0.10.5</version>
</dependency>
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-impl</artifactId>
    <version>0.10.5</version>
    <scope>runtime</scope>
</dependency>
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-jackson</artifactId>
    <version>0.10.5</version>
    <scope>runtime</scope>
</dependency>
<!-- Uncomment this next dependency if you want to use RSASSA-PSS (PS256, PS384, PS512) algorithms:
<dependency>
    <groupId>org.bouncycastle</groupId>
    <artifactId>bcprov-jdk15on</artifactId>
    <version>1.60</version>
    <scope>runtime</scope>
</dependency>
-->

注意事项 ==scope必须是runtime==

快速开始

快速创建一个jws

  • 项目代码中编写:

    Key key = Keys.secretKeyFor(SignatureAlgorithm.HS256); 
    String jws = Jwts.builder().setSubject("Joe").signWith(key).compact();
    
  • 分析代码

    代码中都做了些什么?

    1. 创建了一个秘钥,这个key使用的是HMAC-SHA-256加密算法
    2. 构建一个JWT,将注册的ClaimSub(Subject)设置为Joe
    3. 使用秘钥加密并压缩形成最后的字符串,一个签名的jwt成为jws。This is called a 'JWS' - short for signed JWT.
  • 最终的结果

    eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJKb2UifQ.1KP0SsvENi7Uz1oQc07aXTL7kpQG5jBNIybqr60AlD4
    
  • 验证jws

    try {
    
        Jwts.parser().setSigningKey(key).parseClaimsJws(compactJws);
    
        //OK, we can trust this JWT
    
    } catch (JwtException e) {
      
        //don't trust the JWT!
    }
    

    注意事项parseClaimsJws有很多相似的方法,别用错了,如果用错了会抛出UnsuptedJwtException异常

签名 jwts

如何签名一个jwt

  1. 假如我们有一个jwt如下

    header

    {
      "alg": "HS256"
    }
    

    body

    {
      "sub": "Joe"
    }
    
  2. 去除多余的空白字符

    String header = '{"alg":"HS256"}'
    String claims = '{"sub":"Joe"}'
    
  3. 获取字符串的UTF-8字节数组,并使用BASE64转码

    String encodedHeader = base64URLEncode( header.getBytes("UTF-8") )
    String encodedClaims = base64URLEncode( claims.getBytes("UTF-8") )
    
  4. 使用句号.连接两个字符串

    String concatenated = encodedHeader + '.' + encodedClaims
    
  5. 使用足够强壮的秘钥,结合加密算法,对连接的字符串进行加密

    Key key = getMySecretKey()
    byte[] signature = hmacSha256( concatenated, key )
    
  6. 对签名数组进行BASE64转码并使用句号.连接之前的字符串

    String jws = concatenated + '.' + base64URLEncode( signature )
    

    最终的字符串如下:

    eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJKb2UifQ.1KP0SsvENi7Uz1oQc07aXTL7kpQG5jBNIybqr60AlD4
    

    这些生成步骤不需要我们自己写,jjwt已经帮我封装好了这些方法,包括生成jws(signed JWT)和验证jws

加密算法

算法列表

JWT规范识别12种标准的签名算法,包括3种秘钥算法9种非对称加密算法

HS256: HMAC using SHA-256
HS384: HMAC using SHA-384
HS512: HMAC using SHA-512
    
ES256: ECDSA using P-256 and SHA-256
ES384: ECDSA using P-384 and SHA-384
ES512: ECDSA using P-521 and SHA-512
    
RS256: RSASSA-PKCS-v1_5 using SHA-256
RS384: RSASSA-PKCS-v1_5 using SHA-384
RS512: RSASSA-PKCS-v1_5 using SHA-512
PS256: RSASSA-PSS using SHA-256 and MGF1 with SHA-256
PS384: RSASSA-PSS using SHA-384 and MGF1 with SHA-384
PS512: RSASSA-PSS using SHA-512 and MGF1 with SHA-512

他们所在的位置:io.jsonwebtoken.SignatureAlgorithm的枚举中

为了安全起见,JJWT强制你必须使用足够强壮的秘钥进行加密,否则JJWT就会拒绝并抛出异常

算法要求

HMAC-SHA

该算法包括 HS256, HS384 HS512

HS256 表示 HMAC-SHA-256, 它生成256位(32字节)长的摘要,所以HS256需要使用至少32字节长的密钥。
HS384 表示 HMAC-SHA-384, 它生成384位(48字节)长的摘要,所以HS384需要使用至少48字节长的密钥。
HS512 表示 HMAC-SHA-512, 它生成512位(64字节)长的摘要,所以HS512需要使用至少64字节长的密钥。
RSA

该算法包括RS256, RS384, RS512, PS256, PS384 and PS512

JWT RSA签名算法RS 256、RS 384、RS 512、PS 256、PS 384和PS 512每RFC 7512段3.3和3.5都需要2048位的最小密钥长度(也就是RSA模数位长)。任何小于此值的内容(如1024位)都会被InvalidKeyException拒绝。

尽管如此,为了与最佳实践和安全寿命,JJWT建议我们使用

at least 2048 bit keys with RS256 and PS256 
at least 3072 bit keys with RS384 and PS384
at least 4096 bit keys with RS512 and PS512

这些只是JJWT的建议,而不是强制要求。JJWT只强制JWT规范要求,对于任何RSA密钥,要求是以位为单位的RSA密钥(模数)长度必须>=2048位。

Elliptic Curve

该算法包括 ES256, ES384, and ES512

ES 256要求您使用至少256位(32字节)长的私钥。
ES 384要求您使用至少384位(48字节)长的私钥。 
ES 512要求您使用至少512位(64字节)长的私钥。

创建一个安全的秘钥

加密秘钥

JJWT提供了一个类io.jsonwebtoken.security.Keys可以对指定的算法生成一个足够安全的秘钥

eg:

SecretKey key = Keys.secretKeyFor(SignatureAlgorithm.HS256); //or HS384 or HS512

如果我们有已存在的字节数组,可以使用下列方法生成秘钥

byte[] keyBytes = getSigningKeyFromApplicationConfiguration();//实现该方法,从配置中获取秘钥
SecretKey key = Keys.hmacShaKeyFor(keyBytes);
非对称秘钥

use the Keys.keyPairFor(SignatureAlgorithm) helper

KeyPair keyPair = Keys.keyPairFor(SignatureAlgorithm.RS256); //or RS384, RS512, PS256, PS384, PS512, ES256, ES384, ES512

使用私钥keyPair.getPrivate()创建JWS,使用公钥keyPair.getPublic()解析/验证JWS

==注意事项==

NOTE: The PS256, PS384, and PS512 algorithms require JDK 11 or a compatible JCA Provider (like BouncyCastle) in the runtime classpath. If you are using JDK 10 or earlier and you want to use them, see the Installation section to see how to enable BouncyCastle. All other algorithms are natively supported by the JDK.

创建JWS

You create a JWS as follows:

  1. Use the Jwts.builder() method to create a JwtBuilder instance.
  2. Call JwtBuilder methods to add header parameters and claims as desired.
  3. Specify the SecretKey or asymmetric PrivateKey you want to use to sign the JWT.
  4. Finally, call the compact() method to compact and sign, producing the final jws.

For example:

String jws = Jwts.builder() // (1)

    .setSubject("Bob")      // (2) 

    .signWith(key)          // (3)
     
    .compact();             // (4)

Header参数

JWT报头提供与JWT声明相关的内容、格式和加密操作的元数据。

如果您需要设置一个或多个JWT头参数,例如 kid (Key ID) header parameter, 则可以根据需要调用JwtBuilder setHeaderParameter一次或多次:

String jws = Jwts.builder()

    .setHeaderParameter("kid", "myKeyId")
    
    // ... etc ...

==注意事项== 设置的头信息有可能会被覆盖,不需要设置alg或zip头参数,因为jjWT将根据使用的签名算法或压缩算法自动设置它们。

还有一种方法时我们可以一次性设置头信息

Header header = Jwts.header();

populate(header); //书写该方法

String jws = Jwts.builder()

    .setHeader(header)
    
    // ... etc ...

==注意==:调用setHeader将用可能已经设置的相同名称覆盖任何现有的标题名称/值对。然而,在所有情况下,JJWT仍然会设置(并覆盖)任何alg和zip头,不管这些标头是否位于指定的标头对象中。

声明(claims)

声明是JWT的“主体”,包含JWT创建者希望提供给JWT收件人的信息。

  1. Standard Claims

    The JwtBuilder provides convenient setter methods for standard registered Claim names defined in the JWT specification. They are:
    
    setIssuer: sets the iss (Issuer) Claim
    setSubject: sets the sub (Subject) Claim
    setAudience: sets the aud (Audience) Claim
    setExpiration: sets the exp (Expiration Time) Claim
    setNotBefore: sets the nbf (Not Before) Claim
    setIssuedAt: sets the iat (Issued At) Claim
    setId: sets the jti (JWT ID) Claim
    

    For example:

    String jws = Jwts.builder()
    
        .setIssuer("me")
        .setSubject("Bob")
        .setAudience("you")
        .setExpiration(expiration) //a java.util.Date
        .setNotBefore(notBefore) //a java.util.Date 
        .setIssuedAt(new Date()) // for example, now
        .setId(UUID.randomUUID()) //just an example id
        
        /// ... etc ...
    
  2. Custom Claims

    If you need to set one or more custom claims that don't match the standard setter method claims shown above, you can simply call JwtBuilder claim one or more times as needed:

    String jws = Jwts.builder()
    
        .claim("hello", "world")
        
        // ... etc ...
    

    Each time claim is called, it simply appends the key-value pair to an internal Claims instance, potentially overwriting any existing identically-named key/value pair.

    Obviously, you do not need to call claim for any standard claim name and it is recommended instead to call the standard respective setter method as this enhances readability.

  3. Claims Instance

    If you want to specify all claims at once, you can use the Jwts.claims() method and build up the claims with it:

    Claims claims = Jwts.claims();
    
    populate(claims); //implement me
    
    String jws = Jwts.builder()
    
        .setClaims(claims)
        
        // ... etc ...
    

    NOTE: Calling setClaims will overwrite any existing claim name/value pairs with the same names that might have already been set.

  4. Claims Map

    If you want to specify all claims at once and you don't want to use Jwts.claims(), you can use JwtBuilder setClaims(Map)method instead:

    Map<String,Object> claims = getMyClaimsMap(); //implement me
    
    String jws = Jwts.builder()
    
        .setClaims(claims)
        
        // ... etc ...
    

    NOTE: Calling setClaims will overwrite any existing claim name/value pairs with the same names that might have already been set.

签名秘钥(Signing Key)

  • 官网建议我们使用JwtBuildersignWith方法来设置签名秘钥,然后让JJWT决定使用最安全的加密算法。

    String jws = Jwts.builder()
    
       // ... etc ...
       
       .signWith(key) // <---不指定加密算法
       
       .compact();
    

    例如,如果您使用一个长256位(32字节)的秘钥调用signWith,那么它对HS384或HS512不够强,因此JJWT将自动使用HS256对JWT进行签名。

    在使用signWith时,JJWT还将使用相关的算法标识符自动设置所需的alg标头。

    类似地,如果使用4096位长的RSA私钥调用signWith,JJWT将使用RS 512算法并自动将alg报头设置为RS 512。

    同样的选择逻辑也适用于椭圆曲线私钥。

    ==注意事项== 不能使用公钥对JWTs进行签名,因为不安全. JJWT拒绝任何公钥加密并会抛出异常InvalidKeyException.

  • 加密算法覆盖(SignatureAlgorithm Override)

    如果需要指定JJWT的加密算法时使用如下方法

     .signWith(privateKey, SignatureAlgorithm.RS512) // <---
     .compact();//压缩
    

    这是允许的,因为JWT规范允许任何RSA密钥的RSA算法强度>=2048位。JJWT更倾向于秘钥>=4096位的RS 512,其次是秘钥>=3072位的RS 384,最后是秘钥>=2048位的RS 256。

  • JWS压缩(JWS Compression)

    如果您的JWT声明集很大(包含大量数据),并且JJWT 读取/解析都是使用JWS的同一个库,那么您可能需要压缩JWS以缩小其大小。注意,这不是JWS的标准特性,其他JWT库不太可能支持它

读取JWS(Reading a JWS)

You read (parse) a JWS as follows:

  1. 使用 Jwts.parser() 方法创建 JwtParser 实例.

  2. 指定需要验证JWS签名的 SecretKey or asymmetric PublicKey

  3. 最终调用 parseClaimsJws(String) 方法,入参是签名的 String, 最终得到原始的 JWS.

  4. 整个调用将被包装在try/catch块中。我们稍后将讨论异常和失败的原因。

  5. If you don't which key to use at the time of parsing, you can look up the key using a SigningKeyResolver which we'll cover later.

For example:

Jws<Claims> jws;

try {
    jws = Jwts.parser()         // (1)
    .setSigningKey(key)         // (2)
    .parseClaimsJws(jwsString); // (3)
    
    // we can safely trust the JWT
     
catch (JwtException ex) {       // (4)
    
    // we *cannot* use the JWT as intended by its creator
}

NOTE: JwtParser's parseClaimsJws 有很多相似的方法。注意区分

验证秘钥(Verification Key)

在读取JWS时,最重要的是指定用于验证JWS密码签名的密钥。如果签名验证失败,则不能安全地信任JWT,因此应该丢弃JWT。

  • If the jws was signed with a SecretKey, the same SecretKey should be specified on the JwtParser. For example:

    Jwts.parser()
        
      .setSigningKey(secretKey) // <----
      
      .parseClaimsJws(jwsString);
    
  • If the jws was signed with a PrivateKey, that key's corresponding PublicKey (not the PrivateKey) should be specified on the JwtParser. For example:

    Jwts.parser()
        
      .setSigningKey(publicKey) // <---- publicKey, not privateKey
      
      .parseClaimsJws(jwsString);
    

还有一点我们要注意如果签名是使用的不是单一秘钥或 公/私秘钥,或者是两者的结合, 我们不能使用JwtParser's setSigningKey方法而要使用SigningKeyResolver来代替

Signing Key Resolver

如果您的应用程序期望使用不同的密钥签名的JWS,则不能使用setSigningKey方法。而应该实现SigningKeyResolver接口,并通过setSigningKeyResolver方法在JwtParser上指定一个实例。

For example:

SigningKeyResolver signingKeyResolver = getMySigningKeyResolver();

Jwts.parser()

    .setSigningKeyResolver(signingKeyResolver) // <----
    
    .parseClaimsJws(jwsString);

您可以通过继承SigningKeyResolverAdapter实现theresolveSigningKey(JwsHeader,Claims)方法来稍微简化一些事情。例如:

public class MySigningKeyResolver extends SigningKeyResolverAdapter {
    
    @Override
    public Key resolveSigningKey(JwsHeader jwsHeader, Claims claims) {
        // implement me
    }
}

JwtParser 将在解析JWSJSON之后,但在验证JWS签名之前,调用resolveSigningKey 方法。这允许您检查JwsHeaderClaims 中的任何信息,这些信息可以帮助您查找用于验证特定JWS的密钥。这对于具有更复杂的安全模型的应用程序非常强大,因为它可以在不同的时间或用户或客户时使用不同的秘钥。

我们需要检查哪些数据呢?

在JWS创建时允许创建一个ID标识

for example:

Key signingKey = getSigningKey();

String keyId = getKeyId(signingKey); //any mechanism you have to associate a key with an ID is fine

String jws = Jwts.builder()
    
    .setHeaderParam(JwsHeader.KEY_ID, keyId) // 1
    
    .signWith(signingKey)                    // 2
    
    .compact();

在解析过程中你的SigningKeyResolver 可以检查JwsHeader并获取kid,然后利用这个id查找一些信息例如数据库等等。。

For example:

public class MySigningKeyResolver extends SigningKeyResolverAdapter {
    
    @Override
    public Key resolveSigningKey(JwsHeader jwsHeader, Claims claims) {
        
        //inspect the header or claims, lookup and return the signing key
        
        String keyId = jwsHeader.getKeyId(); //or any other field that you need to inspect
        
        Key key = lookupVerificationKey(keyId); //implement me
        
        return key;
    }
}

注意,检查jwsHeader.getKeyId()只是查找密钥的最常见方法-您可以检查任意数量的头字段或声明,以确定如何查找验证密钥。这一切都是基于JWS是如何创建的。

声明断言(Claim Assertions)

我们可以强制要求我们解析的JWS符合我们所期望的格式,比如我们希望JWS中包含 特定的subject的值,如果没有则该jws就扔掉,我们可以通过JwtParser上的require*方法

try {
    Jwts.parser().requireSubject("jsmith").setSigningKey(key).parseClaimsJws(s);
} catch(InvalidClaimException ice) {
    // the sub field was missing or did not have a 'jsmith' value
}

如果该值是否丢失对我们很重要,那么我们可以不捕获InvalidClaimException,我们可以捕获 MissingClaimException or IncorrectClaimException

try {
    Jwts.parser().requireSubject("jsmith").setSigningKey(key).parseClaimsJws(s);
} catch(MissingClaimException mce) {
    // the parsed JWT did not have the sub field
} catch(IncorrectClaimException ice) {
    // the parsed JWT had a sub field, but its value was not equal to 'jsmith'
}

我们也可以使用require(fieldName, requiredFieldValue)来要求JWS中包含某个字段并且该字段指定的值是我们指定的值

try {
    Jwts.parser().require("myfield", "myRequiredValue").setSigningKey(key).parseClaimsJws(s);
} catch(InvalidClaimException ice) {
    // the 'myfield' field was missing or did not have a 'myRequiredValue' value
    //注意是固定值,值必须是 myRequiredValue
}   

计算时钟偏差(Accounting for Clock Skew)

在解析JWT时,您可能会发现exp或NBF断言失败(抛出异常),因为解析机器上的时钟与创建JWT的机器上的时钟不完全同步。这可能会出现一些明显的问题,因为exp和nbf是基于时间的断言,对于共享断言,时钟时间需要可靠地同步。

您可以在使用JwtParser'ssetAllowedClockSkewSeconds进行解析时说明这些差异(通常不超过几分钟)。

例如

long seconds = 3 * 60; //3 minutes

Jwts.parser()
    
    .setAllowedClockSkewSeconds(seconds) // <----
    
    // ... etc ...
    .parseClaimsJws(jwt);

这样可以确保两个不同的机器上的始终问题可以忽略,一般2到3分钟就足够了,如果一台生产环境的机器的时钟与世界上大多数原子钟相差5分钟以上,那将是相当少见的

Custom Clock Support

如果上面的setalloningClockSkewSecond不足以满足您的需要,那么在解析时间戳比较时创建的时间戳可以通过自定义的时间源获得。使用io.jsonwebToken.Clock接口的实现调用JwtParsersetClock方法。

例如:

Clock clock = new MyClock();

Jwts.parser().setClock(myClock) //... etc ...

JwtParser的默认时钟实现只是返回新的Date(),以反映解析发生的时间,正如大多数人所期望的那样。但是,提供自己的时钟可能很有用,特别是在编写测试用例以保证确定性行为时。

JWS Decompression

如果使用JJWT压缩JWS并使用自定义压缩算法,则需要告诉JwtParser如何解析CompressionCodec来解压缩JWT。参看压缩章节

Compression

JWT规范只是针对JWEs(Encrypted JWTs)进行标准化,而不支持JWSs (Signed JWTs),而JJWT两个都支持,如果你确定使用JJWT进行创建JWS并且使用JJWT解析它,那么你可以使用这个功能,否则它应该只用在JWEs上

如果JWT的Claims集合很大大-也就是说,它包含许多名称/值对,或者单个值非常大或冗长-您可以通过压缩Claims的体积来缩小创建的JWS的大小。

如果在URL中使用结果JWS,这对您可能很重要,因为由于浏览器、用户邮件代理或HTTP网关兼容性问题,URL最好保持在4096个字符以下。较小的JWT还有助于降低带宽利用率,这可能是重要的,也可能不重要,这取决于您的应用程序的数量或需求。

eg:

   Jwts.builder()
   
   .compressWith(CompressionCodecs.DEFLATE) // or CompressionCodecs.GZIP
   
   // .. etc ...

如果你使用的是DEFLATE or GZIP压缩编解码器-仅此而已,你就完事了。在解析或配置JwtParser进行压缩时你不需要做任何事情-JJWT将按照预期自动解压主体。

自定义压缩编解码器 (Custom Compression Codec)

如果我们在创建JWT(通过JwtBuilder compressWith)时使用的是自定义的压缩编码解码器,那我们需要通过 setCompressionCodecResolver提供一个编码解码器给 JwtParser

eg:

CompressionCodecResolver ccr = new MyCompressionCodecResolver();

Jwts.parser()

    .setCompressionCodecResolver(ccr) // <----
    
    // .. etc ...

通常,CompressionCodecResolver实现将检查zip头,以确定使用了什么算法,然后返回支持该算法的codec实例。

例如:

public class MyCompressionCodecResolver implements CompressionCodecResolver {
        
    @Override
    public CompressionCodec resolveCompressionCodec(Header header) throws CompressionException {
        
        String alg = header.getCompressionAlgorithm();
            
        CompressionCodec codec = getCompressionCodec(alg); //implement me
            
        return codec;
    }
}

JSON处理器

JwtBuilder 将使用Serializer<Map<String, ?>>实例将HeaderClaims 映射(以及可能包含的任何Java对象)序列化为JSON。类似地,JwtParser 将使用Deserializer<Map<String, ?>>实例将JSON反序列化为HeaderClaims

如果没有显式配置JwtBuilder的序列化程序或JwtParser的反序列化器,如果在运行时类路径中找到以下JSON实现,JJWT将自动尝试发现和使用以下JSON实现。它们是按顺序检查的,并使用了第一个发现的方法:

  1. Jackson:如果您将io.jsonwebToken:jjwt-Jackson指定为项目运行时依赖项,这将自动使用。Jackson支持POJO作为声明,并在必要时进行完全封送/解封处理。

  2. json-java(org.json):如果您将io.jsonwebToken:jjwt-orgjson指定为项目运行时依赖项,这将自动使用。

注意:org.jsonAPI是在Android环境中原生启用的,因此这是推荐的用于Android应用程序的JSON处理器,除非您希望使用POJO作为声明。org.json库支持简单的对象到JSON封送处理,但它不支持JSON到对象的解组。

如果希望使用POJO作为claims值,请使用io.jsonwebToken:jjwt-Jackson依赖项(如果需要,可以实现自己的序列化程序和反序列化程序)。但是请注意,Jackson将强制Android应用程序依赖大量(>1MB)的应用程序,从而增加了移动用户的应用程序下载大小。

自定义JSON处理器

如果您不想使用JJWT的运行时依赖方法,或者只想自定义JSON序列化和反序列化的工作方式,那么可以在JwtBuilder和JwtParser上分别实现序列化器和反序列化器接口并指定它们的实例。

例如:

When creating a JWT:

Serializer<Map<String,?>> serializer = getMySerializer(); //implement me

Jwts.builder()

    .serializeToJsonWith(serializer)
    
    // ... etc ...

When reading a JWT:

Deserializer<Map<String,?>> deserializer = getMyDeserializer(); //implement me

Jwts.parser()

    .deserializeJsonWith(deserializer)
    
    // ... etc ...

Jackson JSON 处理器

如果您有一个应用程序范围的Jackson ObjectMapper(通常是大多数应用程序推荐的),则可以通过使用您的ObjectMapper来消除JJWT构建自己的ObjectMapper的开销。

为此,您可以使用编译作用域声明io.jsonwebToken:jjwt-Jackson依赖项(而不是运行时范围,这是典型的JJWT默认值)。即:

Maven

<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-jackson</artifactId>
    <version>0.10.5</version>
    <scope>compile</scope> <!-- Not runtime -->
</dependency>

然后可以在JwtBuilder上使用自己的ObjectMapper指定JacksonSeriizer

ObjectMapper objectMapper = getMyObjectMapper(); //implement me

String jws = Jwts.builder()

    .serializeToJsonWith(new JacksonSerializer(objectMapper))
    
    // ... etc ...

以及在JwtParser上使用ObjectMapperJacksonDeseriizer

ObjectMapper objectMapper = getMyObjectMapper(); //implement me

Jwts.parser()

    .deserializeJsonWith(new JacksonDeserializer(objectMapper))
    
    // ... etc ...

Base64支持

JJWT使用了非常快速的纯Java Base 64编解码器,用于Base 64和Base64Url编码和解码,保证在所有JDK和Android环境中都能确定地工作。

您可以使用io.jsonwebToken.io.Encodersio.jsonwebToken.io.Decodersuency类访问JJWT的编码器和解码器。

io.jsonwebtoken.io.Encoders:

  • BASE64 is an RFC 4648 Base64 encoder
  • BASE64URL is an RFC 4648 Base64URL encoder

io.jsonwebtoken.io.Decoders:

  • BASE64 is an RFC 4648 Base64 decoder
  • BASE64URL is an RFC 4648 Base64URL decoder

定制Base64

如果出于某种原因要指定自己的Base64Url编码器和解码器,可以使用JwtBuilderBase 64UrlEncodeWith方法设置编码器:

Encoder<byte[], String> base64UrlEncoder = getMyBase64UrlEncoder(); //implement me

String jws = Jwts.builder()

    .base64UrlEncodeWith(base64UrlEncoder)
    
    // ... etc ...

and the JwtParser's base64UrlDecodeWith method to set the decoder:

Decoder<String, byte[]> base64UrlDecoder = getMyBase64UrlDecoder(); //implement me

Jwts.parser()

    .base64UrlDecodeWith(base64UrlEncoder)
    
    // ... etc ...

优劣势

优:减少服务器压力

劣:一旦签名的jws无法销毁

Learn More

Author

Maintained by Les Hazlewood & Okta

License

This project is open-source via the Apache 2.0 License.

以下总结摘自https://blog.csdn.net/qq_28165595/article/details/80214994 的博客

JWT应用场景

一次性验证

比如用户注册后需要发一封邮件让其激活账户,通常邮件中需要有一个链接,这个链接需要具备以下的特性:能够标识用户,该链接具有时效性(通常只允许几小时之内激活),不能被篡改以激活其他可能的账户…这种场景就和 jwt 的特性非常贴近,jwt 的 payload 中固定的参数:iss 签发者和 exp 过期时间正是为其做准备的。

restful api 的无状态认证

使用 jwt 来做 restful api 的身份认证也是值得推崇的一种使用方案。客户端和服务端共享 secret;过期时间由服务端校验,客户端定时刷新;签名信息不可被修改…spring security oauth jwt 提供了一套完整的 jwt 认证体系,以笔者的经验来看:使用 oauth2 或 jwt 来做 restful api 的认证都没有大问题,oauth2 功能更多,支持的场景更丰富,后者实现简单。

使用 jwt 做单点登录+会话管理(不推荐)

在《八幅漫画理解使用JSON Web Token设计单点登录系统》一文中提及了使用 jwt 来完成单点登录,本文接下来的内容主要就是围绕这一点来进行讨论。如果你正在考虑使用 jwt+cookie 代替 session+cookie ,我强力不推荐你这么做。
首先明确一点:使用 jwt 来设计单点登录系统是一个不太严谨的说法。首先 cookie+jwt 的方案前提是非跨域的单点登录(cookie 无法被自动携带至其他域名),其次单点登录系统包含了很多技术细节,至少包含了身份认证和会话管理,这还不涉及到权限管理。如果觉得比较抽象,不妨用传统的 session+cookie 单点登录方案来做类比,通常我们可以选择 spring security(身份认证和权限管理的安全框架)和 spring session(session 共享)来构建,而选择用 jwt 设计单点登录系统需要解决很多传统方案中同样存在和本不存在的问题。以下一一详细罗列。
jwt token泄露了怎么办?
前面的文章下有不少人留言提到这个问题,我则认为这不是问题。传统的 session+cookie 方案,如果泄露了 sessionId,别人同样可以盗用你的身份。[扬汤止沸]不如[釜底抽薪],不妨来[追根溯源]一下,什么场景会导致你的 jwt 泄露。
遵循如下的实践可以尽可能保护你的 jwt 不被泄露:使用 https 加密你的应用,返回 jwt 给客户端时设置 httpOnly=true 并且使用 cookie 而不是 LocalStorage 存储 jwt,这样可以防止 XSS 攻击和 CSRF 攻击(对这两种攻击感兴趣的童鞋可以看下 spring security 中对他们的介绍CSRF,XSS)
secret如何设计
jwt 唯一存储在服务端的只有一个 secret,个人认为这个 secret 应该设计成和用户相关的属性,而不是一个所有用户公用的统一值。这样可以有效的避免一些注销和修改密码时遇到的窘境。
注销和修改密码
传统的 session+cookie 方案用户点击注销,服务端清空 session 即可,因为状态保存在服务端。但 jwt 的方案就比较难办了,因为 jwt 是无状态的,服务端通过计算来校验有效性。没有存储起来,所以即使客户端删除了 jwt,但是该 jwt 还是在有效期内,只不过处于一个游离状态。分析下痛点:注销变得复杂的原因在于 jwt 的无状态。我提供几个方案,视具体的业务来决定能不能接受。
- 仅仅清空客户端的 cookie,这样用户访问时就不会携带 jwt,服务端就认为用户需要重新登录。这是一个典型的假注销,对于用户表现出退出的行为,实际上这个时候携带对应的 jwt 依旧可以访问系统。
- 清空或修改服务端的用户对应的 secret,这样在用户注销后,jwt 本身不变,但是由于 secret 不存在或改变,则无法完成校验。这也是为什么将 secret 设计成和用户相关的原因。
- 借助第三方存储自己管理 jwt 的状态,可以以 jwt 为 key,实现去 redis 一类的缓存中间件中去校验存在性。方案设计并不难,但是引入 redis 之后,就把无状态的 jwt 硬生生变成了有状态了,违背了 jwt 的初衷。实际上这个方案和 session 都差不多了。
修改密码则略微有些不同,假设号被到了,修改密码(是用户密码,不是 jwt 的 secret)之后,盗号者在原 jwt 有效期之内依旧可以继续访问系统,所以仅仅清空 cookie 自然是不够的,这时,需要强制性的修改 secret。在我的实践中就是这样做的。
续签问题
续签问题可以说是我抵制使用 jwt 来代替传统 session 的最大原因,因为 jwt 的设计中我就没有发现它将续签认为是自身的一个特性。传统的 cookie 续签方案一般都是框架自带的,session 有效期 30 分钟,30 分钟内如果有访问,session 有效期被刷新至 30 分钟。而 jwt 本身的 payload 之中也有一个 exp 过期时间参数,来代表一个 jwt 的时效性,而 jwt 想延期这个 exp 就有点[身不由己]了,因为 payload 是参与签名的,一旦过期时间被修改,整个 jwt 串就变了,jwt 的特性天然不支持续签!
如果你一定要使用 jwt 做会话管理(payload 中存储会话信息),也不是没有解决方案,但个人认为都不是很[令人满意]
1.每次请求刷新 jwt
jwt 修改 payload 中的 exp 后整个 jwt 串就会发生改变,那…就让它变好了,每次请求都返回一个新的 jwt 给客户端。太暴力了,不用我赘述这样做是多么的不优雅,以及带来的性能问题。但,至少这是最简单的解决方案。
2.只要快要过期的时候刷新 jwt
一个上述方案的改造点是,只在最后的几分钟返回给客户端一个新的 jwt。这样做,触发刷新 jwt 基本就要看运气了,如果用户恰巧在最后几分钟访问了[服务器],触发了刷新,[万事大吉];如果用户连续操作了 27 分钟,只有最后的 3 分钟没有操作,导致未刷新 jwt,无疑会令用户抓狂。
3.完善 refreshToken
借鉴 oauth2 的设计,返回给客户端一个 refreshToken,允许客户端主动刷新 jwt。一般而言,jwt 的过期时间可以设置为数小时,而 refreshToken 的过期时间设置为数天。我认为该方案并可行性是存在的,但是为了解决 jwt 的续签把整个流程改变了,为什么不考虑下 oauth2 的 password 模式和 client 模式呢?
4.使用 redis 记录独立的过期时间
实际上我的项目中由于历史遗留问题,就是使用 jwt 来做登录和会话管理的,为了解决续签问题,我们在 redis 中单独会每个 jwt 设置了过期时间,每次访问时刷新 jwt 的过期时间,若 jwt 不存在与 redis 中则认为过期。
同样改变了 jwt 的流程,不过嘛,世间安得两全法。我只能奉劝各位还未使用 jwt 做会话管理的朋友,尽量还是选用传统的 session+cookie 方案,有很多成熟的分布式 session 框架和安全框架供你开箱即用。

总结

在 web 应用中,使用 jwt 代替 session 存在不小的风险,你至少得解决本文中提及的那些问题,绝大多数情况下,传统的 cookie-session 机制工作得更好。jwt 适合做简单的 restful api 认证,颁发一个固定有效期的 jwt,降低 jwt 暴露的风险,不要对 jwt 做服务端的状态管理,这样才能体现出 jwt 无状态的优势。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 151,829评论 1 331
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 64,603评论 1 273
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 101,846评论 0 226
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 42,600评论 0 191
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 50,780评论 3 272
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 39,695评论 1 192
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,136评论 2 293
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 29,862评论 0 182
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 33,453评论 0 229
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 29,942评论 2 233
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,347评论 1 242
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 27,790评论 2 236
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,293评论 3 221
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 25,839评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,448评论 0 181
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 34,564评论 2 249
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 34,623评论 2 249

推荐阅读更多精彩内容