1. Introduction

Json Web Token (JWT) is a common Token technology used to separate the front and back ends in recent years, and is the most popular cross-domain authentication solution. You can learn more about JWT by reading about the Web stateless session Token technology in the article. Today we write a generic JWT service. The way to obtain DEMO is at the end of the article, and the implementation is under the JWT related package

2. spring-security-jwt

Spring-security-jwt is a JWT toolkit provided by Spring Security Crypto.

  <dependency>
        <groupId>org.springframework.security</groupId>
        <artifactId>spring-security-jwt</artifactId>
        <version>${spring-security-jwt.version}</version>
  </dependency>        Copy the code

Only one core classes: org. Springframework. Security. JWT. JwtHelper. It provides two very useful static methods.

3. JWT coding

The first static method JwtHelper provides is encode(CharSequence Content, Signer Signer). This method is used to generate JWT, and it needs to specify payload and Signer signature algorithms. Payload Payload Payload payload payload payload payload payload payload

  • issJWT issued to
  • subThe users JWT is targeting
  • audThe party receiving the JWT
  • iatIssue time of JWT
  • expThe expiration date of the JWT must be greater than the issue timeiat
  • jtiThe unique identifier of the JWT is mainly used as a one-time token to avoid replay attacks

In addition to the basic information provided above, we can define some information that we need to pass, such as the target user’s permission set, etc. Be careful not to pass sensitive information like passwords, because the first two sections of JWT are BASE64 encoded and almost plain text.

3.1 Constructing payload in JWT

Let’s build payload first:

/** * Build JWT payload ** @author Felordcn * @since 11:27 2019/10/25 **/ public class JwtPayloadBuilder {private Map<String, String> payload = new HashMap<>(); /** * Additional attributes */ private Map<String, String> Additional; /** * JWT **/ private String iss; /** * private String sub; /** * private String aud; Private LocalDateTime exp; private LocalDateTime exp; /** * private LocalDateTime iat = localDatetime.now (); /** * Roles = new HashSet<>(); /** * Roles = new HashSet<>(); /** * private String jti = idutil.simpleuuid (); /** * private String jti = simpleUUID(); public JwtPayloadBuilder iss(String iss) { this.iss = iss; return this; } public JwtPayloadBuilder sub(String sub) { this.sub = sub; return this; } public JwtPayloadBuilder aud(String aud) { this.aud = aud; return this; } public JwtPayloadBuilder roles(Set<String> roles) { this.roles = roles; return this; } public JwtPayloadBuilder expDays(int days) { Assert.isTrue(days > 0, "jwt expireDate must after now"); this.exp = this.iat.plusDays(days); return this; } public JwtPayloadBuilder additional(Map<String, String> additional) { this.additional = additional; return this; } public String builder() { payload.put("iss", this.iss); payload.put("sub", this.sub); payload.put("aud", this.aud); payload.put("exp", this.exp.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"))); payload.put("iat", this.iat.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"))); payload.put("jti", this.jti); if (! CollectionUtils.isEmpty(additional)) { payload.putAll(additional); } payload.put("roles", JSONUtil.toJsonStr(this.roles)); return JSONUtil.toJsonStr(JSONUtil.parse(payload)); }}Copy the code

By building the JwtClaimsBuilder class, we can easily construct the payload JSON string required by JWT and pass it to the content in encode(CharSequence Content, Signer Signer).

3.2 Generating and signing an RSA Key

To generate JWT tokens we also need to use the RSA algorithm for signature. Here we use the certificate management tool Keytool provided by the JDK to generate an RSA certificate in JKS format.

Certificate generation command reference:

 keytool -genkey -alias felordcn -keypass felordcn -keyalg RSA -storetype PKCS12 -keysize 1024 -validity 365 -keystore d:/keystores/felordcn.jks -storepass 123456  -dname "CN=(Felord), OU=(felordcn), O=(felordcn), L=(zz), ST=(hn), C=(cn)"
 Copy the code

Where -alias felordcn -storePass 123456 we want to use as a configuration note down. We will use the class defined below to read the certificate

package cn.felord.spring.security.jwt; import org.springframework.core.io.ClassPathResource; import java.security.KeyFactory; import java.security.KeyPair; import java.security.KeyStore; import java.security.PublicKey; import java.security.interfaces.RSAPrivateCrtKey; import java.security.spec.RSAPublicKeySpec; /** * KeyPairFactory * * @author Felordcn * @since 13:41 2019/10/25 **/ class KeyPairFactory { private KeyStore store; private final Object lock = new Object(); ** @param keyPath JKS file in resources classpath * @param keyAlias -alias generated by the keytool felordcn * @param KeyPass keytool Generated -keypass value felordcn * @return the key pair public/private key pair */ KeyPair create(String keyPath, String keyAlias, String keyPass) { ClassPathResource resource = new ClassPathResource(keyPath); char[] pem = keyPass.toCharArray(); try { synchronized (lock) { if (store == null) { synchronized (lock) { store = KeyStore.getInstance("jks"); store.load(resource.getInputStream(), pem); } } } RSAPrivateCrtKey key = (RSAPrivateCrtKey) store.getKey(keyAlias, pem); RSAPublicKeySpec spec = new RSAPublicKeySpec(key.getModulus(), key.getPublicExponent()); PublicKey publicKey = KeyFactory.getInstance("RSA").generatePublic(spec); return new KeyPair(publicKey, key); } catch (Exception e) { throw new IllegalStateException("Cannot load keys from store: " + resource, e); }}}Copy the code

By acquiring KeyPair, you can obtain the public and private keys to generate the two elements of the Jwt. We can encapsulate the method of generating a Jwt Token with the JwtPayloadBuilder defined earlier:

     private String jwtToken(String aud, int exp, Set<String> roles, Map<String, String> additional) {
         String payload = jwtPayloadBuilder
                 .iss(jwtProperties.getIss())
                 .sub(jwtProperties.getSub())
                 .aud(aud)
                 .additional(additional)
                 .roles(roles)
                 .expDays(exp)
                 .builder();
         RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate();
 
         RsaSigner signer = new RsaSigner(privateKey);
         return JwtHelper.encode(payload, signer).getEncoded();
     }Copy the code

Usually Jwt tokens come in pairs, one for accessToken carried in normal requests and the other for accessToken refreshToken only. And refreshTokens expire for a relatively long time. When accessToken is invalid and refreshToken is valid, we can obtain a new Jwt Token pair through refreshToken. When both fail the user must log in again.

Jwt Token pairs are generated as follows:

public JwtTokenPair jwtTokenPair(String aud, Set<String> roles, Map<String, String> additional) { String accessToken = jwtToken(aud, jwtProperties.getAccessExpDays(), roles, additional); String refreshToken = jwtToken(aud, jwtProperties.getRefreshExpDays(), roles, additional); JwtTokenPair jwtTokenPair = new JwtTokenPair(); jwtTokenPair.setAccessToken(accessToken); jwtTokenPair.setRefreshToken(refreshToken); JwtTokenStorage. Put (jwtTokenPair, aud); return jwtTokenPair; }Copy the code

Usually Jwt Token pairs are put into the cache as they are returned to the foreground. Expiration Policy You can choose to process the refreshToken separately or use the expiration time of the refreshToken.

4. JWT decoding and verification

The second static method JwtHelper provides is Jwt decodeAndVerify(String Token, SignatureVerifier verifier), which verifies and decodes Jwt tokens. After obtaining the token in the request, we parse out some information about the user. The corresponding tokens in the cache are then compared and verified for validity (including expiration).

** @param jwtToken The JWT token * @return the JWT claims */ public JSONObject decodeAndVerify(String jwtToken) { Assert.hasText(jwtToken, "jwt token must not be bank"); RSAPublicKey rsaPublicKey = (RSAPublicKey) this.keyPair.getPublic(); SignatureVerifier rsaVerifier = new RsaVerifier(rsaPublicKey); Jwt jwt = JwtHelper.decodeAndVerify(jwtToken, rsaVerifier); String claims = jwt.getClaims(); JSONObject jsonObject = JSONUtil.parseObj(claims); String exp = jsonObject.getStr(JWT_EXP_KEY); If (isExpired(exp)) {throw new IllegalStateException(" JWT token is expired"); } return jsonObject; }Copy the code

The payload in the valid Jwt Token is interpreted as a JSON object to facilitate subsequent operations.

Configuration of 5.

We pulled out the configurable items of JWT and put them in JwtProperties as follows:

/** * Jwt config file in SpringBoot Application.yml ** @author Felordcn * @since 15:06 2019/10/25 */ @data @ConfigurationProperties(prefix=JWT_PREFIX) public class JwtProperties { static final String JWT_PREFIX= "jwt.config"; /** * whether to enable */ private Boolean enabled; /** * JKS path */ private String keyLocation; /** * key alias */ private String keyAlias; /** * key store pass */ private String keyPass; /** * JWT **/ private String iss; /** * private String sub; /** * Access JWT token valid days */ private int accessExpDays; /** * refresh token valid number of days */ private int refreshExpDays; }Copy the code

We can then configure the javaConfig of JWT as follows:

/** * JwtConfiguration * * @author Felordcn * @since 16 :54 2019/10/25 */ @EnableConfigurationProperties(JwtProperties.class) @ConditionalOnProperty(prefix = "jwt.config",name = "enabled") @Configuration public class JwtConfiguration { /** * Jwt token storage . * * @return the jwt token storage */ @Bean public JwtTokenStorage jwtTokenStorage() { return new JwtTokenCacheStorage(); } /** * Jwt token generator. * * @param jwtTokenStorage the jwt token storage * @param jwtProperties the jwt properties * @return the jwt token generator */ @Bean public JwtTokenGenerator jwtTokenGenerator(JwtTokenStorage jwtTokenStorage, JwtProperties jwtProperties) { return new JwtTokenGenerator(jwtTokenStorage, jwtProperties); }}Copy the code

You can then authenticate the JwtToken pair with JwtTokenGenerator encoding/decoding and handle the JwtToken cache with JwtTokenStorage. I used Spring Cache Ehcache for caching, but you can switch to Redis. See DEMO for unit tests

6. Summary

Today we wrote a set of JWT logic by hand using Spring-security-jWT. This will be useful for your future integration with Spring Security and Shiro. In the next article, we will cover JWT with Spring Security. Please follow our public account: Felordcn to get more information.

The DEMO can also be obtained by following the public account and replying to DAY05.

Follow our public id: Felordcn for more information

Personal blog: https://felord.cn