JWT vs Session: Pros, Cons, and How to Implement Secure Authentication
This article compares JWT and session-based authentication, detailing their differences, advantages, disadvantages, security considerations, performance impacts, and provides a complete Java implementation with token generation, Redis storage, login, logout, password update, and interceptor configuration.
In recent work I was responsible for the user management module, which involves encryption and authentication; encryption was covered in a previous article. This piece discusses the technical choices and implementation of authentication, a straightforward yet valuable exercise for a newcomer.
Technical Selection
When implementing authentication, JWT and session are the obvious options, but what are their differences, pros and cons, and which should be chosen?
Difference
The main difference between session-based and JWT-based approaches is where the user state is stored: session is stored on the server side , while JWT is stored on the client side .
Authentication Process
Session-based authentication flow
The user enters username and password in the browser; the server validates the password and creates a session stored in the database.
The server generates a sessionId and places a cookie containing the sessionId in the user's browser; subsequent requests carry this cookie.
The server retrieves the cookie, extracts the sessionId, looks it up in the database, and determines whether the request is valid.
JWT-based authentication flow
The user enters username and password; the server validates the password and generates a token stored in the database.
The front end receives the token and stores it in a cookie or local storage; subsequent requests include the token.
The server extracts the token, verifies it against the database, and determines its validity.
Advantages and Disadvantages
JWT is stored on the client, so in a distributed environment no extra work is needed; session requires multi‑node data sharing.
Session usually relies on cookies, which browsers must support, making it unsuitable for some mobile scenarios.
Security
The JWT payload is Base64‑encoded, so sensitive data should not be stored in JWT . Session data resides on the server, which is relatively more secure.
If sensitive information is stored in JWT, it can be decoded, posing a security risk.
Performance
After encoding, JWT can become very long; the typical cookie size limit is 4 KB, so JWT is usually stored in local storage. Each HTTP request carries the JWT in the header, making the request larger than a session‑based request where only a short sessionId is sent.
One‑time Nature
JWT is stateless, which means it is one‑time use; modifying its content requires issuing a new JWT.
Cannot be revoked: once issued, a JWT remains valid until expiration. Revocation can be handled by combining with Redis.
Renewal: traditional cookie renewal refreshes the session expiration on each access. For JWT, a new token must be issued, either on every request (inefficient) or by resetting its Redis expiration.
Choosing JWT or Session
I vote for JWT. Although JWT has drawbacks, it avoids the need for multi‑node session sharing in distributed systems. Session sharing can be achieved with sticky sessions, session replication, persistence, etc., but JWT works out‑of‑the‑box. Its one‑time limitation can be mitigated with Redis, so the project adopts JWT for authentication.
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";
/**
* 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;
@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 Implementation
Login
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
public boolean loginOut(String id) {
boolean result = redisService.delete(id);
if (!redisService.delete(id)) {
throw new UserException(ErrorCodeEnum.TNP1001003);
}
return result;
}Update Password
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) {
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();
}
}Feel free to point out any omissions or errors.
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.
Programmer DD
A tinkering programmer and author of "Spring Cloud Microservices in Action"
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.
