JWT implements login authentication + Token automatic renewal scheme

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.

Certification process

session based authentication process

JWT based certification process

Advantages and disadvantages

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

performance

After coding, JWT will be very long. The limited size of cookies is usually 4k, and cookies may not fit, so JWT is generally placed in local storage. And every HTTP request of the user in the system will carry the JWT in the Header. The Header of the HTTP request may be larger than the Body. The sessionId is only a short string, so the HTTP request using JWT is much more expensive than using session

disposable

Statelessness is the characteristic of JWT, but it also leads to this problem. JWT is disposable. If you want to modify the contents, you must sign and issue a new JWT

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);
    }
}

explain:

Redis tool class

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

Business realization

Login function

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;
}

explain:

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();
    }
}

Tags: Operation & Maintenance server

Posted by sticks464 on Tue, 02 Aug 2022 01:50:45 +0930