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.
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.
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.
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.
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.
