Choosing Between JWT and Session: A Practical Guide to Secure Authentication in Java

This article compares JWT and session authentication, explains their differences, advantages, and drawbacks, and provides a complete Java implementation with Redis integration, token generation, validation, renewal, and related interceptor configuration for a robust backend authentication system.

Java High-Performance Architecture
Java High-Performance Architecture
Java High-Performance Architecture
Choosing Between JWT and Session: A Practical Guide to Secure Authentication in Java

Technical Selection

When implementing authentication, the two most common approaches are JWT and session. The main difference lies in where the user state is stored: session data is kept on the server , while JWT is stored on the client .

Difference

Session stores user state on the server side; JWT stores it on the client side.

Authentication Flow

Session-based authentication flow

User submits username and password; the server validates them and creates a session stored in the database.

The server generates a sessionId and places a cookie containing this ID in the user's browser. Subsequent requests include this cookie.

The server reads the cookie, extracts the sessionId, and checks the database to determine if the request is valid.

JWT-based authentication flow

User submits username and password; the server validates them 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 include this token.

The server retrieves the token, looks it up in the database, and verifies its validity.

Advantages and Disadvantages

JWT is stored on the client, so in a distributed environment no extra work is needed to share state. Session data resides on the server, requiring multi‑node data sharing.

Session usually relies on cookies, which browsers must support; therefore session authentication is not suitable for mobile clients.

Security

The JWT payload is Base64‑encoded, so sensitive data should not be stored in a JWT . Session information lives on the server, making it relatively more secure.

Performance

After encoding, a JWT can be very long; cookies have a size limit of about 4 KB, so JWTs are typically stored in local storage. Each HTTP request carries the JWT in the header, which can be larger than a simple session ID, increasing overhead.

One‑time Nature

JWTs are immutable; to modify their content a new JWT must be issued. Once issued, a JWT remains valid until its expiration and cannot be revoked early without additional mechanisms such as Redis.

Revocation: a JWT cannot be invalidated before expiry; a common solution is to combine it with Redis.

Renewal: traditional session renewal refreshes the expiration time on activity. For JWTs, a new token must be issued—either on every request (which is heavy) or by resetting the Redis expiration for the token.

Choosing JWT or Session

The author votes for JWT because it avoids the extra work required for multi‑node session sharing in distributed systems. Although JWT has drawbacks, they can be mitigated with Redis for revocation and renewal, making JWT the preferred choice for the project.

Implementation

JWT Dependency

<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)
     */
    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 correctness
     */
    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 Utility Class

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

Logout Function

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

Update Password Function

public String updatePassword(UpdatePasswordUserVO updatePasswordUserVO) {
    // 1. Update password in 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. 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;
}

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 token needs renewal
    if (redisService.getExpireTime(userTokenDTO.getId()) < 1 * 60 * 30) { // less than 30 minutes
        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();
    }
}

Additional notes: administrators have delete permissions (not implemented in the demo), and passwords are transmitted encrypted in a real project.

JavaRedisAuthenticationSpringBootJWTSession
Java High-Performance Architecture
Written by

Java High-Performance Architecture

Sharing Java development articles and resources, including SSM architecture and the Spring ecosystem (Spring Boot, Spring Cloud, MyBatis, Dubbo, Docker), Zookeeper, Redis, architecture design, microservices, message queues, Git, etc.

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.