The premise

This is the fifth article in the “Cold Rice New Stir-fry” series.

This article will take a look at JWT, an open source standard for generating access tokens, and introduce the specifications, underlying implementation principles, basic usage, and application scenarios of JWT.

JWT specification

Unfortunately, there is no wikipedia entry for JWT, but from the image on the front page of jwt. IO, you can see the description:

JSON Web Tokens are an open, industry standard RFC 7519 method for representing claims securely between two parties

The JWT specification file RFC 7519 describes Claims, definitions, layout, and algorithm implementation in detail.

Basic concepts of JWT

JWT stands for JSON Web Token, which literally feels like a jSON-formatted Token for network transport. In fact, JWT is a compact Claims declaration format designed for transport in space-constrained environments, such as HTTP authorization request header parameters and URI query parameters. JWT converts Claims to JSON format, and this JSON content is applied either to a JWS payload or to a JWE (encrypted) raw string. Digitally sign or protect the integrity of Claims using a Message Authentication Code or MAC and/or encryption operation.

There are three concepts mentioned briefly in other specification documents:

  • JWE(Specification documentRFC 7516) :JSON Web Encryption, represents based onJSONEncrypting the contents of data structures, encryption mechanisms to encrypt any 8-bit byte sequence, provide integrity protection and increase the difficulty of cracking,JWEThe compact serialization layout in
BASE64URL(UTF8(JWE Protected Header)) || '.' ||
BASE64URL(JWE Encrypted Key) || '.' ||
BASE64URL(JWE Initialization Vector) || '.' ||
BASE64URL(JWE Ciphertext) || '.' ||
BASE64URL(JWE Authentication Tag)
Copy the code
  • JWS(Specification documentRFC 7515) :JSON Web Signature, indicates usingJSONData structure andBASE64URLEncoding represents a digital signature or message authentication code (MAC) authenticated content, digital signature orMACCan provide integrity protection,JWSThe compact serialization layout in:
ASCII(BASE64URL(UTF8(JWS Protected Header)) || '.' || 
BASE64URL(JWS Payload)) || '.' ||
BASE64URL(JWS Signature)
Copy the code
  • JWA(Specification documentRFC 7518) :JSON Web Algorithm.JSON WebAlgorithms, digital signatures orMACAlgorithm, applied toJWSThe list of available algorithms for

In general, JWT has two implementations. A JWE-based implementation relies on encryption and decryption algorithms, BASE64URL encoding, and identity authentication to make it difficult to decrypt transmitted Claims, while a JWS based implementation uses BASE64URL encoding and digital signature to provide integrity protection for transmitted Claims. That is, only to ensure that the contents of the Claims transmitted are not tampered with, but that the plaintext is exposed. At present, most of the mainstream JWT frameworks do not implement JWE, so the following is mainly through the implementation of JWS for in-depth discussion.

JWT Claims of

Claim has the meaning of Claim, Claim, Claim or Claim, but I feel that none of the translation is semantic, so I reserve the keyword Claim as the name directly. The core role of JWT is to protect the integrity of Claims (or data encryption) and ensure that Claims are not tampered with (or cracked) during JWT transmission. Claims in JWT raw content is a JSON-formatted string, where a single Claim is a K-V structure and serves as a field-value in JsonNode. Here are the predefined Claims in common specifications:

Referred to as” The full name meaning
iss Issuer The issuer
sub Subject The main body
aud Audience (Receiving) Target party
exp Expiration Time Expiration time
nbf Not Before Prior to the time definedJWTCannot be accepted for processing
iat Issued At JWTTime stamp at launch
jti JWT ID JWTUnique identifier of

These predefined claims are not required to be used forcibly, and it is entirely up to the user to decide when and which Claim to choose. In order to make JWT more compact, these claims are defined in a short naming way. On the premise of not conflicting with the built-in Claim, users can customize new public claims, such as:

Referred to as” The full name meaning
cid Customer ID Customer ID
rid Role ID Character ID

