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