-
Notifications
You must be signed in to change notification settings - Fork 1
[Feat] 로그아웃 API 구현 및 프론트 연동 #187
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
bd91c41
2963b66
ec30810
896cd50
3a212bc
6f04b25
66c3224
2260f4b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,25 @@ | ||
| package com.opensource.docgrid.domain.auth.jwt; | ||
|
|
||
| import java.time.Duration; | ||
|
|
||
| import org.springframework.data.redis.core.StringRedisTemplate; | ||
| import org.springframework.stereotype.Component; | ||
|
|
||
| import lombok.RequiredArgsConstructor; | ||
|
|
||
| @Component | ||
| @RequiredArgsConstructor | ||
| public class TokenBlacklistService { | ||
|
|
||
| private static final String KEY_PREFIX = "auth:blacklist:"; | ||
|
|
||
| private final StringRedisTemplate redisTemplate; | ||
|
|
||
| public void blacklist(String jti, long ttlSeconds) { | ||
| redisTemplate.opsForValue().set(KEY_PREFIX + jti, "1", Duration.ofSeconds(ttlSeconds)); | ||
| } | ||
|
|
||
| public boolean isBlacklisted(String jti) { | ||
| return Boolean.TRUE.equals(redisTemplate.hasKey(KEY_PREFIX + jti)); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,5 +1,7 @@ | ||
| package com.opensource.docgrid.domain.auth.service.command; | ||
|
|
||
| import java.time.Duration; | ||
| import java.time.Instant; | ||
| import java.time.LocalDateTime; | ||
| import java.util.List; | ||
|
|
||
|
|
@@ -12,6 +14,7 @@ | |
| import com.opensource.docgrid.domain.auth.dto.response.LoginResponse; | ||
| import com.opensource.docgrid.domain.auth.dto.response.SignupResponse; | ||
| import com.opensource.docgrid.domain.auth.jwt.JwtProvider; | ||
| import com.opensource.docgrid.domain.auth.jwt.TokenBlacklistService; | ||
| import com.opensource.docgrid.domain.user.entity.Department; | ||
| import com.opensource.docgrid.domain.user.entity.Role; | ||
| import com.opensource.docgrid.domain.user.entity.User; | ||
|
|
@@ -25,6 +28,7 @@ | |
| import com.opensource.docgrid.global.exception.DocGridException; | ||
| import com.opensource.docgrid.global.exception.ErrorCode; | ||
|
|
||
| import io.jsonwebtoken.Claims; | ||
| import lombok.RequiredArgsConstructor; | ||
| import lombok.extern.slf4j.Slf4j; | ||
|
|
||
|
|
@@ -40,6 +44,7 @@ public class AuthCommandService { | |
| private final UserRoleRepository userRoleRepository; | ||
| private final PasswordEncoder passwordEncoder; | ||
| private final JwtProvider jwtProvider; | ||
| private final TokenBlacklistService tokenBlacklistService; | ||
|
|
||
| public SignupResponse signup(SignupRequest request) { | ||
| if (userRepository.existsByEmail(request.email())) { | ||
|
|
@@ -97,4 +102,22 @@ public LoginResponse login(LoginRequest request) { | |
|
|
||
| return LoginResponse.of(token, jwtProvider.getExpirationSeconds(), user.getId(), user.getEmail(), roles); | ||
| } | ||
|
|
||
| public void logout(String token) { | ||
| Claims claims = jwtProvider.getClaimsIfValid(token); | ||
| if (claims == null) { | ||
| return; | ||
| } | ||
|
|
||
| String jti = claims.get("jti", String.class); | ||
| if (jti == null) { | ||
| return; | ||
| } | ||
|
|
||
| long remainingMillis = Duration.between(Instant.now(), claims.getExpiration().toInstant()).toMillis(); | ||
| if (remainingMillis > 0) { | ||
| long remainingSeconds = (remainingMillis + 999) / 1000; | ||
| tokenBlacklistService.blacklist(jti, remainingSeconds); | ||
|
Comment on lines
+112
to
+120
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win
배포 전에 발급된 JWT에는 기존 토큰을 즉시 거부하는 정책을 filter에 추가하거나, 원본 토큰의 안정적인 hash를 legacy blacklist key로 사용하세요.
📍 Affects 3 files
🤖 Prompt for AI Agents |
||
| } | ||
| } | ||
|
Comment on lines
+106
to
+122
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win 로그아웃 처리와 관련 테스트의 순차 실행 단계에 1., 2., 3. 형식의 주석을 추가해 claims 검증, TTL 계산, 블랙리스트 저장과 HTTP 검증의 순서를 명확히 해 주세요. 📍 Affects 2 files
🤖 Prompt for AI AgentsSource: Coding guidelines |
||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,64 @@ | ||
| package com.opensource.docgrid.domain.auth.integration; | ||
|
|
||
| import static org.assertj.core.api.Assertions.assertThat; | ||
|
|
||
| import java.util.UUID; | ||
|
|
||
| import org.junit.jupiter.api.DisplayName; | ||
| import org.junit.jupiter.api.Tag; | ||
| import org.junit.jupiter.api.Test; | ||
| import org.springframework.beans.factory.annotation.Autowired; | ||
| import org.springframework.boot.test.context.SpringBootTest; | ||
| import org.springframework.test.context.ActiveProfiles; | ||
|
|
||
| import com.opensource.docgrid.domain.auth.dto.request.LoginRequest; | ||
| import com.opensource.docgrid.domain.auth.dto.request.SignupRequest; | ||
| import com.opensource.docgrid.domain.auth.dto.response.LoginResponse; | ||
| import com.opensource.docgrid.domain.auth.jwt.JwtProvider; | ||
| import com.opensource.docgrid.domain.auth.jwt.TokenBlacklistService; | ||
| import com.opensource.docgrid.domain.auth.service.command.AuthCommandService; | ||
| import com.opensource.docgrid.domain.user.repository.DepartmentRepository; | ||
|
|
||
| import io.jsonwebtoken.Claims; | ||
|
|
||
| /** | ||
| * 실제 PostgreSQL·Redis에서 로그인한 토큰이 로그아웃 이후 블랙리스트에 등록되어 | ||
| * 더 이상 인증에 쓰일 수 없는 상태가 되는지 검증한다. | ||
| */ | ||
| @Tag("integration") | ||
| @ActiveProfiles("test") | ||
| @SpringBootTest | ||
| @DisplayName("로그아웃 PostgreSQL·Redis 통합 테스트") | ||
| class AuthLogoutIntegrationTest { | ||
|
|
||
| @Autowired | ||
| private AuthCommandService authCommandService; | ||
|
|
||
| @Autowired | ||
| private TokenBlacklistService tokenBlacklistService; | ||
|
|
||
| @Autowired | ||
| private JwtProvider jwtProvider; | ||
|
|
||
| @Autowired | ||
| private DepartmentRepository departmentRepository; | ||
|
|
||
| @Test | ||
| @DisplayName("로그인 후 로그아웃하면 발급된 토큰의 jti가 블랙리스트에 등록된다") | ||
| void logout_blacklistsIssuedToken() { | ||
| Long departmentId = departmentRepository.findAll().get(0).getId(); | ||
| String email = "logout-integration-" + UUID.randomUUID() + "@test.com"; | ||
| authCommandService.signup(new SignupRequest(email, "password1234", "로그아웃통합테스트", departmentId)); | ||
|
|
||
| LoginResponse loginResponse = authCommandService.login(new LoginRequest(email, "password1234")); | ||
| String token = loginResponse.accessToken(); | ||
| Claims claims = jwtProvider.getClaimsIfValid(token); | ||
| String jti = claims.get("jti", String.class); | ||
|
|
||
| assertThat(tokenBlacklistService.isBlacklisted(jti)).isFalse(); | ||
|
|
||
| authCommandService.logout(token); | ||
|
|
||
| assertThat(tokenBlacklistService.isBlacklisted(jti)).isTrue(); | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
새 인증 관련 클래스의 클래스 수준 문서화에 각 클래스의 책임과 경계를 명시해 주세요. 서비스는 Redis 저장·조회만 담당하고 인증 정책과 장애 처리는 호출자에 있으며, 테스트는 모킹 범위와 검증 책임을 설명해야 합니다.
📍 Affects 2 files
backend/src/main/java/com/opensource/docgrid/domain/auth/jwt/TokenBlacklistService.java#L10-L16(this comment)backend/src/test/java/com/opensource/docgrid/domain/auth/jwt/TokenBlacklistServiceTest.java#L18-L20🤖 Prompt for AI Agents
Source: Coding guidelines