It is important to note that in JWS implementations, Claims are BASE64 encoded as part of the payload, and the plaintext is exposed directly. Sensitive information should generally not be designed as a custom Claim.

The Header of JWT

These headers are called JOSE Headers in the JWT specification file, which stands for Javascript Object Signature Encryption, JOSE Header is simply a Header parameter for signing and encrypting Javascript objects. Here are some common headers in JWS:

Referred to as” The full name meaning
alg Algorithm Used to protectJWSEncryption and decryption algorithm
jku JWK Set URL A set ofJSONOf the encoded public keyURL, one of which is used forJWSKey for digital signature
jwk JSON Web Key Used forJWSPublic key corresponding to the digital signature key
kid Key ID Used to protectJWSEnter the key
x5u X.509 URL X.509related
x5c X.509 Certificate Chain X.509related
x5t X.509 Certificate SHA-1 Thumbprin X.509related
x5t#S256 X.509 Certificate SHA-256 Thumbprint X.509related
typ Type Type, for exampleJWT,JWSorJWE, etc.
cty Content Type Content type, decidepayloadPart of theMediaType

The two most common headers are ALG and TYP, for example:

{
  "alg": "HS256"."typ": "JWT"
}
Copy the code

The layout of the JWT

We will focus on the layout of JWS. As mentioned earlier, the compact layout of JWS is as follows:

ASCII(BASE64URL(UTF8(JWS Protected Header)) || '.' || 
BASE64URL(JWS Payload)) || '.' ||
BASE64URL(JWS Signature)
Copy the code

There is also a non-compact layout that fully displays Header arguments, Claims, and group signatures via a JSON structure:

{
    "payload":"<payload contents>"."signatures":[
    {"protected":"<integrity-protected header 1 contents>"."header":<non-integrity-protected header 1 contents>,
    "signature":"<signature 1 contents>"},... {"protected":"<integrity-protected header N contents>"."header":<non-integrity-protected header N contents>,
    "signature":"<signature N contents>"}}]Copy the code

An uncompact layout also has a flat representation:

{
    "payload":"<payload contents>"."protected":"<integrity-protected header contents>"."header":<non-integrity-protected header contents>,
    "signature":"<signature contents>"
}
Copy the code

The payload is a complete claim. Assume that the JSON form for a claim is:

{
   "iss": "throwx"."jid": 1
}
Copy the code

The payload node in the flat, uncompact format is:

{..."payload": {
      "iss": "throwx"."jid": 1}... }Copy the code

JWS signature algorithm

JWS signature generation relies on hash or encryption and decryption algorithms, such as HMAC SHA-256, which hashes the encoded Header and Claims string using the HASH algorithm sha-256. The pseudo-code generated by the signature is as follows:

## No encoding
HMACSHA256(
  base64UrlEncode(header) + "." +
  base64UrlEncode(payload),
  256 bit secret key
)

## encode
base64UrlEncode(
    HMACSHA256(
       base64UrlEncode(header) + "." +
       base64UrlEncode(payload)
       [256 bit secret key])
)
Copy the code

The operation of other algorithms is basically similar, the generated good signature is directly added to a front. The complete JWS is generated by concatenating base64UrlEncode(header). Base64UrlEncode (payload).

Generation, parsing, and validation of JWT

Some basic concepts, layout and signature algorithms of JWT have been analyzed before. Here, the generation, parsing and verification of JWT are carried out based on the previous theory. First, common-Codec library is introduced to simplify some encoding and decryption operations, and a mainstream JSON framework is introduced to serialize and deserialize:

<dependency>
    <groupId>commons-codec</groupId>
    <artifactId>commons-codec</artifactId>
    <version>1.15</version>
</dependency>
<dependency>
    <groupId>com.fasterxml.jackson.core</groupId>
    <artifactId>jackson-databind</artifactId>
    <version>2.11.0</version>
</dependency>
Copy the code

For simplicity, the Header argument is written as:

{
  "alg": "HS256"."typ": "JWT"
}
Copy the code

