Information Security 13 min read

Technical Selection and Implementation of Authentication: JWT vs Session in a Java Backend

This article compares JWT and session authentication, discusses their differences, advantages, security and performance considerations, and provides a complete Java implementation with Redis integration, including token generation, validation, renewal, and interceptor configuration for secure backend services.

Architect's Guide
Architect's Guide
Architect's Guide
Technical Selection and Implementation of Authentication: JWT vs Session in a Java Backend

Technical Selection

When implementing authentication, the two common approaches are JWT and session. The article examines their differences, advantages, disadvantages, and suitability for distributed environments.

Difference

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 the request.

JWT-based flow

User submits credentials; server validates and creates a token stored 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 database.

Pros and Cons

JWT is stateless and works well in distributed systems without extra session sharing, but it can be large, is sent in every request, and cannot be revoked before expiration without additional mechanisms. Session is more secure because data resides on the server, but it requires server‑side storage and cookie support, limiting mobile use.

Security

JWT payload is only base64‑encoded, so sensitive data should not be stored inside it. Session data stays on the server, offering better protection.

Performance

JWTs are typically larger than a session ID, increasing request header size. Session IDs are short, resulting in lower overhead.

One‑time Use & Revocation

JWTs are immutable; changing their content requires issuing a new token. Revocation before expiration is not possible unless a Redis blacklist is used.

Renewal

Both JWT and session can be renewed: sessions by extending the timeout on activity, JWTs by issuing a new token either on each request or when the Redis‑managed expiration is near.

Choice

The author prefers JWT for its simplicity in distributed environments, mitigating its drawbacks with Redis for expiration management.

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

    public static String generateToken(UserTokenDTO userTokenDTO) {
        try {
            Algorithm algorithm = Algorithm.HMAC256(TOKEN_SECRET);
            Map
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;
        }
    }

    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 Implementation

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

Login Function

public String login(LoginUserVO loginUserVO) {
    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);
    }
    UserTokenDTO userTokenDTO = new UserTokenDTO();
    PropertiesUtil.copyProperties(userTokenDTO, loginUserVO);
    userTokenDTO.setId(userPO.getId());
    userTokenDTO.setGmtCreate(System.currentTimeMillis());
    String token = JWTUtil.generateToken(userTokenDTO);
    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) {
    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);
    }
    UserTokenDTO userTokenDTO = UserTokenDTO.builder()
        .id(updatePasswordUserVO.getId())
        .username(user.getUsername())
        .gmtCreate(System.currentTimeMillis())
        .build();
    String token = JWTUtil.generateToken(userTokenDTO);
    redisService.set(user.getId(), token);
    return token;
}

Authentication Interceptor

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);
    if (redisService.get(userTokenDTO.getId()) == null ||
        !redisService.get(userTokenDTO.getId()).equals(token)) {
        return false;
    }
    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 JWT, combined with Redis for expiration management, provides a practical authentication solution for modern distributed Java backend systems.

backendJavaRedisAuthenticationJWTsession
Architect's Guide
Written by

Architect's Guide

Dedicated to sharing programmer-architect skills—Java backend, system, microservice, and distributed architectures—to help you become a senior architect.

0 followers
Reader feedback

How this landed with the community

login 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.