1. Understanding JWT
1.1. What is JWT?
JSON Web Token (JWT) is an open standard( RFC 7519 ), it defines a compact and independent way to securely transfer information between parties as JSON objects. This information can be verified and trusted because it is digitally signed. JWT can sign with a key (using HMAC algorithm) or with RSA or ECDSA's public / private key pair.
1.2 when to use JWT?
- Authorization: This is the most common scenario for using JWT. After the user logs in, each subsequent request will contain JWT, allowing the user to access the routes, services and resources allowed by the token. Single sign on is a feature of JWT that is widely used today because it costs little and can be easily used across different domains.
- Information exchange: JSON Web tokens are a good way to securely transfer information between parties. Because JWT can sign (for example, using a public / private key pair), you can determine that the sender is the person they say. In addition, because signatures are calculated using headers and payloads, you can also verify that the content has not been tampered with.
1.3. Structure of JWT?
JWT consists of three parts, consisting of points (.) Separation, respectively:
- Header
- Payload
- Signature
The format is as follows:
aaaaa.bbbbb.cccccc
1.3.1,Header
The header usually consists of two parts: the type of token (i.e. JWT) and the signature algorithm being used (such as HMAC SHA256 or RSA).
{ "alg": "HS256", "typ": "JWT" }
Then encode this JSON through Base64Url to form the first part of JWT.
1.3.2,PayLoad
The second part of the token is the payload, which contains the declaration. Declarations are statements about entities (usually users) and other data. There are three types of declarations: Registration declaration, public declaration and private declaration.
-
Registration statement : These are a predefined set of declarations that are not required, but it is recommended to provide a set of useful and interoperable declarations. Some of them are: iss (issuer), exp (expiration time), sub (subject), aud (audience) etc..
Note that the declaration name is only three characters long, because JWT should be compact.
-
Public statement : these can be defined at will by those who use JWT. However, in order to avoid conflict, we should IANA JSON Web token registry Define them in or as URI s that contain anti-collision namespaces.
-
Private statement : These are custom statements created to share information between parties who agree to use them. They are neither registration statements nor public statements.
PayLoad example:
{ "iss": "songshu", "sub": "1234567890", "name": "haha", "admin": true }
The PayLoad is then Base64Url encoded to form the second part of the JSON Web token.
Warning:
Note that for signed tokens, this information is protected against tampering, but can be read by anyone. Do not place confidential information in the payload or header element of the JWT unless it is encrypted.
1.3.3,Signature
To create a signature part, you must obtain and sign the encoded header, encoded payload, secret, algorithm specified in the header.
For example, if you want to use the HMAC SHA256 algorithm, you will create a signature as follows:
HMACSHA256( base64UrlEncode(header) + "." + base64UrlEncode(payload), secret)
The signature is used to verify that the message has not changed during this process, and for tokens signed with the private key, it can also verify whether the sender of the JWT is the sender it says.
1.3.4 complete JWT
Putting all the content together, the output is three Base64 URL strings separated by dots, which can be easily passed in HTML and HTTP environments, and more compact than XML based standards such as SAML.
The following shows a JWT that has an encoded previous header and payload and is signed with a key.
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJzb25nc2h1IiwiZXhwIjoxNjQ0NDg0MzM2LCJ1c2VybmFtZSI6Imxpc2kifQ.3nqmjgxMMw67jT-KO3T8VC7AQGhI9wU9GucQnAV9NeA
1.4 how does JSON Web token work?
In authentication, a JSON Web token is returned when a user successfully logs in using their credentials.
Whenever a user wants to access a protected route or resource, the user agent should send a JWT, usually using the holder schema in the authorization header. The contents of the header should be as follows:
Authorization: Bearer <token>
If the token is sent in the header, cross source resource sharing (CORS) will not be a problem because it does not use cookies.
The following figure shows how to obtain JWT and use it to access API or resources:
- The application or client requests authorization to the authorization server. This is done through one of the different authorization flows. For example, typical OpenID Connect Compatible Web applications will use Authorization code flow Through the endpoint/ oauth/authorize
- After authorization is granted, the authorization server will return an access token to the application.
- Applications use access tokens to access protected resources, such as API s.
1.5 why use JWT?
1.5.1. Session authentication based on tradition
Certification method:
Since http is a stateless protocol, which means that if the user provides the user name and password to the application for authentication, the user needs to authenticate again in the next request. According to the http protocol, we don't know which user sent the request, so in order to identify which user sent the request, We can only store a copy of user information in the server. This user information will be returned to the browser and told to save it as a cookie so that it can be sent to the server when the next request is made, so that we can identify which user made the request.
Certification process:
1. The client sends an authentication request to the Web Application
2. Web Application response client cookie
3. The client sends a cookie carrying request to the Web Application and finds the corresponding session
2. The first program of JWT (Spring Boot)
2.1. Introducing dependency
<dependency> <groupId>com.auth0</groupId> <artifactId>java-jwt</artifactId> <version>3.18.3</version> </dependency>
2.2. Create a Token and sign it
@SpringBootTest class JwtApplicationTests { @Test void contextLoads() { //Create an Algorithm object and set the secret key ("secret") Algorithm algorithm = Algorithm.HMAC256("secret"); //Create date class Calendar calendar = Calendar.getInstance(); //Add 90 seconds to the current time calendar.add(Calendar.SECOND,90); //Create token instance String token = JWT.create() .withIssuer("songshu") //Set issuer .withClaim("username", "lisi") //Set custom user name .withExpiresAt(calendar.getTime()) //Set expiration time .sign(algorithm); //Setting the signature is confidential and requires a secret key System.out.println(token); } }
Generate token result:
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJzb25nc2h1IiwiZXhwIjoxNjQ0NDg0MzM2LCJ1c2VybmFtZSI6Imxpc2kifQ.3nqmjgxMMw67jT-KO3T8VC7AQGhI9wU9GucQnAV9NeA
2.3. Parsing according to Token
@SpringBootTest class JwtApplicationTests { @Test void VerifierTest() { String token ="eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJzb25nc2h1IiwiZXhwIjoxNjQ0NDg0MzM2LCJ1c2VybmFtZSI6Imxpc2kifQ.3nqmjgxMMw67jT-KO3T8VC7AQGhI9wU9GucQnAV9NeA"; //Create an Algorithm object and set the secret key ("secret") Algorithm algorithm = Algorithm.HMAC256("secret"); JWTVerifier jwtVerifier = JWT.require(algorithm).build(); DecodedJWT jwt = jwtVerifier.verify(token); System.out.println(jwt.getClaim("username")); System.out.println(jwt.getIssuer()); } }
Raised if the token has an invalid signature or does not meet the declaration requirements. JWTVerificationException
3. JWT usage
3.1,Header
Algorithm ("alg")
Returns the "Algorithm" value, or null if not defined in the header.
String algorithm = jwt.getAlgorithm();
Type ("typ")
Returns the Type value. If it is not defined in the Header, it returns null.
String type = jwt.getType();
Content Type ("cty")
Returns the "Content Type" value, or null if it is not defined in the header.
String contentType = jwt.getContentType();
Key Id ("kid")
Returns the Key Id value. If it is not defined in the Header, it returns null.
String keyId = jwt.getKeyId();
Private Claims
Other declarations defined in the token header can be obtained by calling and passing the declaration name. Claims are always returned even if they cannot be found. You can check whether the declared value is empty by calling. getHeaderClaim() claim.isNull()
Claim claim = jwt.getHeaderClaim("owner");
When creating tokens with, you can specify header declarations by calling and passing declaration mappings. JWT.create() withHeader()
Map<String, Object> headerClaims = new HashMap(); headerClaims.put("owner", "auth0"); String token = JWT.create() .withHeader(headerClaims) .sign(algorithm);
3.2,PayLoad
Issuer ("iss")
Returns the "Issuer" value or null if not defined in the payload.
String issuer = jwt.getIssuer();
Subject ("sub")
Returns the Subject value or null if not defined in the payload.
String subject = jwt.getSubject();
Audience ("aud")
Returns the Audience value, or null if it is not defined in the payload.
List<String> audience = jwt.getAudience();
Expiration Time ("exp")
Returns the value of "Expiration Time", or null if not defined in "payload".
Date expiresAt = jwt.getExpiresAt();
Not Before ("nbf")
Returns a "Not Before" value, or null if not defined in the payload.
Date notBefore = jwt.getNotBefore();
Issued At ("iat")
Returns the "Issued At" value, or null if not defined in the payload.
Date issuedAt = jwt.getIssuedAt();
JWT ID ("jti")
Returns the JWT ID value, or null if not defined in the payload.
String id = jwt.getId();
Private Claims
Other declarations defined in the payload of the token can be obtained by calling or passing the declaration name. Claims are always returned even if they cannot be found. You can check whether the declared value is empty by calling. getClaims() getClaim() claim.isNull()
Map<String, Claim> claims = jwt.getClaims(); //Key is the Claim name Claim claim = claims.get("isAdmin");
or
Claim claim = jwt.getClaim("isAdmin");
When using to create a token, you can specify a custom declaration by calling and passing a name and value. JWT.create() withClaim()
String token = JWT.create() .withClaim("name", 123) .withArrayClaim("array", new Integer[]{1, 2, 3}) .sign(algorithm);
You can also create JWT: withPayload() by calling the mapping of the declared name and passing it to the value
Map<String, Object> payloadClaims = new HashMap<>(); payloadClaims.put("@context", "https://auth0.com/"); String token = JWT.create() .withPayload(payloadClaims) .sign(algorithm);
You can also validate custom declarations on by calling and passing names and required values. JWT.require() withClaim()
JWTVerifier verifier = JWT.require(algorithm) .withClaim("name", 123) .withArrayClaim("array", 1, 2, 3) .build(); DecodedJWT jwt = verifier.verify("my.jwt.token");
4. Customize the simple JWTUtils class
4.1. Create JWTUtils class
public class JWTUtils { //Set secret key private static final String SING = "secret"; /** * Get token * @param map * @return */ public static String getToken(Map<String,String> map) { //Create an Algorithm object and set the secret key ("secret") Algorithm algorithm = Algorithm.HMAC256(SING); //Create date class Calendar calendar = Calendar.getInstance(); //Add 90 seconds to the current time calendar.add(Calendar.DATE,7); //Create JWT build JWTCreator.Builder builder = JWT.create(); map.forEach((k,v) -> { builder.withClaim(k,v); }); //Specify the expiration time and get a token String token = builder.withExpiresAt(calendar.getTime()).sign(algorithm); return token; } /** * Verify token * @param token * @return */ public static DecodedJWT verify(String token) { try { DecodedJWT decodedJWT = JWT.require(Algorithm.HMAC256(SING)).build().verify(token); return decodedJWT; } catch (JWTVerificationException exception){ //Invalid signature/claims throw exception; } } }
4.2 test
@Test void JWTUtilsTest() { HashMap<String,String> map = new HashMap<>(); map.put("username","songshu"); //Get token String token = JWTUtils.getToken(map); System.out.println(token); //verification DecodedJWT verify = JWTUtils.verify(token); if (verify != null) { String username = verify.getClaim("username").asString(); System.out.println(username); } }
5. Spring boot integrates JWT
5.1 preparations
5.1.1. Introducing dependency
<dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.0.5</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> </dependency>
5.1.2. Create database table
SET NAMES utf8mb4; SET FOREIGN_KEY_CHECKS = 0; -- ---------------------------- -- Table structure for user -- ---------------------------- DROP TABLE IF EXISTS `user`; CREATE TABLE `user` ( `id` int NOT NULL AUTO_INCREMENT, `username` varchar(8) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL, `password` varchar(16) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL, PRIMARY KEY (`id`) USING BTREE, UNIQUE INDEX `username`(`username`) USING BTREE ) ENGINE = InnoDB AUTO_INCREMENT = 2 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic; SET FOREIGN_KEY_CHECKS = 1;
5.1.3 configuration file
server.port=8081 #mysql configuration spring.datasource.url=jdbc:mysql://localhost:3306/school?characterEncoding=utf-8&serverTimezone=GMT%2B8 spring.datasource.password=root spring.datasource.username=root spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
5.2. Code writing
5.2.1. Create User class
@Data @AllArgsConstructor @NoArgsConstructor @ToString public class User { private String id; private String name; private String password; }
5.2.2. Create UserMapper interface
@Mapper public interface UserMapper extends BaseMapper<User> { }
Start scan on package class
@SpringBootApplication @MapperScan("com.songshu.jwt.mapper") public class JwtApplication { public static void main(String[] args) { SpringApplication.run(JwtApplication.class, args); } }
5.2.3. Create UserService interface and UserServiceImpl class
@Service public class UserServiceImpl implements UserService{ @Autowired private UserMapper userMapper; @Override public User login(User user) { QueryWrapper<User> queryWrapper = new QueryWrapper<>(); queryWrapper.eq("username",user.getUsername()); queryWrapper.eq("password",user.getPassword()); //Authentication based on user name and password User user1 = userMapper.selectOne(queryWrapper); if (user1 != null) { return user1; } throw new RuntimeException("Account or password does not exist"); } }
5.2.4. Create UserController class
@RestController @RequestMapping("/user") public class UserController { @Autowired private UserService userService; /** * Sign in * @param user * @return */ @PostMapping("/login") public Map<String,Object> login(User user) { Map<String,Object> map = new HashMap<>(); try { Map<String,String> payLoad = new HashMap<>(); User user1 = userService.login(user); payLoad.put("id",user1.getId().toString()); payLoad.put("username",user1.getUsername()); //Generate token String token = JWTUtils.getToken(payLoad); map.put("state",true); map.put("msg","Login successful"); map.put("token",token); return map; } catch (Exception e) { map.put("state",false); map.put("msg",e.getMessage()); } return map; } /** * Test token * @param token * @return */ @PostMapping("/testToken") public Map<String,Object> testToken(String token) { Map<String,Object> map = new HashMap<>(); try { DecodedJWT decodedJWT = JWTUtils.verify(token); map.put("state",true); map.put("msg","Token authentication succeeded"); return map; } catch (SignatureVerificationException e) { //Signature verification exception map.put("msg","Signature verification exception"); } catch (TokenExpiredException e) { //token expiration exception map.put("msg","token Expiration exception"); } catch (AlgorithmMismatchException e) { //Algorithm inconsistency exception map.put("msg","Algorithm inconsistency exception"); } catch (Exception e) { map.put("msg","Signature verification exception"); } map.put("state",false); return map; } }
5.3 test by Postman
Interface I: http://localhost:8081/user/login
K/V in Body:
username: lishi password: 123456
Response result:
{ "msg": "Login successful", "state": true, "token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpZCI6IjEiLCJleHAiOjE2NDUxMDU1MTksInVzZXJuYW1lIjoibGlzaGkifQ.0L7WPFBrcrajzRlRJcWLQm0_OoqS_vLmlE2Kpviv2c0" }
Interface 2: http://localhost:8081/user/testToken
K/V in Body:
token: eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpZCI6IjEiLCJleHAiOjE2NDUxMDU1MTksInVzZXJuYW1lIjoibGlzaGkifQ.0L7WPFBrcrajzRlRJcWLQm0_OoqS_vLmlE2Kpviv2c0
Response result:
{ "msg": "Token authentication succeeded", "state": true }
5.4 interceptor
Using the above methods, you need to pass the token data every time, and each method needs to verify the token, resulting in code redundancy. How should you optimize it?
- Optimize with interceptors
5.4.1. Create JWTInterceptor interceptor
public class JWTInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { Map<String,Object> map = new HashMap<>(); //JWT official website suggests putting the token into the header //Get token from request header String token = request.getHeader("token"); try { DecodedJWT decodedJWT = JWTUtils.verify(token); return true; } catch (SignatureVerificationException e) { //Signature verification exception e.printStackTrace(); map.put("msg","Signature verification exception"); } catch (TokenExpiredException e) { //token expiration exception e.printStackTrace(); map.put("msg","token Expiration exception"); } catch (AlgorithmMismatchException e) { //Algorithm inconsistency exception e.printStackTrace(); map.put("msg","Algorithm inconsistency exception"); } catch (Exception e) { e.printStackTrace(); map.put("msg","Signature verification exception"); } map.put("state",false); //Convert Map to Json String data = new ObjectMapper().writeValueAsString(map); response.setContentType("application/json;charset=utf-8"); response.getWriter().write(data); return false; } }
5.4.2. Create InterceptorConfig configuration class and configure interceptor
@Configuration public class InterceptorConfig implements WebMvcConfigurer { @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new JWTInterceptor()) .addPathPatterns("/user/testToken") .excludePathPatterns("/user/login"); } }
5.4.3. Modify UserController class
/** * Test token * @return */ @PostMapping("/testToken") public Map<String,Object> testToken() { Map<String,Object> map = new HashMap<>(); //Processing business logic map.put("state",true); map.put("msg","Token authentication succeeded"); return map; }
5.4.4 test
Same as above.