한정판 상품을 판매하는 E-Commerce 서비스를 구현하면서, JWT 기반의 사용자 인증 기능을 구현하고, 보안을 강화하기 위해 적용한 방식들을 기록한다.


한정판 구매 상품 오픈이 임박할수록 동시 접속자가 급증할 것이고, 인증 요청도 많아질 것이라고 생각했다. 사용자 인증을 안정적으로 처리하면서도 보안성을 유지할 수 있는 전략이 필요했다.
기존에 고려했던 인증 방식은 크게 두 가지였다.
1. 세션 기반 인증
- 세션을 활용한 인증은 서버에서 사용자의 상태를 관리하기 때문에 보안성이 높다.
- 그러나, 트래픽이 증가할수록 DB의 부하가 증가하여 확장성이 떨어진다.
2. JWT(JSON Web Token) 기반 인증
- JWT는 서버에서 상태를 관리하지 않고도 사용자 인증이 가능하며, 각 서비스에서 토큰만 검증하면 되기 때문에 MSA 환경에서 효율적이라고 생각했다.
- 그러나, 클라이언트에서 JWT를 Local Storage나 Session Storage에 저장하여 직접 관리해야 하기 때문에, 보안 및 유지보수 측면에서 부담이 생길 수 있다. XSS 공격이나 CSRF 공격 위험이 있기 때문에 기본적으로 보안성이 낮기 때문이다.
나는 세션 기반 보다는 JWT 기반 인증이 적합하다고 생각했다. 그러나, 보안성을 추가로 강화해야 한다고 생각했다.
그래서 토큰 탈취 가능성을 최소화하는 방향으로 JWT 기반 인증을 설계했다.
일반적으로 JWT 기반 인증은 클라이언트가 직접 로컬 스토리지나 세션 스토리지에 저장하는 방식이 자주 사용된다. 그러나 이 방식은 XSS 공격에 취약하며, 토큰이 탈취될 가능성이 있다.
이를 해결하기 위해 Access Token을 HTTP-Only Cookie에 저장하는 방식을 사용했다. httponly(true) 태그를 사용하면 클라이언트 측 JavaScript에서 접근할 수 없도록 설정하여 XSS 공격을 방지할 수 있다.
또한, Access Token의 만료 시간을 10분으로 설정하여, 탈취 위험을 줄이고 짧은 주기로 재인증하도록 설정해 보안성을 높였다.
// Access 토큰 생성
public String generateAccessToken(String email) {
return Jwts.builder()
.setSubject(email)
.setIssuedAt(new Date())
.setExpiration(new Date(System.currentTimeMillis() + accessTokenExpirationTime))
.signWith(getSecretKey(), SignatureAlgorithm.HS512)
.compact();
}
// JWT를 쿠키에 저장하고 보안 설정 적용
private ResponseCookie createAccessTokenCookie(String accessToken) {
return ResponseCookie.from("accessToken", accessToken)
.httpOnly(true)
.secure(false)
.path("/")
.maxAge(jwtTokenProvider.getAccessTokenExpirationTime() / 1000)
.build();
}
HTTP-Only Cookie를 사용한 로그아웃 시 쿠키 만료 처리 방법
HTTP-Only Cookie를 사용하면 클라이언트 측에서 직접 쿠키를 조작할 수 없기 때문에, 사용자가 로그아웃할 때 쿠키를 어떻게 만료시킬 수 있는지 고민해보았고, 서버에서 만료된 Access Token을 담은 쿠키를 다시 전송하여 기존 쿠키를 덮어씌우는 방식으로 결정했다.
1. 클라이언트가 로그아웃 요청을 보내면, 서버에서 만료된 Access Token을 새로운 쿠키로 설정하여 덮어씌운다.
2. maxAge(0)을 설정하여 즉시 만료시킨다.
3. Refresh Token은 Redis에 저장되어 있기 때문에, Redis에서 해당 토큰을 바로 삭제하여 더 이상 새로운 Access Token을 발급받을 수 없도록 만들었다.
Access Token 의 만료 시간을 짧게 설정하여 보안성을 높였지만, 사용자가 매번 다시 인증하는 것은 불편하다. 따라서 Access Token 만료 시 Refresh Token 을 활용하여 Access Token을 자동적으로 재발급하도록 설계했다.
- Refresh Token의 유효기간을 7일로 설정하여, Access Token이 만료되더라도 로그인 상태를 계속 길게 유지할 수 있도록 했다.
- Refresh Token도 클라이언트가 직접 관리하는 것이 아니라, 보안을 위해 Redis에 저장했다.
- Refresh Token을 활용한 재발급 로직을 구현하여, Access Token이 만료된 경우 새로운 Access Token을 발급하는 기능을 추가했다.
// Refresh Token 생성
public String generateRefreshToken(String email) {
try {
String token = Jwts.builder()
.setSubject(email)
.setIssuedAt(new Date())
.setExpiration(new Date(System.currentTimeMillis() + refreshTokenExpirationTime))
.signWith(getSecretKey(), SignatureAlgorithm.HS512)
.compact();
return token;
} catch (Exception e) {
throw e;
}
}
// Refresh Token을 Redis에 저장
private void saveRefreshTokenToRedis(String refreshToken, User user) {
redisTemplate.opsForValue().set(
"refresh:" + user.getId(),
refreshToken,
jwtTokenProvider.getRefreshTokenExpirationTime(),
TimeUnit.MILLISECONDS
);
}
서명(Signature) 알고리즘은 JWT의 무결성과 관련이 있으므로, 보안성을 결정하는 요소 중 하나이다. 이 서명이 있어야 변조되지 않은 JWT라는 것을 증명할 수 있다.
주요 서명 알고리즘으로는 HS256, HS384, HS512가 있다. 그중 해시 길이가 가장 길고 Brute Force에 강한 HS512를 적용하여 보안성을 강화했다.
1. XSS 공격 방지
- Access Token을 HTTP-Only Cookie에 저장하여 클라이언트 측 스크립트에서 접근 불가능하도록 만들었기 때문에 XSS 공격으로 인한 토큰 탈취를 방지했다.
2. JWT 기반 인증의 보안성과 확장성 확보
- 세션 기반 인증보다 확장성이 뛰어난 JWT 인증 사용 + 보안성 강화하기 위해 추가 설정을 진행하여, 동시 접속자가 많아도 안정적인 인증 프로세스를 유지할 수 있게 되었다.
MSA 환경에서 효율적으로 운영될 수 있도록 확장성과 보안성을 모두 고려한 JWT 기반 인증을 적용했다.
- Access Token을 HTTP-Only Cookie에 저장하여 XSS 공격을 방지
- Refresh Token을 활용하여 유저의 인증 상태를 유지하면서도 탈취 위험을 줄였다.
향후 추가적으로 개선할 점 :
- 이번 프로젝트의 핵심 목표는 선착순 구매 시스템 구축이다. 따라서 현재는 사용자별 역할이 존재하지 않으므로 Authorization 기능을 별도로 구현하지 않았다.
- 앞으로 Admin 기능이 추가되거나 사용자별 접근 권한을 다르게 설정해야 할 경우 Spring Security + JWT 기반 Authorization 로직을 추가할 예정이다.