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.
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.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
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.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
