Choosing JWT vs Session: A Practical Guide to Secure Backend Authentication

This article compares JWT and session authentication, explains their differences, security and performance trade‑offs, and provides a complete Java implementation with Redis integration, guiding developers on selecting and implementing the most suitable authentication method for distributed backend systems.

MaGe Linux Operations
MaGe Linux Operations
MaGe Linux Operations
Choosing JWT vs Session: A Practical Guide to Secure Backend Authentication

Recently I was responsible for the user management module, which involves encryption and authentication. Encryption was introduced earlier. Today I will discuss the technical selection and implementation of authentication. The technology is not difficult, but for a beginner it is a good exercise.

Technical Selection

To implement authentication, one often thinks of JWT or session. What are the differences, pros and cons, and which to pick?

Differences

The main difference is where the user state is stored: session stores it on the server, JWT stores it on the client.

Authentication Process

Session‑based authentication flow

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

The server generates a sessionId and places a cookie containing this sessionId in the browser; subsequent requests include this cookie.

The server retrieves the cookie, looks up the sessionId in the database to verify request validity.

JWT‑based authentication flow

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

The frontend stores the token in a cookie or local storage; subsequent requests include this token.

The server retrieves the token and checks its validity in the database.

Advantages and Disadvantages

JWT stored client‑side works well in distributed environments without extra work, while session requires server‑side data sharing.

Session typically relies on cookies, which may not be supported on mobile clients.

Security

JWT payload is only base64‑encoded, so sensitive data should not be stored; session data resides on the server and is more secure.

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

Performance

JWT can become large; cookies have a 4KB limit, so JWT is usually stored in local storage. Each HTTP request carries the JWT in the header, increasing overhead compared to a short sessionId.

One‑time Use

JWT is stateless, making it one‑time; modifying its content requires issuing a new JWT.

Cannot revoke a JWT before expiration; a common workaround is to combine with Redis.

Renewal: traditional cookie renewal extends session expiration; for JWT you must issue a new token, either on each request (inefficient) or by resetting expiration in Redis.

Choosing JWT or Session

I vote for JWT. Although it has drawbacks, it avoids the need for multi‑machine session sharing in distributed systems. Its one‑time nature can be mitigated with Redis, so I chose JWT for authentication in the project.

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

Notes:

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

UserTokenDTO does not contain sensitive fields such as password.

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

Notes: validate credentials, generate token, store token 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 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;
}

When the password is changed, a new token is generated and stored in Redis so the frontend can update its stored token without requiring a re‑login.

Other Notes

In real projects, admin users have permission to delete users, which also involves token handling (omitted in this demo).

Password transmission is 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. Refresh token if 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;
}

The interceptor validates the token's existence and consistency, and refreshes its expiration when less than 30 minutes remain.

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

redisSecurityAuthenticationJWTSession
MaGe Linux Operations
Written by

MaGe Linux Operations

Founded in 2009, MaGe Education is a top Chinese high‑end IT training brand. Its graduates earn 12K+ RMB salaries, and the school has trained tens of thousands of students. It offers high‑pay courses in Linux cloud operations, Python full‑stack, automation, data analysis, AI, and Go high‑concurrency architecture. Thanks to quality courses and a solid reputation, it has talent partnerships with numerous internet firms.

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.