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

This article compares JWT and session-based authentication, outlines their differences, security and performance trade‑offs, and demonstrates a Java backend implementation using JWT, Redis for token storage, and interceptor-based validation and renewal, providing complete code examples and practical guidance.

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

Technical Selection

When implementing authentication, the two common choices are JWT and session . The main difference lies in where the user state is stored: sessions keep state on the server, while JWT stores it on the client.

Authentication Flow

Session‑based flow

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

Server returns a sessionId in a cookie; subsequent requests carry this cookie.

Server reads the cookie, looks up the sessionId in the database, and verifies the request.

JWT‑based flow

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

Frontend stores the token in a cookie or localStorage; each request includes the token in the Authorization header.

Server verifies the token by checking the stored record.

Pros and Cons

JWT advantages: client‑side storage eliminates the need for distributed session sharing; suitable for micro‑service architectures. Drawbacks: token size can be large, it is stateless (cannot be revoked before expiration), and sensitive data must not be placed in the payload.

Session advantages: token lives on the server, making it more secure for sensitive data. Drawbacks: requires session sharing across instances (sticky sessions, replication, etc.) and depends on browser cookies, which are unavailable on many mobile clients.

Performance

JWT payloads are often larger than a simple sessionId, increasing request header size. Every request carries the full token, leading to higher bandwidth and processing overhead compared with a lightweight session identifier.

Renewal & Revocation

JWTs are immutable; to change their content a new token must be issued. Because they cannot be revoked before expiry, a common pattern is to store the token in Redis with an expiration time and refresh that expiration on each request when the remaining TTL falls below a threshold (e.g., 30 minutes).

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

    /** Parse token */
    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 (Simplified)

public final class RedisServiceImpl implements RedisService {
    private final Long DURATION = 1L * 24 * 60 * 60 * 1000; // 1 day
    @Resource private RedisTemplate<String, String> redisTemplate;
    private ValueOperations<String, String> valueOps;

    @PostConstruct
    public void init() {
        RedisSerializer<String> serializer = new StringRedisSerializer();
        redisTemplate.setKeySerializer(serializer);
        redisTemplate.setValueSerializer(serializer);
        redisTemplate.setHashKeySerializer(serializer);
        redisTemplate.setHashValueSerializer(serializer);
        valueOps = redisTemplate.opsForValue();
    }

    @Override
    public void set(String key, String value) {
        valueOps.set(key, value, DURATION, TimeUnit.MILLISECONDS);
        log.info("key={}, value is: {} into redis cache", key, value);
    }

    @Override
    public String get(String key) {
        String val = valueOps.get(key);
        log.info("get from redis, value is: {}", val);
        return val;
    }

    @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 valueOps.getOperations().getExpire(key);
    }
}

Business Logic

Login

public String login(LoginUserVO loginUserVO) {
    UserPO user = userMapper.getByUsername(loginUserVO.getUsername());
    if (user == null || !loginUserVO.getPassword().equals(user.getPassword())) {
        throw new UserException(ErrorCodeEnum.TNP1001001);
    }
    UserTokenDTO dto = new UserTokenDTO();
    PropertiesUtil.copyProperties(dto, loginUserVO);
    dto.setId(user.getId());
    dto.setGmtCreate(System.currentTimeMillis());
    String token = JWTUtil.generateToken(dto);
    redisService.set(user.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 vo) {
    UserPO user = userMapper.getById(vo.getId());
    if (user == null) {
        throw new UserException(ErrorCodeEnum.TNP1001001);
    }
    UserPO update = UserPO.builder().id(vo.getId()).password(vo.getPassword()).build();
    if (userMapper.updatePassword(update) != 1) {
        throw new UserException(ErrorCodeEnum.TNP1001005);
    }
    UserTokenDTO dto = UserTokenDTO.builder()
            .id(vo.getId())
            .username(user.getUsername())
            .gmtCreate(System.currentTimeMillis())
            .build();
    String token = JWTUtil.generateToken(dto);
    redisService.set(user.getId(), token);
    return token;
}

Interceptor for Token Validation & 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 dto = JWTUtil.parseToken(token);
    // Validate existence and consistency
    if (redisService.get(dto.getId()) == null || !redisService.get(dto.getId()).equals(token)) {
        return false;
    }
    // Refresh if TTL < 30 minutes
    if (redisService.getExpireTime(dto.getId()) < 30 * 60) {
        redisService.set(dto.getId(), token);
        log.error("update token info, id is:{}, user info is:{}", dto.getId(), token);
    }
    return true;
}

Interceptor Configuration

@Configuration
public class InterceptorConfig implements WebMvcConfigurer {
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(authenticateInterceptor())
                .excludePathPatterns("/logout/**", "/login/**")
                .addPathPatterns("/**");
    }

    @Bean
    public AuthenticateInterceptor authenticateInterceptor() {
        return new AuthenticateInterceptor();
    }
}

The article concludes with practical advice: store tokens without expiration in the JWT payload, manage TTL via Redis, avoid placing passwords in the token, and use the interceptor to enforce single‑session semantics and automatic 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.

JavaJWTSession
Top Architect
Written by

Top Architect

Top Architect focuses on sharing practical architecture knowledge, covering enterprise, system, website, large‑scale distributed, and high‑availability architectures, plus architecture adjustments using internet technologies. We welcome idea‑driven, sharing‑oriented architects to exchange and learn together.

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.