Using the signature algorithm is HMAC SHA – 256, enter the length of the encryption KEY must be 256 – bit (if simple characters and Numbers in English, to 32 characters), here for the sake of simplicity, in 00000000111111112222222233333333 as a KEY. Define Claims section as follows:

{
  "iss": "throwx"."jid": 10087, # <---- there is a clerical error here, I intended to write jti, but later I found it wrong, I will not change it"exp": 1613227468168     # 20210213    
}
Copy the code

The code to generate JWT is as follows:

@Slf4j
public class JsonWebToken {

    private static final String KEY = "00000000111111112222222233333333";

    private static final String DOT = ".";

    private static final Map<String, String> HEADERS = new HashMap<>(8);

    private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();

    static {
        HEADERS.put("alg"."HS256");
        HEADERS.put("typ"."JWT");
    }

    String generateHeaderPart(a) throws JsonProcessingException {
        byte[] headerBytes = OBJECT_MAPPER.writeValueAsBytes(HEADERS);
        String headerPart = new String(Base64.encodeBase64(headerBytes,false ,true), StandardCharsets.US_ASCII);
        log.info("The generated Header part is :{}", headerPart);
        return headerPart;
    }

    String generatePayloadPart(Map<String, Object> claims) throws JsonProcessingException {
        byte[] payloadBytes = OBJECT_MAPPER.writeValueAsBytes(claims);
        String payloadPart = new String(Base64.encodeBase64(payloadBytes,false ,true), StandardCharsets.UTF_8);
        log.info("Payload generated :{}", payloadPart);
        return payloadPart;
    }

    String generateSignaturePart(String headerPart, String payloadPart) {
        String content = headerPart + DOT + payloadPart;
        Mac mac = HmacUtils.getInitializedMac(HmacAlgorithms.HMAC_SHA_256, KEY.getBytes(StandardCharsets.UTF_8));
        byte[] output = mac.doFinal(content.getBytes(StandardCharsets.UTF_8));
        String signaturePart = new String(Base64.encodeBase64(output, false ,true), StandardCharsets.UTF_8);
        log.info("Generated Signature part :{}", signaturePart);
        return signaturePart;
    }

    public String generate(Map<String, Object> claims) throws Exception {
        String headerPart = generateHeaderPart();
        String payloadPart = generatePayloadPart(claims);
        String signaturePart = generateSignaturePart(headerPart, payloadPart);
        String jws = headerPart + DOT + payloadPart + DOT + signaturePart;
        log.info("The generated JWT is :{}", jws);
        return jws;
    }

    public static void main(String[] args) throws Exception {
        Map<String, Object> claims = new HashMap<>(8);
        claims.put("iss"."throwx");
        claims.put("jid".10087L);
        claims.put("exp".1613227468168L);
        JsonWebToken jsonWebToken = new JsonWebToken();
        System.out.println("Self-generated JWT:"+ jsonWebToken.generate(claims)); }}Copy the code

The output log is as follows:

23:37:48.743[the main] INFO club. Throwable. JWT. JsonWebToken - generate the Header part is: eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ923:37:48.747[main] INFO club.throwable.jwt.JsonWebToken - Generated content part is: eyJpc3MiOiJ0aHJvd3giLCJqaWQiOjEwMDg3LCJleHAiOjE2MTMyMjc0NjgxNjh923:37:48.748[the main] INFO club. Throwable. JWT. JsonWebToken - generate the Signature part is: 7 skdudgxv - BP2p_CXyr3Na7WBvENNl - Pm4HQ8cJuEs23:37:48.749[main] INFO club.throwable.jwt.JsonWebToken - Generated JWT as: eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9. EyJpc3MiOiJ0aHJvd3giLCJqaWQiOjEwMDg3LCJleHAiOjE2MTMyMjc0NjgxNjh9. 7 skdudgxv - BP2p_CXyr3Na7WBvENNl--Pm4HQ8cJuEs Generated by JWT: eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9. EyJpc3MiOiJ0aHJvd3giLCJqaWQiOjEwMDg3LCJleHAiOjE2MTMyMjc0NjgxNjh9. 7 skdudgxv -BP2p_CXyr3Na7WBvENNl--Pm4HQ8cJuEsCopy the code

