Now we need to complete the following requirements:
After the user executes the login interface, a token needs to be generated and returned to the front end. Then, the front end carries the token in the request header to request other background interfaces.
Before completing this requirement, let's first understand what JWT is.
1. Understand JWT
Introduction to JWT 1.1
Introduction to JWT:
The full name of JWT is Jason web token. It is a form defined in RFC 7519 for securely transmitting information as a Json object. The information stored in JWT is "digitally signed", so it can be trusted and understood. JWT can be signed using HMAC algorithm or public / private key of RSA/ECDSA.
JWT function:
JWT has the following two functions:
- Authentication: once 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 of its low overhead.
- Information exchange: JWT is a way to safely transmit information. JWT is signed and authenticated by using public / private key. In addition, because the signature is calculated using head and payload, you can also verify whether the content has been tampered with.
1.2 structure of JWT
JWT is mainly composed of three parts, each of which is made of The parts are divided into:
- Header header
- Payload
- Signature
Therefore, JWT is usually: XXX yyyy. zzzzz
For example:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9 . eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ . SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
1.2.1 Header
Header is the header of JWT, including two parts of information:
- Token type: JWT
- Encryption algorithm: HMAC SHA256 or RSA
For example, the default header of JWT is:
{ "alg": "HS256", // algorithm "typ": "JWT" // type }
Then the Header is base64 encoded to form the first part of JWT:
Base64eyJhbGciOiAiSFMyNTYiLCAidHlwIjogIkpXVCJ9
In Java, Base64 coding can be implemented. As follows:
This is Base64 encoding and decoding of the default header of JWT:
public class Test { public static void main(String[] args) throws Exception{ Base64.Encoder encoder = Base64.getEncoder(); Base64.Decoder decoder = Base64.getDecoder(); String header = "{\"alg\": \"HS256\", \"typ\": \"JWT\"}"; byte[] headerBytes = header.getBytes(); // code String encodeHeader = encoder.encodeToString(headerBytes); System.out.println(encodeHeader); // decode byte[] decode = decoder.decode(encodeHeader); System.out.println(new String(decode, "UTF-8")); } }
1.2.2 Payload
Payload contains a declaration. Declarations are declarations about entities (usually users) and other data. Similarly, it uses base64 encoding to form the second part of JWT. There are three types of declarations: registered, public, and private
1. registered declaration
registered declaration: it contains a set of predefined declarations recommended to use, mainly including:
- iss: jwt issuer
- Sub: the user JWT is targeting
- aud: the party receiving jwt
- Exp: the expiration time of JWT, which must be greater than the issuing time
- nbf: define the time before which this jwt is unavailable
- IAT: issuing time of JWT
- JTI: the unique identity of JWT, which is mainly used as a one-time token to avoid replay attacks.
2. public declaration
Public declaration: any information can be added to the public declaration. Generally, the user's relevant information or other necessary information for business needs can be added, but it is not recommended to add sensitive information, because this part can be decrypted at the client.
3. private statement
private declaration: a custom declaration designed to share information between parties who agree to use them. It is neither a registration declaration nor a public declaration.
For example:
{ "sub": "1234567890", "name": "John Doe", "iat": 1516239022 }
- name: custom field
- sub/iat: standard statement
Then the Payload is base64 encoded to form the second part:
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ
1.2.3 Signature
Signature refers to visa information, which consists of three parts:
- Header (after Base64)
- Payload (after Base64)
- secret
For example, we need HMAC SHA256 algorithm for signature:
HMACSHA256( base64UrlEncode(header) + "." + base64UrlEncode(payload), secret )
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 the true identity of the sender of the JWT
2. Use JWT
Import dependency:
<dependency> <groupId>com.auth0</groupId> <artifactId>java-jwt</artifactId> <version>3.4.0</version> </dependency>
Here is the test class of SpringBoot. It can be different from me here. Just change these two methods to main.
public class AppTest { // Key (complex) private static final String SECRET = "1qazXSW2"; @Test public void createToken() { Map<String, Object> map = new HashMap<>(); Calendar expireTime = Calendar.getInstance(); expireTime.add(Calendar.SECOND, 2000); String token = JWT.create() // Header (the default data is used, so the map has no value. This line of code can also be omitted) .withHeader(map) // Payload .withClaim("userId", 666) .withClaim("username", "zzc") // Expiration time .withExpiresAt(expireTime.getTime()) // Signature .sign(Algorithm.HMAC256(SECRET)); System.out.println(token); } @Test public void verifyToken() { JWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC256(SECRET)).build(); String token = ""; DecodedJWT decodedJWT = jwtVerifier.verify(token); // Get the information in the token System.out.println(decodedJWT.getClaim("userId").asInt()); System.out.println(decodedJWT.getClaim("username").asString()); } }
explain:
- createToken(): method of generating token. As the second part of token, Payload can store information. So here, I put userId and username into the token. Later, you can obtain the corresponding value according to the token.
- verifyToken(): the method to verify the token. DecodedJWT decodedJWT = jwtVerifier.verify(token); If the token verification fails, this line of code will throw an exception; Otherwise, the execution will be successful and continue. Then, you can get the information decodedjwt in the token getClaim("username"). asString().
3. Spring boot integrates JWT
[development environment]
- IDEA-2020.2
- SpringBoot-2.5.5
- MAVEN-3.5.3
- Mybatis
- Mysql
[project structure chart]
1. Introduce dependency
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!--jwt--> <dependency> <groupId>com.auth0</groupId> <artifactId>java-jwt</artifactId> <version>3.4.0</version> </dependency> <!--mybatis--> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>2.0.0</version> </dependency> <!--mysql--> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>5.1.38</version> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> </dependency>
2. Add application YML configuration
spring: datasource: url: jdbc:mysql://localhost:3306/zzc?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8 password: root username: root driver-class-name: com.mysql.jdbc.Driver mybatis: type-aliases-package: com.zzc.entity # Entity class alias mapper-locations: classpath:mapper/*.xml # mapper configuration file (required) configuration: map-underscore-to-camel-case: true # Hump naming
3. Add JWT tool class
Abstract the token generation method and token verification method into a tool class:
public class JwtUtil { private static final String SECRET = "1qazXSW2"; // Generate Token public static String createToken(Map<String, String> paramMap) { Map<String, Object> headMap = new HashMap<>(); Calendar expireTime = Calendar.getInstance(); // The default expiration time is 7 days expireTime.add(Calendar.DATE, 7); JWTCreator.Builder builder = JWT.create(); // Header (the default data is used, so the map has no value and can be omitted) builder.withHeader(headMap); // Payload paramMap.forEach((key, value) -> { builder.withClaim(key, value); }); // Expiration time String token = builder.withExpiresAt(expireTime.getTime()) // Signature .sign(Algorithm.HMAC256(SECRET)); return token; } // Verify the legitimacy of token public static void verify(String token) { JWT.require(Algorithm.HMAC256(SECRET)).build().verify(token); } // Get token information public static DecodedJWT getTokenInfo(String token) { return JWT.require(Algorithm.HMAC256(SECRET)).build().verify(token); } }
4. Add Controller class
There are two interfaces in this Controller class:
@Slf4j @RestController @RequestMapping("/user") public class UserController { @Autowired private UserService userService; @GetMapping("/login") public Map<String, Object> login(User user) { log.info("User name:[{}]", user.getName()); log.info("Password:[{}]", user.getPwd()); Map<String, Object> map = new HashMap<>(); try { User u = userService.login(user); // Generate token Map<String, String> payload = new HashMap<>(); payload.put("id", u.getId()); payload.put("name", u.getName()); String token = JwtUtil.createToken(payload); map.put("status", true); map.put("msg", "Authentication successful"); map.put("token", token); } catch (Exception e) { map.put("status", false); map.put("msg", e.getMessage()); } return map; } @PostMapping("/testToken") public Map<String, Object> testToken(String token) { log.info("Current login token: [{}]", token); Map<String, Object> map = new HashMap<>(); try { JwtUtil.verify(token); map.put("status", true); map.put("msg", "Request succeeded"); return map; } catch (SignatureVerificationException e) { log.error("[Request failed]:{}", e.getMessage()); map.put("msg", "Invalid signature"); } catch (TokenExpiredException e) { log.error("[Request failed]:{}", e.getMessage()); map.put("msg", "token be overdue"); } catch (AlgorithmMismatchException e) { log.error("[Request failed]:{}", e.getMessage()); map.put("msg", "token Inconsistent algorithm"); } catch (Exception e) { log.error("[Request failed]:{}", e.getMessage()); map.put("msg", "token invalid"); } map.put("status", false); return map; } }
explain:
- login(): after the user logs in successfully, a token is generated through JWT. Then, the id and name of the user are stored in the token, and then the token is directly returned to the front end
- testToken(): when requesting this interface, the request header needs to carry the token jwtutil verify(token);, Otherwise, an error will be reported.
5. Add UserServiceImpl class
UserService interface omitted
@Service public class UserServiceImpl implements UserService { @Autowired private UserMapper userMapper; @Override public User login(User user) { User u = userMapper.login(user); if (null != u) { return u; } throw new RuntimeException("Authentication failed"); } }
6. Add UserMapper interface
public interface UserMapper { User login(User user); }
The corresponding usermapper XML file:
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="com.zzc.mapper.UserMapper"> <select id="login" parameterType="User" resultType="User"> SELECT id,name,pwd FROM TAB_USER WHERE 1=1 AND name = #{name} AND pwd = #{pwd} </select> </mapper>
Query user information through user name and password.
7. Run code
7-1. Call login interface
After successful authentication, a token is returned. The front end can save the token and carry it in the request header.
7-2. Call other interfaces
If the request does not carry a token, the request will fail "token is invalid".
If a token is carried in the request, the request will succeed.
8. Optimize code
Do you usually need to write one or more interfaces in the background to verify the redundancy of each project?
Obviously, not so!
At this time, we thought of the Interceptor. As follows:
8-1. Add an interceptor
@Slf4j public class JwtInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { Map<String, Object> map = new HashMap<>(); String token = request.getHeader("token"); try { JwtUtil.verify(token); // Release return true; } catch (SignatureVerificationException e) { log.error("[Request failed]:{}", e.getMessage()); map.put("msg", "Invalid signature"); } catch (TokenExpiredException e) { log.error("[Request failed]:{}", e.getMessage()); map.put("msg", "token be overdue"); } catch (AlgorithmMismatchException e) { log.error("[Request failed]:{}", e.getMessage()); map.put("msg", "token Inconsistent algorithm"); } catch (Exception e) { log.error("[Request failed]:{}", e.getMessage()); map.put("msg", "token invalid"); } // Return the error message to the foreground map.put("status", false); // Convert Map to json string String errorResult = new ObjectMapper().writeValueAsString(map); response.setContentType("application/json;charset=UTF-8"); response.getWriter().println(errorResult); return false; } }
If the token verification fails, the failure information will be returned to the foreground.
8-2. Add an interceptor configuration class
@Configuration public class InterceptorConfig implements WebMvcConfigurer { @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new JwtInterceptor()) .addPathPatterns("/user/testToken2") .excludePathPatterns("/user/login"); } }
explain:
- The above configuration is used to intercept the url of: / user/testToken2/ user/login does not intercept.
Well, SpringBoot + JWT is here.