Choosing Between JWT and Session for Authentication: Technical Selection and Implementation

This article compares JWT and session authentication, discusses their differences, security, performance, and lifecycle considerations, and provides a complete Java implementation—including dependency configuration, token utilities, Redis integration, login/logout flows, password updates, and request interception—guiding developers to select the most suitable approach for their projects.

Architect
Architect
Architect
Choosing Between JWT and Session for Authentication: Technical Selection and Implementation

In this technical article, the author examines the choice between JSON Web Token (JWT) and traditional session-based authentication, outlining their fundamental differences, advantages, disadvantages, security implications, performance characteristics, and lifecycle management.

Technical Selection

Differences

Session stores user state on the server, while JWT stores it on the client.

Authentication Process

Session-based flow

User submits credentials; server validates and creates a session stored in the database.

Server returns a sessionId cookie to the client.

Subsequent requests include the cookie; server looks up the session to verify authenticity.

JWT-based flow

User submits credentials; server validates and creates a JWT token saved in the database.

Client stores the token in a cookie or local storage and sends it with each request.

Server verifies the token against the stored record.

Pros and Cons

JWT is stateless and works well in distributed environments without extra session sharing, but it cannot be used on platforms lacking cookie support (e.g., some mobile clients). Session requires server-side storage and coordination across nodes, increasing complexity in distributed setups.

Security

JWT payloads are only Base64‑encoded, so sensitive data should not be placed inside; session data resides on the server, offering better protection.

Performance

JWTs can become large, exceeding typical 4 KB cookie limits, and are sent in HTTP headers on every request, increasing bandwidth usage. Session IDs are short, resulting in lower overhead.

One‑time Use and Revocation

JWTs are immutable; to change their content a new token must be issued. Once issued, a JWT remains valid until its expiration, making mid‑life revocation difficult without auxiliary mechanisms such as Redis blacklists.

Renewal

Both approaches can refresh validity: sessions extend on activity, while JWTs require issuing a new token—either on every request (inefficient) or by updating an expiration entry in Redis.

Choosing JWT or Session

The author prefers JWT for its simplicity in distributed systems, acknowledging its drawbacks and proposing Redis‑based token expiration as a mitigation strategy.

Implementation

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 static final String TOKEN_SECRET = "123456";

    /** Generate token with custom expiration (ms) */
    public static String generateToken(UserTokenDTO userTokenDTO) {
        try {
            Algorithm algorithm = Algorithm.HMAC256(TOKEN_SECRET);
            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 and extract payload */
    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);
    }
}

Redis Service Wrapper

public final class RedisServiceImpl implements RedisService {
    private final Long DURATION = 1 * 24 * 60 * 60 * 1000L; // 1 day
    @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);
    }
}

Business Logic

Login

public String login(LoginUserVO loginUserVO) {
    // 1. Validate credentials
    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;
}

Logout

public boolean loginOut(String id) {
    boolean result = redisService.delete(id);
    if (!result) {
        throw new UserException(ErrorCodeEnum.TNP1001003);
    }
    return result;
}

Update Password

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

Interceptor for Token Validation and Renewal

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);
    // Validate token existence
    if (redisService.get(userTokenDTO.getId()) == null ||
        !redisService.get(userTokenDTO.getId()).equals(token)) {
        return false;
    }
    // Refresh if close to expiration (less than 30 minutes left)
    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;
}

Interceptor Configuration

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

The article concludes that, despite JWT's drawbacks, its stateless nature makes it a better fit for distributed systems, especially when combined with Redis for token revocation and renewal.

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.

JavaredisAuthenticationJWTSession
Architect
Written by

Architect

Professional architect sharing high‑quality architecture insights. Topics include high‑availability, high‑performance, high‑stability architectures, big data, machine learning, Java, system and distributed architecture, AI, and practical large‑scale architecture case studies. Open to ideas‑driven architects who enjoy sharing and learning.

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.