This can be verified on jwt. IO:

The process of parsing a JWT is the reverse process of constructing a JWT, first based on dot numbers. It is divided into three sections, and then BASE64 decoding is carried out respectively, and the plaintext of three parts is obtained. JSON deserialization of header parameters and payload is required to restore the JSON structure of each part:

public Map<Part, PartContent> parse(String jwt) throws Exception {
    System.out.println("JWT currently resolved :" + jwt);
    Map<Part, PartContent> result = new HashMap<>(8);
    // All input JWT formats are considered valid
    StringTokenizer tokenizer = new StringTokenizer(jwt, DOT);
    String[] jwtParts = new String[3];
    int idx = 0;
    while (tokenizer.hasMoreElements()) {
        jwtParts[idx] = tokenizer.nextToken();
        idx++;
    }
    String headerPart = jwtParts[0];
    PartContent headerContent = new PartContent();
    headerContent.setRawContent(headerPart);
    headerContent.setPart(Part.HEADER);
    headerPart = new String(Base64.decodeBase64(headerPart), StandardCharsets.UTF_8);
    headerContent.setPairs(OBJECT_MAPPER.readValue(headerPart, new TypeReference<Map<String, Object>>() {
    }));
    result.put(Part.HEADER, headerContent);
    String payloadPart = jwtParts[1];
    PartContent payloadContent = new PartContent();
    payloadContent.setRawContent(payloadPart);
    payloadContent.setPart(Part.PAYLOAD);
    payloadPart = new String(Base64.decodeBase64(payloadPart), StandardCharsets.UTF_8);
    payloadContent.setPairs(OBJECT_MAPPER.readValue(payloadPart, new TypeReference<Map<String, Object>>() {
    }));
    result.put(Part.PAYLOAD, payloadContent);
    String signaturePart = jwtParts[2];
    PartContent signatureContent = new PartContent();
    signatureContent.setRawContent(signaturePart);
    signatureContent.setPart(Part.SIGNATURE);
    result.put(Part.SIGNATURE, signatureContent);
    return result;
}

enum Part {

    HEADER,

    PAYLOAD,

    SIGNATURE
}

@Data
public static class PartContent {

    private Part part;

    private String rawContent;

    private Map<String, Object> pairs;
}
Copy the code

Here is an attempt to parse with the JWT produced earlier:

public static void main(String[] args) throws Exception {
    JsonWebToken jsonWebToken = new JsonWebToken();
    String jwt = "EyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9. EyJpc3MiOiJ0aHJvd3giLCJqaWQiOjEwMDg3LCJleHAiOjE2MTMyMjc0NjgxNjh9. 7 skdudgxv - BP2p_CX yr3Na7WBvENNl--Pm4HQ8cJuEs";
    Map<Part, PartContent> parseResult = jsonWebToken.parse(jwt);
    System.out.printf(\nHEADER:%s\nPAYLOAD:%s\nSIGNATURE:%s%n",
            parseResult.get(Part.HEADER),
            parseResult.get(Part.PAYLOAD),
            parseResult.get(Part.SIGNATURE)
    );
}
Copy the code

The analytical results are as follows:

The current analytical JWT: eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9. EyJpc3MiOiJ0aHJvd3giLCJqaWQiOjEwMDg3LCJleHAiOjE2MTMyMjc0NjgxNjh9. 7 skdudgxv -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)Copy the code

Verification of JWT is based on the completion of JWT parsing. It is necessary to do a MAC signature for the parsed header parameters and valid load, and check with the parsed signature. In addition, you can customize validation of specific Claim items, such as expiration time and issuer. In general, different runtime exceptions will be customized for different situations to distinguish scenarios. Here, IllegalStateException will be thrown for convenience:

