Choosing Between JWT and Session: Pros, Cons, and Implementation Guide

This article compares JWT and session authentication, outlines their differences, advantages, security considerations, performance impacts, and provides a complete Java implementation with Redis integration, helping developers decide the best approach for their projects.

Programmer DD
Programmer DD
Programmer DD
Choosing Between JWT and Session: Pros, Cons, and Implementation Guide
In recent work I was responsible for the user management module, which involves encryption and authentication. The encryption part was covered earlier; this article discusses the technology selection and implementation of the authentication feature.

Technical Selection

When implementing authentication, JWT and session are the two obvious choices. What are their differences, advantages, and disadvantages?

Difference

The main difference between session‑based and JWT‑based authentication is where the user state is stored: session is stored on the server side , while JWT is stored on the client side .

Authentication Process

Session‑based authentication process

User enters username and password in the browser; the server validates the password and creates a session stored in the database.

The server generates a sessionId and places a cookie containing the sessionId in the user's browser; subsequent requests carry this cookie.

The server retrieves the cookie, extracts the sessionId, looks it up in the database, and determines whether the request is valid.

JWT‑based authentication process

User enters username and password; the server validates and creates a token stored in the database.

The front end receives the token and stores it in a cookie or local storage; subsequent requests carry the token.

The server extracts the token, looks it up in the database, and checks its validity.

Advantages and Disadvantages

JWT is stored on the client, so in a distributed environment no extra work is needed; session is stored on the server, requiring data sharing across machines.

Session usually relies on cookies, which browsers must support, making it unsuitable for mobile clients.

Security

The JWT payload is Base64‑encoded, so sensitive data should not be stored in JWT . Session data resides on the server, which is relatively more secure.

If sensitive information is stored in JWT, it can be decoded and is unsafe.

Performance

After encoding, JWT can become very long; the typical cookie size limit is 4 KB, so JWT often cannot be placed in a cookie and is stored in local storage. Each HTTP request carries the JWT in the header, which may be larger than the request body, while a sessionId is a short string, making JWT requests heavier.

One‑time Use

JWT is stateless, which means it is one‑time use; to modify its content a new JWT must be issued.

JWT cannot be revoked before expiration; a common solution is to combine it with Redis.

Renewal: traditional cookie renewal refreshes the session expiration on each access. For JWT, the common approach is to issue a new token on each request or to store the token in Redis with an expiration time that is refreshed on access.

Choosing JWT or Session

I vote for JWT. Although JWT has many drawbacks, it does not require extra work for data sharing in distributed environments, unlike session which needs sticky sessions, session sharing, replication, persistence, or Terracotta. JWT’s one‑time drawback can be mitigated with Redis, so in practice I choose JWT for authentication.

Implementation

JWT Dependencies

<dependency>
    <groupId>com.auth0</groupId>
    <artifactId>java-jwt</artifactId>
    <version>3.10.3</version>
</dependency>

JWT Utility Class

public class JWTUtil {
    private static final Logger logger = LoggerFactory.getLogger(JWTUtil.class);
    // Private key
    private static final String TOKEN_SECRET = "123456";

    /**
     * Generate token with custom expiration (milliseconds)
     * @param userTokenDTO
     * @return token string
     */
    public static String generateToken(UserTokenDTO userTokenDTO) {
        try {
            // Private key and algorithm
            Algorithm algorithm = Algorithm.HMAC256(TOKEN_SECRET);
            // Header
            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))
                    .sign(algorithm);
        } catch (Exception e) {
            logger.error("generate token occur error, error is:{}", e);
            return null;
        }
    }

    /**
     * Verify token correctness
     * @param token
     * @return UserTokenDTO
     */
    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);
    }
}

Explanation:

The generated token does not contain an expiration time; token expiration is managed by Redis.

UserTokenDTO does not carry sensitive fields such as passwords.

Redis Utility Class

public final class RedisServiceImpl implements RedisService {
    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 wrapper.

Business Implementation

Login Function

public String login(LoginUserVO loginUserVO) {
    // 1. Validate username and password
    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. Generate token
    UserTokenDTO userTokenDTO = new UserTokenDTO();
    PropertiesUtil.copyProperties(userTokenDTO, loginUserVO);
    userTokenDTO.setId(userPO.getId());
    userTokenDTO.setGmtCreate(System.currentTimeMillis());
    String token = JWTUtil.generateToken(userTokenDTO);
    // 3. Store token in Redis
    redisService.set(userPO.getId(), token);
    return token;
}

Explanation: check credentials, generate JWT, store it in 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 Redis key.

Update Password Function

public String updatePassword(UpdatePasswordUserVO updatePasswordUserVO) {
    // 1. Update 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 new token
    UserTokenDTO userTokenDTO = UserTokenDTO.builder()
            .id(updatePasswordUserVO.getId())
            .username(user.getUsername())
            .gmtCreate(System.currentTimeMillis()).build();
    String token = JWTUtil.generateToken(userTokenDTO);
    // 3. Update token in Redis
    redisService.set(user.getId(), token);
    return token;
}

Explanation: after changing the password, a new token is generated and stored in Redis, allowing the front end to update its stored token without forcing the user to log in again.

Other Notes

In a real project, users are divided into normal and admin users; only admins can delete users (demo omitted).

Password transmission should be encrypted in production.

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. Validate request
    if (redisService.get(userTokenDTO.getId()) == null
            || !redisService.get(userTokenDTO.getId()).equals(token)) {
        return false;
    }
    // 2. Check if renewal is needed
    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;
}

Explanation: the interceptor validates the token and decides whether to renew it. Validation checks that the token exists in Redis and matches the stored value; renewal occurs when the remaining expiration is less than 30 minutes.

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

Final Note

If there are any omissions or issues, feel free to point them out.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

JavaredisSecurityAuthenticationJWTSession
Programmer DD
Written by

Programmer DD

A tinkering programmer and author of "Spring Cloud Microservices in Action"

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.