728x90
전체 아키텍처 개요
이 시스템은 다음과 같은 특징을 가진다.
- JWT 기반 토큰 인증 (`Access Token` + `Refresh Token`)
- 일반 로그인과 소셜 로그인 통합 지원
- 게스트 사용자 지원 및 회원 전환 시 데이터 이관
- 쿠키 기반 토큰 저장으로 XSS 공격 방어
- Redis 를 활용한 Refresh Token 관리
핵심 컴포넌트 구조
1. PrincipalMember - 통합 사용자 정보 객체
public class PrincipalMember implements UserDetails, OAuth2User {
private final Member member;
private final Map<String, Object> attributes;
// 일반 로그인용 생성자
public PrincipalMember(Member member) {
this.member = member;
this.attributes = Map.of();
}
// 소셜 로그인용 생성자
public PrincipalMember(Member member, Map<String, Object> attributes) {
this.member = member;
this.attributes = attributes;
}
}
핵심포인트
- `UserDetails` 와 `OAuth2User` 인터페이스를 동시에 구현
- 일반 로그인과 소셜 로그인을 하나의 객체로 통합 처리
- 생성자 오버로딩으로 각 로그인 방식에 맞는 초기화 지원
2. JWT 토큰 관리 시스템
: JwtProvider - 토큰 생성 및 관리
public class JwtProvider {
// 토큰 생성
public String createAccessToken(String name, String loginId, String role)
public String createRefreshToken(String loginId)
// 토큰 검증 및 파싱
public JwtPrincipal createMemberFromToken(String token)
public boolean isExpiredToken(String token)
// 쿠키 관리
public Cookie createCookie(TokenName name, String token)
public String extractTokenFromCookies(Cookie[] cookies, TokenName name)
}
보안 고려사항
- 쿠키에 `HttpOnly`, `Secure` 플래그 설정
- 토큰 만료 시간에 여유분(3분) 추가하여 쿠키와 토큰 만료 시점 동기화
3. 인증 필터 및 성공 핸들러
: JwtFilter - 요청별 인증 처리
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) {
String path = request.getRequestURI();
if (isPublicPath(path)) {
filterChain.doFilter(request, response);
return;
}
AuthStatus status = jwtAuthenticationService.authenticateStatus(request, response);
if (status == AuthStatus.SUCCESS) {
filterChain.doFilter(request, response);
return;
}
if (status == AuthStatus.NO_TOKEN) {
response.sendRedirect("/login");
return;
}
response.sendRedirect("/login?error=expired");
}
: CustomAuthenticationSuccessHandler - 로그인 성공 처리
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) {
PrincipalMember principalMember = (PrincipalMember) authentication.getPrincipal();
JwtPrincipal jwtPrincipal = jwtLoginSuccessProcessor.createJwtPrincipal(principalMember);
jwtLoginSuccessProcessor.reissueTokensAndAuthenticate(request, response, jwtPrincipal);
getRedirectStrategy().sendRedirect(request, response, "/");
}
토큰 갱신 매커니즘
Access Token과 Refresh Token 전략
- Access Token: 짧은 만료 시간 (보통 15분~1시간)
- Refresh Token: 긴 만료 시간 (보통 1주~1개월), `Redis` 에 저장
자동 토큰 갱신 플로우
private AuthStatus handleAccessToken(String accessToken, String refreshToken, HttpServletResponse response) {
if (jwtProvider.isExpiredToken(accessToken)) {
// Access Token이 만료된 경우
if (refreshToken != null && !jwtProvider.isExpiredToken(refreshToken)) {
// Refresh Token이 유효한 경우 새로운 Access Token 발급
if (authenticateWithRefreshToken(response, refreshToken)) {
return AuthStatus.SUCCESS;
}
}
return AuthStatus.TOKEN_EXPIRED;
}
// Access Token이 유효한 경우 바로 인증
if (authenticateWithAccessToken(accessToken)) {
return AuthStatus.SUCCESS;
}
return AuthStatus.INVALID_TOKEN;
}
장점
- 사용자는 토큰 만료를 인식하지 못함
- 보안성과 사용자 경험의 균형점 확보
게스트 사용자 시스템
게스트 → 회원 전환 시 데이터 이관
private void transferGuestDataIfExists(HttpServletRequest request, JwtPrincipal jwtPrincipal) {
String guestToken = jwtProvider.extractTokenFromCookies(request.getCookies(), TokenName.ACCESS_TOKEN);
if (guestToken != null) {
JwtPrincipal guest = jwtProvider.createMemberFromToken(guestToken);
if (guest != null && guest.getRole() == Role.ROLE_GUEST) {
// 게스트 데이터를 회원 계정으로 이관
guestService.transferGuestMemos(guest, jwtPrincipal.getUsername());
redisService.deleteRefreshToken(guest.getUsername());
}
}
}
핵심 아이디어
- 게스트로 생성한 데이터가 회원가입 후에도 유지됨
- 사용자 경험을 해치지 않음
Redis 를 활용한 토큰 관리
Refresh Token 저장 및 검증
// 토큰 저장
redisService.saveRefreshToken(username, refreshToken, expiry);
// 토큰 검증
String savedRefreshToken = redisService.getRefreshTokenByLoginId(loginId);
if (!refreshToken.equals(savedRefreshToken)) {
return false; // 토큰 불일치
}
// 토큰 삭제
redisService.deleteRefreshToken(guest.getUsername());
보안 효과
- 서버에서 `Refresh Token` 을 무효화할 수 있음
- 중복 로그인 방지 또는 허용 정책 구현 가능
- 로그아웃 시 즉시 토큰 무효화
로그아웃 처리
public void processSuccess(HttpServletRequest request, HttpServletResponse response) {
String accessToken = jwtProvider.extractTokenFromCookies(request.getCookies(), TokenName.ACCESS_TOKEN);
JwtPrincipal jwtPrincipal = jwtProvider.createMemberFromToken(accessToken);
// Redis에서 Refresh Token 삭제
redisService.deleteRefreshToken(jwtPrincipal.getUsername());
// 쿠키 만료 처리
response.addCookie(jwtProvider.expireCookie(TokenName.ACCESS_TOKEN));
response.addCookie(jwtProvider.expireCookie(TokenName.REFRESH_TOKEN));
}
728x90
'💠프로젝트 및 경험 > 프로젝트' 카테고리의 다른 글
[메모장 프로젝트] 트러블슈팅 모음 (0) | 2025.05.07 |
---|