-
Notifications
You must be signed in to change notification settings - Fork 1
[Fix] 역할 회수 API 및 Redis 캐시 기반 권한 즉시 반영 #227
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
f390863
f89e60e
0fdc8cf
1d88bf0
6ea8e92
8068748
3887923
26a71da
030d351
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,75 @@ | ||
| package com.opensource.docgrid.domain.auth.jwt; | ||
|
|
||
| import java.time.Duration; | ||
| import java.util.Arrays; | ||
| import java.util.List; | ||
|
|
||
| import org.springframework.data.redis.core.StringRedisTemplate; | ||
| import org.springframework.stereotype.Component; | ||
|
|
||
| import com.opensource.docgrid.domain.user.repository.UserRoleRepository; | ||
|
|
||
| import lombok.RequiredArgsConstructor; | ||
| import lombok.extern.slf4j.Slf4j; | ||
|
|
||
| /** | ||
| * 인가(hasRole) 판단에 쓰는 사용자 role을 JWT가 아니라 DB에서 매 요청 조회한다. | ||
| * | ||
| * <p>JWT에 role을 박제하면 관리자가 role을 부여/회수해도 재로그인 전까지 반영되지 않는다. | ||
| * DB 조회 부하를 줄이기 위해 Redis에 짧은 TTL로 캐싱하고, role 변경 시 즉시 무효화한다. | ||
| * | ||
| * <p>이 서비스는 인증 필터(모든 요청)의 critical path에 있으므로, {@code TokenBlacklistService}와 | ||
| * 동일하게 Redis 장애 시 예외를 전파하지 않고 DB 조회로 폴백한다 — Redis가 죽었다고 전체 API가 | ||
| * 막히면 안 된다. | ||
| */ | ||
| @Slf4j | ||
| @Component | ||
| @RequiredArgsConstructor | ||
| public class RoleAuthorityService { | ||
|
|
||
| private static final String KEY_PREFIX = "auth:roles:"; | ||
| private static final Duration TTL = Duration.ofSeconds(30); | ||
|
|
||
| private final StringRedisTemplate redisTemplate; | ||
| private final UserRoleRepository userRoleRepository; | ||
|
|
||
| public List<String> getRoles(Long userId) { | ||
| // 1. 먼저 Redis 캐시를 확인한다 — 대부분의 요청은 여기서 끝나 DB 부하를 줄인다. | ||
| String cached = readCache(userId); | ||
| if (cached != null) { | ||
| return cached.isBlank() ? List.of() : Arrays.asList(cached.split(",")); | ||
| } | ||
|
|
||
| // 2. 캐시 미스면 DB에서 최신 role을 조회한다(source of truth). | ||
| List<String> roles = userRoleRepository.findRoleCodesByUserId(userId); | ||
|
|
||
| // 3. 다음 요청부터는 캐시로 처리되도록 짧은 TTL로 저장해둔다. | ||
| writeCache(userId, roles); | ||
| return roles; | ||
|
Comment on lines
+36
to
+48
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 | 🏗️ Heavy lift 캐시 미스 경쟁으로 오래된 역할이 다시 저장됩니다. 요청 A가 Line 42에서 이전 역할 목록을 읽은 뒤, 요청 B가 역할을 회수하고 사용자별 버전 또는 세대 값을 캐시 값에 포함하고 저장 전에 검증하세요. 또는 무효화와 캐시 재저장을 원자적으로 조정하세요. 이 경쟁 조건을 검증하는 동시성 테스트도 추가하세요. 🤖 Prompt for AI Agents📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win 순차 실행 단계에 번호 주석을 추가하세요.
As per coding guidelines: “For sequential execution flows, add numbered comments such as 🤖 Prompt for AI AgentsSource: Coding guidelines |
||
| } | ||
|
|
||
| public void invalidate(Long userId) { | ||
| try { | ||
| redisTemplate.delete(KEY_PREFIX + userId); | ||
| } catch (Exception e) { | ||
| log.error("Redis role 캐시 무효화 실패, userId={}: {}", userId, e.getMessage()); | ||
| } | ||
|
Comment on lines
+51
to
+56
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 | 🏗️ Heavy lift Redis 삭제 실패 시 역할 회수가 즉시 반영되지 않습니다.
무효화 실패 시 해당 사용자의 캐시를 우회하는 지속 가능한 무효화 버전 또는 세대 값을 사용하세요. Redis 장애 후 복구 시나리오를 테스트해야 합니다. 🤖 Prompt for AI Agents |
||
| } | ||
|
|
||
| private String readCache(Long userId) { | ||
| try { | ||
| return redisTemplate.opsForValue().get(KEY_PREFIX + userId); | ||
| } catch (Exception e) { | ||
| log.error("Redis role 캐시 조회 실패, DB로 폴백합니다. userId={}: {}", userId, e.getMessage()); | ||
| return null; | ||
| } | ||
| } | ||
|
|
||
| private void writeCache(Long userId, List<String> roles) { | ||
| try { | ||
| redisTemplate.opsForValue().set(KEY_PREFIX + userId, String.join(",", roles), TTL); | ||
| } catch (Exception e) { | ||
| log.error("Redis role 캐시 저장 실패, userId={}: {}", userId, e.getMessage()); | ||
| } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,33 @@ | ||
| package com.opensource.docgrid.domain.auth.jwt; | ||
|
|
||
| import static org.assertj.core.api.Assertions.assertThat; | ||
|
|
||
| import org.junit.jupiter.api.DisplayName; | ||
| import org.junit.jupiter.api.Test; | ||
|
|
||
| import io.jsonwebtoken.Claims; | ||
|
|
||
| /** | ||
| * JWT 발급·검증 계약을 검증한다. 특히 role은 더 이상 토큰에 담기지 않는다는 계약을 고정한다. | ||
| */ | ||
| class JwtProviderTest { | ||
|
|
||
| private static final String TEST_SECRET = "test-secret-key-for-jwt-provider-unit-test"; | ||
|
|
||
| private final JwtProvider jwtProvider = new JwtProvider(TEST_SECRET, 3600L); | ||
|
|
||
| @Test | ||
| @DisplayName("토큰에는 roles claim이 없고, userId/sub/jti/expiration만 담긴다") | ||
| void generateToken_omitsRolesClaim() { | ||
| String token = jwtProvider.generateToken(1L, "user@test.com"); | ||
|
|
||
| Claims claims = jwtProvider.getClaimsIfValid(token); | ||
|
|
||
| assertThat(claims).isNotNull(); | ||
| assertThat(claims.get("roles")).isNull(); | ||
| assertThat(claims.get("userId", Long.class)).isEqualTo(1L); | ||
| assertThat(claims.getSubject()).isEqualTo("user@test.com"); | ||
| assertThat(claims.get("jti", String.class)).isNotBlank(); | ||
| assertThat(claims.getExpiration()).isAfter(claims.getIssuedAt()); | ||
| } | ||
| } |
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
rolesclaim 제거의 불변식을 주석과 회귀 테스트로 고정해 주세요.generateToken은 이제 역할을 JWT에 저장하지 않습니다.RoleAuthorityService가 요청마다 역할을 조회한다는 이유를Jwts.builder()앞에 짧게 주석으로 남겨 주세요.JwtProvider테스트도roles가 없고userId,sub,jti,expiration이 유지되는지 확인해야 합니다.제안
+ // 역할은 요청마다 RoleAuthorityService에서 조회하므로 JWT에 저장하지 않는다. return Jwts.builder()코딩 가이드라인의 “중요한 코드 라인에는 로직 또는 불변식이 필요한 이유를 설명하는 간결한 주석을 추가한다” 규칙을 적용했습니다.
🤖 Prompt for AI Agents
Source: Coding guidelines