public void verify(String jwt) throws Exception {
    System.out.println("JWT currently verified :" + 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("Signature verification exception");
    }
    String iss = payloadContent.getPairs().get("iss").toString();
    / / iss
    if(! Objects.equals(iss,"throwx")) {
        throw new IllegalStateException("ISS check exception");
    }
    long exp = Long.parseLong(payloadContent.getPairs().get("exp").toString());
    // exp check, valid for 14 days
    if (System.currentTimeMillis() - exp > 24 * 3600 * 1000 * 14) {
        throw new IllegalStateException("Exp check error,JWT expired");
    }
    // Omit other verification items
    System.out.println("JWT verification passed");
}
Copy the code

Similarly, verification with the JWT generated above results as follows:

The current calibration JWT: eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9. EyJpc3MiOiJ0aHJvd3giLCJqaWQiOjEwMDg3LCJleHAiOjE2MTMyMjc0NjgxNjh9. 7 skdudgxv -BP2p_CXyr3Na7WBvENNl--Pm4HQ8cJuEs The current analytical JWT: eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9. EyJpc3MiOiJ0aHJvd3giLCJqaWQiOjEwMDg3LCJleHAiOjE2MTMyMjc0NjgxNjh9. 7 skdudgxv - BP2p_CXyr3Na7WBvENNl -- Pm4HQ8cJuEs 23:33:00. 174. [the main] INFO club. Throwable. JWT. JsonWebToken - 7skduDGxV-BP2p_CXyr3Na7WBvENNl--Pm4HQ8cJuEs JWT check passedCopy the code

The above code has a hard coding problem, just to re-implement the JWT generation, parsing and verification process using the simplest JWS implementation. The algorithm also uses HS256, which is very low in complexity and security, so it is not recommended to spend a lot of time implementing JWS in production. You can use existing JWT libraries such as Auth0 and JJWT.

JWT usage scenarios and actual combat

JWT is essentially a token, more commonly used as a session ID (session_ID) to ‘maintain session stickiness’ and carry authentication information (or, in JWT terminology, securely deliver Claims). The author remembers that a Session ID solution used a long time ago is generated and persisted by the server. The returned Session ID needs to be written into the user’s Cookie, and then the user must carry the Cookie with each request. The Session ID will map some authentication information of the user. This is all managed by the server. A very common example is the J(Ava)SESSIONID found in the Tomcat container. Unlike previous schemes, JWT is a stateless token that does not need to be persisted by the server, carrying data or session data. JWT requires only the integrity and validity of Claims. All valid data is encoded and stored in the JWT string when the JWT is generated. Because JWT is stateless, once it is issued, any client that gets JWT can interact with the server through it. Once JWT is leaked, it may cause serious security problems. Therefore, in practice, it is generally necessary to do the following:

  • JWTYou need to set the expiration date, which isexpthisClaimChecksum must be enabled
  • JWTNeed to create a blacklist, commonly usedjtithisClaimTechnically, it is possible to use a combination of Bloom filters and databases (simple operations can even be used in small numbers)RedistheSETData type)
  • JWSAs far as possible, the signature algorithm with high security, such asRSXXX
  • ClaimsTry not to write sensitive information
  • High-risk scenarios such as payment operations cannot be relied on aloneJWTAuthentication requires secondary authentication, such as SMS and fingerprint authentication

PS: Many of my colleagues work on projects that persist JWT, which actually goes against the design philosophy of JWT and uses JWT as a traditional session ID

JWT is generally used in authentication scenarios and works well with API gateways. In most cases, API gateways have some generic interfaces that do not require authentication, while others need to authenticate JWT and extract message payload content from JWT for invocation. For this scenario:

  • A custom annotation can be provided for the controller entry to identify the specific interface requiredJWTAuthentication, this scenario is inSpring Cloud GatewayYou need a custom implementation inJWTThe certificationWebFilter
  • One can be provided for pure routing and forwardingURIWhitelist collection. Matching whitelist does not need to be performedJWTAuthentication, this scenario is inSpring Cloud GatewayYou need a custom implementation inJWTThe certificationGlobalFilter

