Technology selection
To realize the authentication function, it is easy to think of JWT or session, but what is the difference between the two? Respective advantages and disadvantages? Who should Pick? Lethal third company
difference
The main difference between session based and JWT based methods is the location where the user's state is saved. Session is saved on the server, while JWT is saved on the client.
session based authentication process
- The user enters the user name and password in the browser, and the server generates a session after passing the password verification and saves it to the database
- The server generates a sessionId for the user, and places the cookie with sessionId in the user's browser, and will access it with this cookie information in subsequent requests
- The server obtains the cookie, and finds the database by obtaining the sessionId in the cookie to determine whether the current request is valid
JWT based certification process
- The user enters the user name and password in the browser, and the server generates a token after passing the password verification and saves it to the database
- The front end obtains a token and stores it in a cookie or local storage. Subsequent requests will be accessed with this token information
- The server obtains the token value and determines whether the current token is valid by looking up the database
Advantages and disadvantages
- JWT is saved on the client side, so no extra work is required in the distributed environment. Because the session is stored on the server, it needs to realize multi machine data sharing in a distributed environment
- Session generally needs to be authenticated with cookies, so the browser needs to support cookies, so the mobile terminal cannot use the session authentication scheme
Security
- The payload of JWT uses base64 encoding, so sensitive data cannot be stored in JWT. The session information is stored on the server, which is relatively safer
If sensitive information is stored in JWT, it can be decoded, which is very unsafe
- Cannot abandon once a JWT is issued, it will always be valid before expiration and cannot be abandoned halfway. If you want to discard, a common treatment method is to combine redis.
- Renewal if JWT is used for session management, the traditional cookie renewal scheme is generally built-in. The session is valid for 30 minutes. If there is access within 30 minutes, the validity is refreshed to 30 minutes. In the same way, to change the effective time of JWT, it is necessary to issue a new JWT. The simplest way is to refresh the JWT every time, that is, every HTTP request returns a new JWT. This method is not only violent and inelegant, but also requires JWT encryption and decryption for every request, which will bring performance problems. Another method is to set the expiration time for each JWT separately in redis, and refresh the expiration time of the JWT every time you visit
Select JWT or session
I vote for JWT. JWT has many shortcomings, but it does not need to realize multi machine data sharing like session in a distributed environment. Although seesion's multi machine data sharing can be solved by viscous session, session sharing, session replication, persistent session, terracoa to realize seesion replication and other mature solutions.
But JWT doesn't need extra work. Isn't it fragrant to use JWT? And the one-time disadvantage of JWT can be made up in combination with redis. We should use JWT for authentication in actual projects.
Function realization
JWT required dependencies
<dependency> <groupId>com.auth0</groupId> <artifactId>java-jwt</artifactId> <version>3.10.3</version> </dependency>
JWT tool class
public class JWTUtil { private static final Logger logger = LoggerFactory.getLogger(JWTUtil.class); //Private key private static final String TOKEN_SECRET = "123456"; /** * Generate a token, and customize the expiration time in milliseconds * * @param userTokenDTO * @return */ public static String generateToken(UserTokenDTO userTokenDTO) { try { // Private key and encryption algorithm Algorithm algorithm = Algorithm.HMAC256(TOKEN_SECRET); // Set header information Map<String, Object> header = new HashMap<>(2); header.put("Type", "Jwt"); header.put("alg", "HS256"); return JWT.create() .withHeader(header) .withClaim("token", JSONObject.toJSONString(userTokenDTO)) //.withExpiresAt(date) .sign(algorithm); } catch (Exception e) { logger.error("generate token occur error, error is:{}", e); return null; } } /** * Verify whether the token is correct * * @param token * @return */ public static UserTokenDTO parseToken(String token) { Algorithm algorithm = Algorithm.HMAC256(TOKEN_SECRET); JWTVerifier verifier = JWT.require(algorithm).build(); DecodedJWT jwt = verifier.verify(token); String tokenInfo = jwt.getClaim("token").asString(); return JSON.parseObject(tokenInfo, UserTokenDTO.class); } }
-
UserTokenDTO does not contain sensitive information. For example, the password field will not appear in the token
public final class RedisServiceImpl implements RedisService { /** * Expiration duration */ private final Long DURATION = 1 * 24 * 60 * 60 * 1000L; @Resource private RedisTemplate redisTemplate; private ValueOperations<String, String> valueOperations; @PostConstruct public void init() { RedisSerializer redisSerializer = new StringRedisSerializer(); redisTemplate.setKeySerializer(redisSerializer); redisTemplate.setValueSerializer(redisSerializer); redisTemplate.setHashKeySerializer(redisSerializer); redisTemplate.setHashValueSerializer(redisSerializer); valueOperations = redisTemplate.opsForValue(); } @Override public void set(String key, String value) { valueOperations.set(key, value, DURATION, TimeUnit.MILLISECONDS); log.info("key={}, value is: {} into redis cache", key, value); } @Override public String get(String key) { String redisValue = valueOperations.get(key); log.info("get from redis, value is: {}", redisValue); return redisValue; } @Override public boolean delete(String key) { boolean result = redisTemplate.delete(key); log.info("delete from redis, key is: {}", key); return result; } @Override public Long getExpireTime(String key) { return valueOperations.getOperations().getExpire(key); } }
RedisTemplate simple encapsulation
public String login(LoginUserVO loginUserVO) { //1. Judge whether the user name and password are correct UserPO userPO = userMapper.getByUsername(loginUserVO.getUsername()); if (userPO == null) { throw new UserException(ErrorCodeEnum.TNP1001001); } if (!loginUserVO.getPassword().equals(userPO.getPassword())) { throw new UserException(ErrorCodeEnum.TNP1001002); } //2. The user name and password are correct, and the token is generated UserTokenDTO userTokenDTO = new UserTokenDTO(); PropertiesUtil.copyProperties(userTokenDTO, loginUserVO); userTokenDTO.setId(userPO.getId()); userTokenDTO.setGmtCreate(System.currentTimeMillis()); String token = JWTUtil.generateToken(userTokenDTO); //3. Deposit token to redis redisService.set(userPO.getId(), token); return token; }
- Judge whether the user name and password are correct
- If the user name and password are correct, a token is generated
- Save the generated token to redis
Logout function
public boolean loginOut(String id) { boolean result = redisService.delete(id); if (!redisService.delete(id)) { throw new UserException(ErrorCodeEnum.TNP1001003); } return result; }
Delete the corresponding key.
Update password function
public String updatePassword(UpdatePasswordUserVO updatePasswordUserVO) { //1. Change password UserPO userPO = UserPO.builder().password(updatePasswordUserVO.getPassword()) .id(updatePasswordUserVO.getId()) .build(); UserPO user = userMapper.getById(updatePasswordUserVO.getId()); if (user == null) { throw new UserException(ErrorCodeEnum.TNP1001001); } if (userMapper.updatePassword(userPO) != 1) { throw new UserException(ErrorCodeEnum.TNP1001005); } //2. Generate a new token UserTokenDTO userTokenDTO = UserTokenDTO.builder() .id(updatePasswordUserVO.getId()) .username(user.getUsername()) .gmtCreate(System.currentTimeMillis()).build(); String token = JWTUtil.generateToken(userTokenDTO); //3. Update token redisService.set(user.getId(), token); return token; }
Note: when updating the user password, you need to regenerate a new token, and return the new token to the front end. The front end updates the token stored in local storage, and updates the token stored in redis at the same time. In this way, users can avoid re login, and the user experience is not too bad.
Other instructions
- In the actual project, users are divided into ordinary users and administrator users. Only administrator users have the right to delete users. This function also involves token operation, but I'm too lazy to write the demo project
- In actual projects, password transmission is encrypted
Interceptor class
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String authToken = request.getHeader("Authorization"); String token = authToken.substring("Bearer".length() + 1).trim(); UserTokenDTO userTokenDTO = JWTUtil.parseToken(token); //1. Judge whether the request is valid if (redisService.get(userTokenDTO.getId()) == null || !redisService.get(userTokenDTO.getId()).equals(token)) { return false; } //2. Judge whether renewal is required if (redisService.getExpireTime(userTokenDTO.getId()) < 1 * 60 * 30) { redisService.set(userTokenDTO.getId(), token); log.error("update token info, id is:{}, user info is:{}", userTokenDTO.getId(), token); } return true; }
Note: the interceptor mainly does two things: one is to verify the token, and the other is to determine whether the token needs to be renewed token verification:
- Judge whether the token corresponding to the id does not exist. If it does not exist, the token expires
- If the token exists, compare whether the tokens are consistent to ensure that there is only one user operation at the same time
Automatic renewal of token: in order to operate redis infrequently, the expiration time is updated only when it is only 30 minutes from the expiration time
Interceptor configuration class
@Configuration public class InterceptorConfig implements WebMvcConfigurer { @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(authenticateInterceptor()) .excludePathPatterns("/logout/**") .excludePathPatterns("/login/**") .addPathPatterns("/**"); } @Bean public AuthenticateInterceptor authenticateInterceptor() { return new AuthenticateInterceptor(); } }