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 onJSON
Encrypting the contents of data structures, encryption mechanisms to encrypt any 8-bit byte sequence, provide integrity protection and increase the difficulty of cracking,JWE
The 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 usingJSON
Data structure andBASE64URL
Encoding represents a digital signature or message authentication code (MAC
) authenticated content, digital signature orMAC
Can provide integrity protection,JWS
The 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 Web
Algorithms, digital signatures orMAC
Algorithm, applied toJWS
The 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 definedJWT Cannot be accepted for processing |
iat | Issued At | JWT Time stamp at launch |
jti | JWT ID | JWT Unique 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 protectJWS Encryption and decryption algorithm |
jku | JWK Set URL | A set ofJSON Of the encoded public keyURL , one of which is used forJWS Key for digital signature |
jwk | JSON Web Key | Used forJWS Public key corresponding to the digital signature key |
kid | Key ID | Used to protectJWS Enter the key |
x5u | X.509 URL | X.509 related |
x5c | X.509 Certificate Chain | X.509 related |
x5t | X.509 Certificate SHA-1 Thumbprin | X.509 related |
x5t#S256 | X.509 Certificate SHA-256 Thumbprint | X.509 related |
typ | Type | Type, for exampleJWT ,JWS orJWE , etc. |
cty | Content Type | Content type, decidepayload Part 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:
JWT
You need to set the expiration date, which isexp
thisClaim
Checksum must be enabledJWT
Need to create a blacklist, commonly usedjti
thisClaim
Technically, it is possible to use a combination of Bloom filters and databases (simple operations can even be used in small numbers)Redis
theSET
Data type)JWS
As far as possible, the signature algorithm with high security, such asRSXXX
Claims
Try not to write sensitive information- High-risk scenarios such as payment operations cannot be relied on alone
JWT
Authentication 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 required
JWT
Authentication, this scenario is inSpring Cloud Gateway
You need a custom implementation inJWT
The certificationWebFilter
- One can be provided for pure routing and forwarding
URI
Whitelist collection. Matching whitelist does not need to be performedJWT
Authentication, this scenario is inSpring Cloud Gateway
You need a custom implementation inJWT
The 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:
Nginx
Self-check Periodically, some or all of the self-check interface invocation times outAPI
The gateway node is downAPI
Belongs to the machine where the gateway residesCPU
It spikes periodically, leveling off when user visits are low- through
ELK
Check logs and find that the fault occurs in theJWT
Centralized 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:
JWT
When 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 plus7
Days getexp
value
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 time
JWT
Consider extending the expiration date, but the longer you wait, the greater the risk - ascension
API
The hardware configuration of the gateway machine, in particularCPU
Configuration. 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)