Below, I will post some backbone code for Spring Cloud Gateway and JJWT. Introducing dependencies:

<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-dependencies</artifactId>
            <version>Hoxton.SR10</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>
<dependencies>
    <dependency>
        <groupId>io.jsonwebtoken</groupId>
        <artifactId>jjwt-api</artifactId>
        <version>0.11.2</version>
    </dependency>
    <dependency>
        <groupId>io.jsonwebtoken</groupId>
        <artifactId>jjwt-impl</artifactId>
        <version>0.11.2</version>
        <scope>runtime</scope>
    </dependency>
    <dependency>
        <groupId>io.jsonwebtoken</groupId>
        <artifactId>jjwt-jackson</artifactId>
        <version>0.11.2</version>
        <scope>runtime</scope>
    </dependency>
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <version>1.18.18</version>
        <scope>provided</scope>
    </dependency>
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-gateway</artifactId>
    </dependency>
</dependencies>
Copy the code

Then write JwtSpi and the corresponding implementation HMAC256JwtSpiImpl:

@Data
public class CreateJwtDto {

    private Long customerId;

    private String customerName;

    private String customerPhone;
}

@Data
public class JwtCacheContent {

    private Long customerId;

    private String customerName;

    private String customerPhone;
}

@Data
public class VerifyJwtResultDto {

    private Boolean valid;

    private Throwable throwable;

    private long jwtId;

    private JwtCacheContent content;
}

public interface JwtSpi {

    /** * generates JWT **@param dto dto
     * @return String
     */
    String generate(CreateJwtDto dto);

    /** * validates JWT **@param jwt jwt
     * @return VerifyJwtResultDto
     */
    VerifyJwtResultDto verify(String jwt);

    /** * Add JWT to the banned list **@param jwtId jwtId
     */
    void blockJwt(long jwtId);

    /** * Determine if JWT is in the banned list **@param jwtId jwtId
     * @return boolean
     */
    boolean isInBlockList(long jwtId);
}

@Component
public class HMAC256JwtSpiImpl implements JwtSpi.InitializingBean.EnvironmentAware {

    private SecretKey secretKey;
    private Environment environment;
    private int minSeed;
    private String issuer;
    private int seed;
    private Random random;

    @Override
    public void afterPropertiesSet(a) throws Exception {
        String secretKey = Objects.requireNonNull(environment.getProperty("jwt.hmac.secretKey"));
        this.minSeed = Objects.requireNonNull(environment.getProperty("jwt.exp.seed.min", Integer.class));
        int maxSeed = Objects.requireNonNull(environment.getProperty("jwt.exp.seed.max", Integer.class));
        this.issuer = Objects.requireNonNull(environment.getProperty("jwt.issuer"));
        this.random = new Random();
        this.seed = (maxSeed - minSeed);
        this.secretKey = new SecretKeySpec(secretKey.getBytes(), "HmacSHA256");
    }

    @Override
    public void setEnvironment(Environment environment) {
        this.environment = environment;
    }

    @Override
    public String generate(CreateJwtDto dto) {
        long duration = this.random.nextInt(this.seed) + minSeed;
        Map<String, Object> claims = new HashMap<>(8);
        claims.put("iss", issuer);
        // The jTI here is best generated by sequential algorithms such as the Snowflake algorithm to ensure uniqueness
        claims.put("jti", dto.getCustomerId());
        claims.put("uid", dto.getCustomerId());
        claims.put("exp", TimeUnit.NANOSECONDS.toMillis(System.nanoTime()) + duration);
        String jwt = Jwts.builder()
                .setHeaderParam("typ"."JWT")
                .signWith(this.secretKey, SignatureAlgorithm.HS256)
                .addClaims(claims)
                .compact();
        Uid ->JwtCacheContent
        JwtCacheContent content = new JwtCacheContent();
        // redis.set(KEY[uid],toJson(content),expSeconds);
        return jwt;
    }

    @Override
    public VerifyJwtResultDto verify(String jwt) {
        JwtParser parser = Jwts.parserBuilder()
                .requireIssuer(this.issuer)
                .setSigningKey(this.secretKey)
                .build();
        VerifyJwtResultDto resultDto = new VerifyJwtResultDto();
        try {
            Jws<Claims> parseResult = parser.parseClaimsJws(jwt);
            Claims claims = parseResult.getBody();
            long jti = Long.parseLong(claims.getId());
            if (isInBlockList(jti)) {
                throw new IllegalArgumentException(String.format("jti is in block list,[i:%d]", jti));
            }
            long uid = claims.get("uid", Long.class);
            // JwtCacheContent content = JSON.parse(redis.get(KEY[uid]),JwtCacheContent.class);
            // resultDto.setContent(content);
            resultDto.setValid(Boolean.TRUE);
        } catch (Exception e) {
            resultDto.setValid(Boolean.FALSE);
            resultDto.setThrowable(e);
        }
        return resultDto;
    }

    @Override
    public void blockJwt(long jwtId) {}@Override
    public boolean isInBlockList(long jwtId) {
        return false; }}Copy the code

Then there are partial implementations of JwtGlobalFilter and JwtWebFilter:

@Component
public class JwtGlobalFilter implements GlobalFilter.Ordered.EnvironmentAware {

    private final AntPathMatcher pathMatcher = new AntPathMatcher();

    private List<String> accessUriList;

    @Autowired
    private JwtSpi jwtSpi;

    private static final String JSON_WEB_TOKEN_KEY = "X-TOKEN";
    private static final String UID_KEY = "X-UID";
    private static final String JWT_ID_KEY = "X-JTI";

    @Override
    public void setEnvironment(Environment environment) {
        accessUriList = Arrays.asList(Objects.requireNonNull(environment.getProperty("jwt.access.uris"))
                .split(","));
    }

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        ServerHttpRequest request = exchange.getRequest();
        // OPTIONS requests permission directly
        HttpMethod method = request.getMethod();
        if (Objects.nonNull(method) && Objects.equals(method, HttpMethod.OPTIONS)) {
            return chain.filter(exchange);
        }
        // Get the request path
        String requestPath = request.getPath().value();
        // Match the request path whitelist
        boolean matchWhiteRequestPathList = Optional.ofNullable(accessUriList)
                .map(paths -> paths.stream().anyMatch(path -> pathMatcher.match(path, requestPath)))
                .orElse(false);
        if (matchWhiteRequestPathList) {
            return chain.filter(exchange);
        }
        HttpHeaders headers = request.getHeaders();
        String token = headers.getFirst(JSON_WEB_TOKEN_KEY);
        if(! StringUtils.hasLength(token)) {throw new BusinessException(BusinessErrorCode.TOKEN_ERROR.getCode(), "token is null");
        }
        VerifyJwtResultDto resultDto = jwtSpi.verify(token);
        if (Objects.equals(resultDto.getValid(), Boolean.FALSE)) {
            throw new BusinessException(BusinessErrorCode.TOKEN_ERROR.getCode(), resultDto.getThrowable());
        }
        headers.set(JWT_ID_KEY, String.valueOf(resultDto.getJwtId()));
        headers.set(UID_KEY, String.valueOf(resultDto.getContent().getCustomerId()));
        return chain.filter(exchange);
    }

    @Override
    public int getOrder(a) {
        return 1; }}@Component
public class JwtWebFilter implements WebFilter {

    @Autowired
    private RequestMappingHandlerMapping requestMappingHandlerMapping;

    @Autowired
    private JwtSpi jwtSpi;

    private static final String JSON_WEB_TOKEN_KEY = "X-TOKEN";
    private static final String UID_KEY = "X-UID";
    private static final String JWT_ID_KEY = "X-JTI";

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
        // OPTIONS requests permission directly
        HttpMethod method = exchange.getRequest().getMethod();
        if (Objects.nonNull(method) && Objects.equals(method, HttpMethod.OPTIONS)) {
            return chain.filter(exchange);
        }
        HandlerMethod handlerMethod = requestMappingHandlerMapping.getHandlerInternal(exchange).block();
        if (Objects.isNull(handlerMethod)) {
            return chain.filter(exchange);
        }
        RequireJWT typeAnnotation = handlerMethod.getBeanType().getAnnotation(RequireJWT.class);
        RequireJWT methodAnnotation = handlerMethod.getMethod().getAnnotation(RequireJWT.class);
        if (Objects.isNull(typeAnnotation) && Objects.isNull(methodAnnotation)) {
            return chain.filter(exchange);
        }
        HttpHeaders headers = exchange.getRequest().getHeaders();
        String token = headers.getFirst(JSON_WEB_TOKEN_KEY);
        if(! StringUtils.hasLength(token)) {throw new BusinessException(BusinessErrorCode.TOKEN_ERROR.getCode(), "token is null");
        }
        VerifyJwtResultDto resultDto = jwtSpi.verify(token);
        if (Objects.equals(resultDto.getValid(), Boolean.FALSE)) {
            throw new BusinessException(BusinessErrorCode.TOKEN_ERROR.getCode(), resultDto.getThrowable());
        }
        headers.set(JWT_ID_KEY, String.valueOf(resultDto.getJwtId()));
        headers.set(UID_KEY, String.valueOf(resultDto.getContent().getCustomerId()));
        returnchain.filter(exchange); }}Copy the code

Finally, some configuration properties:

jwt.hmac.secretKey='00000000111111112222222233333333'
jwt.exp.seed.min=360000
jwt.exp.seed.max=8640000
jwt.issuer='throwx'
jwt.access.uris=/index,/actuator/*
Copy the code

Use the pit that JWT has encountered

The API gateway in charge of the author uses JWT for authentication scenarios, RS256 with higher security is used in algorithm, and RSA algorithm is used for signature generation. At the initial stage of project launch, the expiration time of JWT was fixed at 7 days, and the production log found that the API gateway periodically occurred the phenomenon of “suspended animation”, as shown in the following:

  • NginxSelf-check Periodically, some or all of the self-check interface invocation times outAPIThe gateway node is down
  • APIBelongs to the machine where the gateway residesCPUIt spikes periodically, leveling off when user visits are low
  • throughELKCheck logs and find that the fault occurs in theJWTCentralized log traces of expiration and regeneration

The investigation results show that the centralized expiration of JWT and the use of RSA algorithm for signature during regeneration are CPU-intensive operations, and the simultaneous generation of a large number of JWT will lead to excessive CPU load on the service machine. The initial solutions are:

  • JWTWhen generated, add a random number to the expiration time, for example360000(milliseconds in 1 hour) to 8640000(milliseconds in 24 hours)Take a random value between add to the current timestamp plus7Days getexpvalue

This approach does not work in some old user marketing scenarios where old users have not logged in for a long time and their JWT cached by their clients is generally out of date. Sometimes, the operation will wake up the old users through marketing activities, and a large number of old users may re-log in to generate JWT in large quantities. For this scenario, two solutions are proposed:

  • Generated for the first timeJWTConsider extending the expiration date, but the longer you wait, the greater the risk
  • ascensionAPIThe hardware configuration of the gateway machine, in particularCPUConfiguration. Many cloud vendors have flexible capacity expansion solutions to cope with such sudden traffic scenarios

summary

The mainstream JWT scheme is JWS, which only encodes and signs without encryption. It must be noted that THE JWS scheme is stateless and insecure. Multiple authentication should be done for key operations, and a blacklist mechanism should also be made to prevent security problems caused by JWT leakage. JWT is not stored on the server, which is both its strength and its weakness. Many software architectures are not perfect, so it’s a tradeoff.

References:

  • RFC 7519
  • JJWT partial source code

(C-3-W E-A-20210219)