From 33b16b86b69dd04f5264ef48b5a81abe2927bfb3 Mon Sep 17 00:00:00 2001 From: JJUYAAA Date: Sun, 4 Jan 2026 21:51:16 +0900 Subject: [PATCH 01/22] =?UTF-8?q?[Refactor]=20#158=20=EA=B4=80=EB=A6=AC?= =?UTF-8?q?=EC=9E=90=EC=9A=A9=20=EB=A6=AC=ED=94=84=EB=A0=88=EC=8B=9C?= =?UTF-8?q?=ED=86=A0=ED=81=B0=20=EB=B0=9C=EA=B8=89=20=EB=A9=94=EC=84=9C?= =?UTF-8?q?=EB=93=9C=20=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/kuit/findyou/global/jwt/util/JwtUtil.java | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/main/java/com/kuit/findyou/global/jwt/util/JwtUtil.java b/src/main/java/com/kuit/findyou/global/jwt/util/JwtUtil.java index bce4f5b1..2753d569 100644 --- a/src/main/java/com/kuit/findyou/global/jwt/util/JwtUtil.java +++ b/src/main/java/com/kuit/findyou/global/jwt/util/JwtUtil.java @@ -72,6 +72,17 @@ public String createRefreshJwt(Long userId) { .compact(); } + public String createRefreshJwt(Long userId, long expireMs) { + return Jwts.builder() + .id(UUID.randomUUID().toString()) + .claim(JwtClaimKey.USER_ID.getKey(), userId) + .claim(JwtClaimKey.TOKEN_TYPE.getKey(), JwtTokenType.REFRESH_TOKEN) + .issuedAt(new Date(System.currentTimeMillis())) + .expiration(new Date(System.currentTimeMillis() + expireMs)) + .signWith(secretKey) + .compact(); + } + public void validateJwt(String token){ log.info("validateJwt"); try{ From 879fe8165d1f2f9bec56c26746ebd60f4cf924d5 Mon Sep 17 00:00:00 2001 From: JJUYAAA Date: Sun, 4 Jan 2026 22:06:46 +0900 Subject: [PATCH 02/22] =?UTF-8?q?[Feat]=20#158=20=EA=B4=80=EB=A6=AC?= =?UTF-8?q?=EC=9E=90=EC=9A=A9=20=EB=A1=9C=EA=B7=B8=EC=9D=B8=20=EC=9D=91?= =?UTF-8?q?=EB=8B=B5=20DTO=20=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../findyou/domain/auth/dto/response/AdminLoginResponse.java | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 src/main/java/com/kuit/findyou/domain/auth/dto/response/AdminLoginResponse.java diff --git a/src/main/java/com/kuit/findyou/domain/auth/dto/response/AdminLoginResponse.java b/src/main/java/com/kuit/findyou/domain/auth/dto/response/AdminLoginResponse.java new file mode 100644 index 00000000..07f330d2 --- /dev/null +++ b/src/main/java/com/kuit/findyou/domain/auth/dto/response/AdminLoginResponse.java @@ -0,0 +1,4 @@ +package com.kuit.findyou.domain.auth.dto.response; + +public record AdminLoginResponse(String accessToken, String refreshToken) { +} From 91c6b73e36ef17b9815a501f792a2ce7de48b543 Mon Sep 17 00:00:00 2001 From: JJUYAAA Date: Sun, 4 Jan 2026 22:07:32 +0900 Subject: [PATCH 03/22] =?UTF-8?q?[Feat]=20#158=20=EA=B4=80=EB=A6=AC?= =?UTF-8?q?=EC=9E=90=EC=9A=A9=20=ED=86=A0=ED=81=B0=20=EB=B0=9C=EA=B8=89=20?= =?UTF-8?q?=EC=84=9C=EB=B9=84=EC=8A=A4=20=EC=BD=94=EB=93=9C=20=EC=83=9D?= =?UTF-8?q?=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../auth/service/AdminLoginService.java | 7 ++++ .../auth/service/AdminLoginServiceImpl.java | 41 +++++++++++++++++++ 2 files changed, 48 insertions(+) create mode 100644 src/main/java/com/kuit/findyou/domain/auth/service/AdminLoginService.java create mode 100644 src/main/java/com/kuit/findyou/domain/auth/service/AdminLoginServiceImpl.java diff --git a/src/main/java/com/kuit/findyou/domain/auth/service/AdminLoginService.java b/src/main/java/com/kuit/findyou/domain/auth/service/AdminLoginService.java new file mode 100644 index 00000000..9af185dd --- /dev/null +++ b/src/main/java/com/kuit/findyou/domain/auth/service/AdminLoginService.java @@ -0,0 +1,7 @@ +package com.kuit.findyou.domain.auth.service; + +import com.kuit.findyou.domain.auth.dto.response.AdminLoginResponse; + +public interface AdminLoginService { + AdminLoginResponse adminLogin(); +} diff --git a/src/main/java/com/kuit/findyou/domain/auth/service/AdminLoginServiceImpl.java b/src/main/java/com/kuit/findyou/domain/auth/service/AdminLoginServiceImpl.java new file mode 100644 index 00000000..6db39f45 --- /dev/null +++ b/src/main/java/com/kuit/findyou/domain/auth/service/AdminLoginServiceImpl.java @@ -0,0 +1,41 @@ +package com.kuit.findyou.domain.auth.service; + +import com.kuit.findyou.domain.auth.dto.response.AdminLoginResponse; +import com.kuit.findyou.domain.auth.repository.RedisRefreshTokenRepository; +import com.kuit.findyou.domain.user.model.User; +import com.kuit.findyou.domain.user.repository.UserRepository; +import com.kuit.findyou.global.common.exception.CustomException; +import com.kuit.findyou.global.jwt.util.JwtUtil; +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; + +import static com.kuit.findyou.global.common.response.status.BaseExceptionResponseStatus.USER_NOT_FOUND; + +@RequiredArgsConstructor +@Service +public class AdminLoginServiceImpl implements AdminLoginService{ + private final JwtUtil jwtUtil; + private final UserRepository userRepository; + private final RedisRefreshTokenRepository redisRefreshTokenRepository; + + @Value("${admin.admin-user-id}") + private Long adminUserId; + + @Value("${admin.refresh-ttl-ms}") + private Long adminRefreshTtlMs; + + @Override + public AdminLoginResponse adminLogin() { + User user = userRepository.findById(adminUserId) + .orElseThrow(() -> new CustomException(USER_NOT_FOUND)); + + String accessToken = jwtUtil.createAccessJwt(user.getId(), user.getRole()); + String refreshToken = jwtUtil.createRefreshJwt(user.getId(), adminRefreshTtlMs); + + // 관리자 계정만 TTL 1년으로 저장 + redisRefreshTokenRepository.save(user.getId(), refreshToken, adminRefreshTtlMs); + + return new AdminLoginResponse(accessToken, refreshToken); + } +} From d6ca8814cffeff24070dcceeaa6ab52bce9a452a Mon Sep 17 00:00:00 2001 From: JJUYAAA Date: Sun, 4 Jan 2026 22:07:56 +0900 Subject: [PATCH 04/22] =?UTF-8?q?[Refactor]=20#158=20=EA=B4=80=EB=A6=AC?= =?UTF-8?q?=EC=9E=90=EC=9A=A9=20=ED=86=A0=ED=81=B0=20=EB=B0=9C=EA=B8=89=20?= =?UTF-8?q?=EA=B4=80=EB=A0=A8=20=ED=99=98=EA=B2=BD=EB=B3=80=EC=88=98=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/resources/application.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 1c1a7505..741e14a8 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -193,3 +193,9 @@ management: exposure: include: health, info, prometheus + +admin: + admin-user-id: ${ADMIN_USER_ID} + refresh-ttl-ms: ${ADMIN_REFRESH_TTL_MS} + api: + key: ${ADMIN_API_KEY} From b719b48a3458cc44e45073ce9a41bd26cdac97c3 Mon Sep 17 00:00:00 2001 From: JJUYAAA Date: Sun, 4 Jan 2026 22:09:08 +0900 Subject: [PATCH 05/22] =?UTF-8?q?[Refactor]=20#158=20=EA=B4=80=EB=A6=AC?= =?UTF-8?q?=EC=9E=90=EC=9A=A9=20=ED=86=A0=ED=81=B0=20=EB=B0=9C=EA=B8=89=20?= =?UTF-8?q?=EC=84=9C=EB=B9=84=EC=8A=A4=20-=20AuthServiceFacade=EC=97=90=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kuit/findyou/domain/auth/service/AuthServiceFacade.java | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/main/java/com/kuit/findyou/domain/auth/service/AuthServiceFacade.java b/src/main/java/com/kuit/findyou/domain/auth/service/AuthServiceFacade.java index d6e73592..5964bed0 100644 --- a/src/main/java/com/kuit/findyou/domain/auth/service/AuthServiceFacade.java +++ b/src/main/java/com/kuit/findyou/domain/auth/service/AuthServiceFacade.java @@ -4,6 +4,7 @@ import com.kuit.findyou.domain.auth.dto.ReissueTokenResponse; import com.kuit.findyou.domain.auth.dto.request.GuestLoginRequest; import com.kuit.findyou.domain.auth.dto.request.KakaoLoginRequest; +import com.kuit.findyou.domain.auth.dto.response.AdminLoginResponse; import com.kuit.findyou.domain.auth.dto.response.GuestLoginResponse; import com.kuit.findyou.domain.auth.dto.response.KakaoLoginResponse; import lombok.RequiredArgsConstructor; @@ -14,6 +15,7 @@ public class AuthServiceFacade { private final LoginService loginService; private final ReissueTokenService reissueTokenService; + private final AdminLoginService adminLoginService; public KakaoLoginResponse kakaoLogin(KakaoLoginRequest request) { return loginService.kakaoLogin(request); @@ -26,4 +28,8 @@ public GuestLoginResponse guestLogin(GuestLoginRequest request) { public ReissueTokenResponse reissueToken(ReissueTokenRequest request) { return reissueTokenService.reissueToken(request); } + + public AdminLoginResponse adminLogin() { + return adminLoginService.adminLogin(); + } } From 64156578ed8900a637cd250bbed020b4f9b500d9 Mon Sep 17 00:00:00 2001 From: JJUYAAA Date: Sun, 4 Jan 2026 22:09:44 +0900 Subject: [PATCH 06/22] =?UTF-8?q?[Refactor]=20#158=20Redis=EC=97=90=20?= =?UTF-8?q?=EA=B4=80=EB=A6=AC=EC=9E=90=EC=9A=A9=20=ED=86=A0=ED=81=B0=20?= =?UTF-8?q?=EC=A0=80=EC=9E=A5=ED=95=98=EB=8A=94=20=EB=A1=9C=EC=A7=81=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/auth/repository/RedisRefreshTokenRepository.java | 1 + .../auth/repository/RedisRefreshTokenRepositoryImpl.java | 5 +++++ 2 files changed, 6 insertions(+) diff --git a/src/main/java/com/kuit/findyou/domain/auth/repository/RedisRefreshTokenRepository.java b/src/main/java/com/kuit/findyou/domain/auth/repository/RedisRefreshTokenRepository.java index 7cec75a2..ed57b487 100644 --- a/src/main/java/com/kuit/findyou/domain/auth/repository/RedisRefreshTokenRepository.java +++ b/src/main/java/com/kuit/findyou/domain/auth/repository/RedisRefreshTokenRepository.java @@ -6,4 +6,5 @@ public interface RedisRefreshTokenRepository { Optional findByUserId(Long userId); void save(Long id, String refreshToken); + void save(Long id, String refreshToken, long ttlMs); } diff --git a/src/main/java/com/kuit/findyou/domain/auth/repository/RedisRefreshTokenRepositoryImpl.java b/src/main/java/com/kuit/findyou/domain/auth/repository/RedisRefreshTokenRepositoryImpl.java index 4d077b69..eff77c60 100644 --- a/src/main/java/com/kuit/findyou/domain/auth/repository/RedisRefreshTokenRepositoryImpl.java +++ b/src/main/java/com/kuit/findyou/domain/auth/repository/RedisRefreshTokenRepositoryImpl.java @@ -30,4 +30,9 @@ public Optional findByUserId(Long userId) { public void save(Long userId, String refreshToken) { redisTemplate.opsForValue().set(key(userId), refreshToken, Duration.ofMillis(refreshTokenExpireMs)); } + + @Override + public void save(Long userId, String refreshToken, long ttlMs) { + redisTemplate.opsForValue().set(key(userId), refreshToken, Duration.ofMillis(ttlMs)); + } } From 848cc4c5359056ebd7f83940ea228929d1026d0f Mon Sep 17 00:00:00 2001 From: JJUYAAA Date: Sun, 4 Jan 2026 22:10:04 +0900 Subject: [PATCH 07/22] =?UTF-8?q?[Refactor]=20#158=20=EA=B4=80=EB=A6=AC?= =?UTF-8?q?=EC=9E=90=EC=9A=A9=20=ED=86=A0=ED=81=B0=20=EB=B0=9C=EA=B8=89=20?= =?UTF-8?q?API=20-=20Controller=EC=97=90=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../auth/controller/AuthController.java | 26 ++++++++++++++++--- 1 file changed, 22 insertions(+), 4 deletions(-) diff --git a/src/main/java/com/kuit/findyou/domain/auth/controller/AuthController.java b/src/main/java/com/kuit/findyou/domain/auth/controller/AuthController.java index 647c564f..5742096c 100644 --- a/src/main/java/com/kuit/findyou/domain/auth/controller/AuthController.java +++ b/src/main/java/com/kuit/findyou/domain/auth/controller/AuthController.java @@ -3,21 +3,22 @@ import com.kuit.findyou.domain.auth.dto.ReissueTokenRequest; import com.kuit.findyou.domain.auth.dto.ReissueTokenResponse; import com.kuit.findyou.domain.auth.dto.request.GuestLoginRequest; +import com.kuit.findyou.domain.auth.dto.response.AdminLoginResponse; import com.kuit.findyou.domain.auth.dto.response.GuestLoginResponse; import com.kuit.findyou.domain.auth.dto.request.KakaoLoginRequest; import com.kuit.findyou.domain.auth.dto.response.KakaoLoginResponse; import com.kuit.findyou.domain.auth.service.AuthServiceFacade; import com.kuit.findyou.global.common.annotation.CustomExceptionDescription; +import com.kuit.findyou.global.common.exception.CustomException; import com.kuit.findyou.global.common.response.BaseResponse; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.web.bind.annotation.*; +import static com.kuit.findyou.global.common.response.status.BaseExceptionResponseStatus.UNAUTHORIZED; import static com.kuit.findyou.global.common.swagger.SwaggerResponseDescription.*; @Tag(name = "Login", description = "로그인 관련 API") @@ -28,6 +29,9 @@ public class AuthController { private final AuthServiceFacade authServiceFacade; + @Value("${admin.api.key}") + private String adminApiKey; + @Operation( summary = "카카오 로그인 API", description = "카카오 사용자 식별자를 이용해서 유저 정보와 엑세스 토큰을 얻을 수 있습니다. 가입된 회원인지 여부를 반환합니다." @@ -58,4 +62,18 @@ public BaseResponse guestLogin(@RequestBody GuestLoginReques public BaseResponse reissueToken(@RequestBody ReissueTokenRequest request){ return BaseResponse.ok(authServiceFacade.reissueToken(request)); } + + @Operation( + summary = "서비스 계정 로그인 API (내부 자동화용)", + description = "내부 자동화/관리용 서비스 계정 토큰을 발급합니다. X-ADMIN-KEY 헤더가 필요합니다." + ) + @PostMapping("/login/admin") + public BaseResponse adminLogin( + @RequestHeader(value = "X-ADMIN-KEY", required = false) String adminKey + ) { + if (adminKey == null || adminKey.isBlank() || !adminApiKey.equals(adminKey)) { + throw new CustomException(UNAUTHORIZED); + } + return BaseResponse.ok(authServiceFacade.adminLogin()); + } } From 243f44429e4fb6087d1f8056e1e619033dc19eef Mon Sep 17 00:00:00 2001 From: JJUYAAA Date: Sun, 4 Jan 2026 22:10:33 +0900 Subject: [PATCH 08/22] =?UTF-8?q?[Test]=20#158=20application-test.yml?= =?UTF-8?q?=EC=97=90=20=EA=B4=80=EB=A6=AC=EC=9E=90=EC=9A=A9=20=ED=86=A0?= =?UTF-8?q?=ED=81=B0=20=EB=B0=9C=EA=B8=89=20=EA=B4=80=EB=A0=A8=20=ED=99=98?= =?UTF-8?q?=EA=B2=BD=EB=B3=80=EC=88=98=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/test/resources/application-test.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/test/resources/application-test.yml b/src/test/resources/application-test.yml index b0ef0a06..607d7687 100644 --- a/src/test/resources/application-test.yml +++ b/src/test/resources/application-test.yml @@ -62,3 +62,9 @@ openapi: api-key: test-key volunteer-work: api-url: http://openapi.1365.go.kr/openapi/service/rest/VolunteerPartcptnService/getVltrSearchWordList + +admin: + api: + key: test-admin-key + admin-user-id: 9999 + refresh-ttl-ms: 31536000000 \ No newline at end of file From cb8d305b95f1d6b5588d9d6213a6ec7d3468254d Mon Sep 17 00:00:00 2001 From: JJUYAAA Date: Sun, 4 Jan 2026 22:11:11 +0900 Subject: [PATCH 09/22] =?UTF-8?q?[Test]=20#158=20TestInitializer=EC=97=90?= =?UTF-8?q?=20=ED=8A=B9=EC=A0=95=ED=95=9C=20ID=EC=9D=98=20=EC=9C=A0?= =?UTF-8?q?=EC=A0=80=20=EC=83=9D=EC=84=B1=ED=95=98=EB=8A=94=20=EB=A9=94?= =?UTF-8?q?=EC=84=9C=EB=93=9C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../global/common/util/TestInitializer.java | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/test/java/com/kuit/findyou/global/common/util/TestInitializer.java b/src/test/java/com/kuit/findyou/global/common/util/TestInitializer.java index 59f26dff..d0364812 100644 --- a/src/test/java/com/kuit/findyou/global/common/util/TestInitializer.java +++ b/src/test/java/com/kuit/findyou/global/common/util/TestInitializer.java @@ -20,6 +20,7 @@ import com.kuit.findyou.domain.user.model.Role; import com.kuit.findyou.domain.user.model.User; import com.kuit.findyou.domain.user.repository.UserRepository; +import jakarta.persistence.EntityManager; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Component; import org.springframework.transaction.annotation.Transactional; @@ -48,6 +49,8 @@ public class TestInitializer { private final SidoRepository sidoRepository; private final SigunguRepository sigunguRepository; + private final EntityManager em; + private User defaultUser; @Transactional @@ -430,4 +433,16 @@ public User createTestGuest() { return userRepository.save(user); } + + @Transactional + public void insertAdminUserWithFixedId(Long id, Role role) { + em.createNativeQuery(""" + INSERT INTO users (id, name, role, receive_notification, status, device_id, kakao_id, profile_image_url) + VALUES (?1, ?2, ?3, 'N', 'Y', NULL, NULL, NULL) + """) + .setParameter(1, id) + .setParameter(2, "관리자") + .setParameter(3, role.name()) + .executeUpdate(); + } } From dca8923c265d5c1bf6de9ba72428d1543053aab6 Mon Sep 17 00:00:00 2001 From: JJUYAAA Date: Sun, 4 Jan 2026 22:12:18 +0900 Subject: [PATCH 10/22] =?UTF-8?q?[Test]=20#158=20=EA=B4=80=EB=A6=AC?= =?UTF-8?q?=EC=9E=90=EC=9A=A9=20=ED=86=A0=ED=81=B0=20=EB=B0=9C=EA=B8=89=20?= =?UTF-8?q?=EC=84=9C=EB=B9=84=EC=8A=A4=20=ED=85=8C=EC=8A=A4=ED=8A=B8?= =?UTF-8?q?=EC=BD=94=EB=93=9C=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/AdminLoginServiceImplTest.java | 90 +++++++++++++++++++ 1 file changed, 90 insertions(+) create mode 100644 src/test/java/com/kuit/findyou/domain/auth/service/AdminLoginServiceImplTest.java diff --git a/src/test/java/com/kuit/findyou/domain/auth/service/AdminLoginServiceImplTest.java b/src/test/java/com/kuit/findyou/domain/auth/service/AdminLoginServiceImplTest.java new file mode 100644 index 00000000..2e367302 --- /dev/null +++ b/src/test/java/com/kuit/findyou/domain/auth/service/AdminLoginServiceImplTest.java @@ -0,0 +1,90 @@ +package com.kuit.findyou.domain.auth.service; + +import com.kuit.findyou.domain.auth.dto.response.AdminLoginResponse; +import com.kuit.findyou.domain.auth.repository.RedisRefreshTokenRepository; +import com.kuit.findyou.domain.user.model.Role; +import com.kuit.findyou.domain.user.model.User; +import com.kuit.findyou.domain.user.repository.UserRepository; +import com.kuit.findyou.global.common.exception.CustomException; +import com.kuit.findyou.global.jwt.util.JwtUtil; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.test.util.ReflectionTestUtils; + +import java.util.Optional; + +import static com.kuit.findyou.global.common.response.status.BaseExceptionResponseStatus.USER_NOT_FOUND; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +class AdminLoginServiceImplTest { + + @InjectMocks + AdminLoginServiceImpl adminLoginService; + + @Mock + JwtUtil jwtUtil; + + @Mock + UserRepository userRepository; + + @Mock + RedisRefreshTokenRepository redisRefreshTokenRepository; + + @DisplayName("관리자 로그인 성공 시 access/refresh 토큰을 반환하고 refresh는 관리자 TTL로 Redis 저장한다") + @Test + void adminLogin_shouldReturnTokens_andSaveRefreshWithAdminTtl() { + // given + Long adminUserId = 9999L; + Long adminRefreshTtlMs = 31_536_000_000L; // 1년 + User user = User.builder().id(adminUserId).role(Role.USER).build(); + + String accessToken = "admin access"; + String refreshToken = "admin refresh"; + + ReflectionTestUtils.setField(adminLoginService, "adminUserId", adminUserId); + ReflectionTestUtils.setField(adminLoginService, "adminRefreshTtlMs", adminRefreshTtlMs); + + when(userRepository.findById(adminUserId)).thenReturn(Optional.of(user)); + when(jwtUtil.createAccessJwt(eq(adminUserId), eq(user.getRole()))).thenReturn(accessToken); + + when(jwtUtil.createRefreshJwt(eq(adminUserId), eq(adminRefreshTtlMs))).thenReturn(refreshToken); + + // when + AdminLoginResponse response = adminLoginService.adminLogin(); + + // then + assertThat(response.accessToken()).isEqualTo(accessToken); + assertThat(response.refreshToken()).isEqualTo(refreshToken); + + // Redis 저장도 관리자 TTL로 호출되는지 검증 + verify(redisRefreshTokenRepository, times(1)).save(eq(adminUserId), eq(refreshToken), eq(adminRefreshTtlMs)); + } + + @DisplayName("관리자 유저가 존재하지 않으면 USER_NOT_FOUND 예외를 발생시킨다") + @Test + void adminLogin_shouldThrowException_whenAdminUserNotFound() { + // given + Long adminUserId = 9999L; + Long adminRefreshTtlMs = 31_536_000_000L; + + ReflectionTestUtils.setField(adminLoginService, "adminUserId", adminUserId); + ReflectionTestUtils.setField(adminLoginService, "adminRefreshTtlMs", adminRefreshTtlMs); + + when(userRepository.findById(adminUserId)).thenReturn(Optional.empty()); + + // when & then + assertThatThrownBy(() -> adminLoginService.adminLogin()) + .isInstanceOf(CustomException.class) + .hasMessage(USER_NOT_FOUND.getMessage()); + + verify(redisRefreshTokenRepository, never()).save(anyLong(), anyString(), anyLong()); + } +} From ca4909e5764cefce233ba6f7597c5f91ed83c873 Mon Sep 17 00:00:00 2001 From: JJUYAAA Date: Sun, 4 Jan 2026 22:13:14 +0900 Subject: [PATCH 11/22] =?UTF-8?q?[Test]=20#158=20AuthControllerTest?= =?UTF-8?q?=EC=97=90=20=EA=B4=80=EB=A6=AC=EC=9E=90=EC=9A=A9=20=ED=86=A0?= =?UTF-8?q?=ED=81=B0=20=EB=B0=9C=EA=B8=89=20API=20=ED=85=8C=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../auth/controller/AuthControllerTest.java | 106 +++++++++++++++++- 1 file changed, 104 insertions(+), 2 deletions(-) diff --git a/src/test/java/com/kuit/findyou/domain/auth/controller/AuthControllerTest.java b/src/test/java/com/kuit/findyou/domain/auth/controller/AuthControllerTest.java index 649607c1..1efcb2db 100644 --- a/src/test/java/com/kuit/findyou/domain/auth/controller/AuthControllerTest.java +++ b/src/test/java/com/kuit/findyou/domain/auth/controller/AuthControllerTest.java @@ -3,8 +3,9 @@ import com.kuit.findyou.domain.auth.dto.ReissueTokenRequest; import com.kuit.findyou.domain.auth.dto.ReissueTokenResponse; import com.kuit.findyou.domain.auth.dto.request.GuestLoginRequest; -import com.kuit.findyou.domain.auth.dto.response.GuestLoginResponse; import com.kuit.findyou.domain.auth.dto.request.KakaoLoginRequest; +import com.kuit.findyou.domain.auth.dto.response.AdminLoginResponse; +import com.kuit.findyou.domain.auth.dto.response.GuestLoginResponse; import com.kuit.findyou.domain.auth.dto.response.KakaoLoginResponse; import com.kuit.findyou.domain.auth.repository.RedisRefreshTokenRepository; import com.kuit.findyou.domain.user.model.Role; @@ -13,6 +14,7 @@ import com.kuit.findyou.global.common.response.BaseErrorResponse; import com.kuit.findyou.global.common.response.BaseResponse; import com.kuit.findyou.global.common.util.DatabaseCleaner; +import com.kuit.findyou.global.common.util.TestInitializer; import com.kuit.findyou.global.config.RedisTestContainersConfig; import com.kuit.findyou.global.config.TestDatabaseConfig; import com.kuit.findyou.global.jwt.util.JwtClaimKey; @@ -22,7 +24,10 @@ import io.restassured.RestAssured; import io.restassured.common.mapper.TypeRef; import io.restassured.http.ContentType; -import org.junit.jupiter.api.*; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.test.context.SpringBootTest; @@ -60,6 +65,15 @@ class AuthControllerTest { @Autowired private DatabaseCleaner databaseCleaner; + @Autowired + TestInitializer testInitializer; + + @Value("${admin.api.key}") + String adminApiKey; + + @Value("${admin.admin-user-id}") + Long adminUserId; + @Value("${findyou.jwt.secret-key}") String secret; @@ -255,4 +269,92 @@ void reissueToken_shouldReturnNotFound_WhenRefreshTokenIsNotMatched(){ assertThat(response.getCode()).isEqualTo(REFRESH_TOKEN_NOT_FOUND.getCode()); assertThat(response.getMessage()).isEqualTo(REFRESH_TOKEN_NOT_FOUND.getMessage()); } + + @DisplayName("관리자 키가 유효하면 관리자 로그인 성공(토큰 반환 + Redis 저장)") + @Test + void adminLogin_shouldReturnTokens_WhenValidAdminKey() { + // given + testInitializer.insertAdminUserWithFixedId(9999L, Role.USER); + + // when + BaseResponse response = given() + .contentType(ContentType.JSON) + .accept(ContentType.JSON) + .header("X-ADMIN-KEY", adminApiKey) + .body("{}") + .when() + .post("/api/v2/auth/login/admin") + .then() + .statusCode(200) + .extract() + .as(new TypeRef>() {}); + + // then + String access = response.getData().accessToken(); + String refresh = response.getData().refreshToken(); + + assertThat(access).isNotBlank(); + assertThat(refresh).isNotBlank(); + + // access 토큰 검증 + assertThat(jwtUtil.getUserId(access)).isEqualTo(adminUserId); + assertThat(jwtUtil.getTokenType(access)).isEqualTo(JwtTokenType.ACCESS_TOKEN); + + // refresh 토큰 검증 + assertThat(jwtUtil.getUserId(refresh)).isEqualTo(adminUserId); + assertThat(jwtUtil.getTokenType(refresh)).isEqualTo(JwtTokenType.REFRESH_TOKEN); + + // Redis에 저장됐는지 확인 (현재 저장값 == 발급 refresh) + String saved = redisRefreshTokenRepository.findByUserId(adminUserId).orElse(null); + assertThat(saved).isEqualTo(refresh); + } + + @DisplayName("관리자 키가 틀리면 401을 반환한다") + @Test + void adminLogin_shouldReturnUnauthorized_WhenInvalidAdminKey() { + // given + testInitializer.insertAdminUserWithFixedId(9999L, Role.USER); + + // when + BaseErrorResponse response = given() + .contentType(ContentType.JSON) + .accept(ContentType.JSON) + .header("X-ADMIN-KEY", "wrong-key") + .body("{}") + .when() + .post("/api/v2/auth/login/admin") + .then() + .extract() + .as(new TypeRef() {}); + + // then + assertThat(response.getCode()).isEqualTo(UNAUTHORIZED.getCode()); + assertThat(response.getMessage()).isEqualTo(UNAUTHORIZED.getMessage()); + assertThat(response.getSuccess()).isFalse(); + } + + @DisplayName("관리자 유저가 존재하지 않으면 404(USER_NOT_FOUND)를 반환한다") + @Test + void adminLogin_shouldReturnNotFound_WhenAdminUserDoesNotExist() { + // given: insertAdminUserWithFixedId 호출 안 함 + + // when + BaseErrorResponse response = given() + .contentType(ContentType.JSON) + .accept(ContentType.JSON) + .header("X-ADMIN-KEY", adminApiKey) + .body("{}") + .when() + .post("/api/v2/auth/login/admin") + .then() + .extract() + .as(new TypeRef() {}); + + // then + assertThat(response.getCode()).isEqualTo(USER_NOT_FOUND.getCode()); + assertThat(response.getMessage()).isEqualTo(USER_NOT_FOUND.getMessage()); + assertThat(response.getSuccess()).isFalse(); + } + + } \ No newline at end of file From ecf97e3de84bd6969c40c87c91d923addd0e0d34 Mon Sep 17 00:00:00 2001 From: JJUYAAA Date: Sun, 4 Jan 2026 22:52:37 +0900 Subject: [PATCH 12/22] =?UTF-8?q?[Test]=20#158=20AuthControllerTest=20?= =?UTF-8?q?=EB=B3=B4=EA=B0=95=20-=20401=20=EB=B0=98=ED=99=98=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../auth/controller/AuthControllerTest.java | 51 ++++++++++++++++++- 1 file changed, 49 insertions(+), 2 deletions(-) diff --git a/src/test/java/com/kuit/findyou/domain/auth/controller/AuthControllerTest.java b/src/test/java/com/kuit/findyou/domain/auth/controller/AuthControllerTest.java index 1efcb2db..c02432b8 100644 --- a/src/test/java/com/kuit/findyou/domain/auth/controller/AuthControllerTest.java +++ b/src/test/java/com/kuit/findyou/domain/auth/controller/AuthControllerTest.java @@ -274,7 +274,7 @@ void reissueToken_shouldReturnNotFound_WhenRefreshTokenIsNotMatched(){ @Test void adminLogin_shouldReturnTokens_WhenValidAdminKey() { // given - testInitializer.insertAdminUserWithFixedId(9999L, Role.USER); + testInitializer.insertAdminUserWithFixedId(adminUserId, Role.USER); // when BaseResponse response = given() @@ -313,7 +313,7 @@ void adminLogin_shouldReturnTokens_WhenValidAdminKey() { @Test void adminLogin_shouldReturnUnauthorized_WhenInvalidAdminKey() { // given - testInitializer.insertAdminUserWithFixedId(9999L, Role.USER); + testInitializer.insertAdminUserWithFixedId(adminUserId, Role.USER); // when BaseErrorResponse response = given() @@ -332,6 +332,53 @@ void adminLogin_shouldReturnUnauthorized_WhenInvalidAdminKey() { assertThat(response.getMessage()).isEqualTo(UNAUTHORIZED.getMessage()); assertThat(response.getSuccess()).isFalse(); } + @DisplayName("관리자 키 헤더가 없으면 401을 반환한다") + @Test + void adminLogin_shouldReturnUnauthorized_WhenAdminKeyMissing() { + // given + testInitializer.insertAdminUserWithFixedId(adminUserId, Role.USER); + + // when + BaseErrorResponse response = given() + .contentType(ContentType.JSON) + .accept(ContentType.JSON) + .body("{}") + .when() + .post("/api/v2/auth/login/admin") + .then() + .extract() + .as(new TypeRef() {}); + + // then + assertThat(response.getCode()).isEqualTo(UNAUTHORIZED.getCode()); + assertThat(response.getMessage()).isEqualTo(UNAUTHORIZED.getMessage()); + assertThat(response.getSuccess()).isFalse(); + } + + @DisplayName("관리자 키가 빈 문자열이면 401을 반환한다") + @Test + void adminLogin_shouldReturnUnauthorized_WhenAdminKeyIsBlank() { + // given + testInitializer.insertAdminUserWithFixedId(adminUserId, Role.USER); + + // when + BaseErrorResponse response = given() + .contentType(ContentType.JSON) + .accept(ContentType.JSON) + .header("X-ADMIN-KEY", "") + .body("{}") + .when() + .post("/api/v2/auth/login/admin") + .then() + .extract() + .as(new TypeRef() {}); + + // then + assertThat(response.getCode()).isEqualTo(UNAUTHORIZED.getCode()); + assertThat(response.getMessage()).isEqualTo(UNAUTHORIZED.getMessage()); + assertThat(response.getSuccess()).isFalse(); + } + @DisplayName("관리자 유저가 존재하지 않으면 404(USER_NOT_FOUND)를 반환한다") @Test From 274e51a6d573220cac8f3849be8c9a8eaf386414 Mon Sep 17 00:00:00 2001 From: JJUYAAA Date: Sat, 10 Jan 2026 21:14:45 +0900 Subject: [PATCH 13/22] =?UTF-8?q?[Refactor]=20#158=20Role-ADMIN=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/com/kuit/findyou/domain/user/model/Role.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/kuit/findyou/domain/user/model/Role.java b/src/main/java/com/kuit/findyou/domain/user/model/Role.java index 49c0f113..e17743c2 100644 --- a/src/main/java/com/kuit/findyou/domain/user/model/Role.java +++ b/src/main/java/com/kuit/findyou/domain/user/model/Role.java @@ -5,7 +5,7 @@ @Getter public enum Role { - USER("회원"), GUEST("비회원"); + USER("회원"), GUEST("비회원"), ADMIN("관리자"); private final String value; From 284159faf6d759cccdb35d5a46238bc1023af30b Mon Sep 17 00:00:00 2001 From: JJUYAAA Date: Sat, 10 Jan 2026 21:15:28 +0900 Subject: [PATCH 14/22] =?UTF-8?q?[Refactor]=20#158=20=EA=B4=80=EB=A6=AC?= =?UTF-8?q?=EC=9E=90=EC=9A=A9=20refreshToken=20=EC=83=9D=EC=84=B1=20?= =?UTF-8?q?=EB=A9=94=EC=84=9C=EB=93=9C=20=EC=82=AD=EC=A0=9C,=20=EA=B4=80?= =?UTF-8?q?=EB=A6=AC=EC=9E=90=EC=9A=A9=20accessToken=20=EC=83=9D=EC=84=B1?= =?UTF-8?q?=20=EB=A9=94=EC=84=9C=EB=93=9C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/kuit/findyou/global/jwt/util/JwtUtil.java | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/main/java/com/kuit/findyou/global/jwt/util/JwtUtil.java b/src/main/java/com/kuit/findyou/global/jwt/util/JwtUtil.java index 2753d569..286074e2 100644 --- a/src/main/java/com/kuit/findyou/global/jwt/util/JwtUtil.java +++ b/src/main/java/com/kuit/findyou/global/jwt/util/JwtUtil.java @@ -61,24 +61,24 @@ public String createAccessJwt(Long userId, Role role) { .compact(); } - public String createRefreshJwt(Long userId) { + public String createAccessJwt(Long userId, Role role, long expireMs) { return Jwts.builder() - .id(UUID.randomUUID().toString()) .claim(JwtClaimKey.USER_ID.getKey(), userId) - .claim(JwtClaimKey.TOKEN_TYPE.getKey(), JwtTokenType.REFRESH_TOKEN) + .claim(JwtClaimKey.ROLE.getKey(), role.name()) + .claim(JwtClaimKey.TOKEN_TYPE.getKey(), JwtTokenType.ACCESS_TOKEN) .issuedAt(new Date(System.currentTimeMillis())) - .expiration(new Date(System.currentTimeMillis() + refreshTokenExpireMs)) + .expiration(new Date(System.currentTimeMillis() + expireMs)) .signWith(secretKey) .compact(); } - public String createRefreshJwt(Long userId, long expireMs) { + public String createRefreshJwt(Long userId) { return Jwts.builder() .id(UUID.randomUUID().toString()) .claim(JwtClaimKey.USER_ID.getKey(), userId) .claim(JwtClaimKey.TOKEN_TYPE.getKey(), JwtTokenType.REFRESH_TOKEN) .issuedAt(new Date(System.currentTimeMillis())) - .expiration(new Date(System.currentTimeMillis() + expireMs)) + .expiration(new Date(System.currentTimeMillis() + refreshTokenExpireMs)) .signWith(secretKey) .compact(); } From 51f42247c9b8b33c4fd8af7edf350b13b860dbaf Mon Sep 17 00:00:00 2001 From: JJUYAAA Date: Sat, 10 Jan 2026 21:16:02 +0900 Subject: [PATCH 15/22] =?UTF-8?q?[Refactor]=20#158=20=EA=B4=80=EB=A6=AC?= =?UTF-8?q?=EC=9E=90=EC=9A=A9=20=EB=A1=9C=EA=B7=B8=EC=9D=B8=20ResponseDTO?= =?UTF-8?q?=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/auth/dto/response/AdminLoginResponse.java | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/kuit/findyou/domain/auth/dto/response/AdminLoginResponse.java b/src/main/java/com/kuit/findyou/domain/auth/dto/response/AdminLoginResponse.java index 07f330d2..56b1eb61 100644 --- a/src/main/java/com/kuit/findyou/domain/auth/dto/response/AdminLoginResponse.java +++ b/src/main/java/com/kuit/findyou/domain/auth/dto/response/AdminLoginResponse.java @@ -1,4 +1,12 @@ package com.kuit.findyou.domain.auth.dto.response; -public record AdminLoginResponse(String accessToken, String refreshToken) { +import io.swagger.v3.oas.annotations.media.Schema; + +@Schema(description = "관리자 로그인 응답 DTO") +public record AdminLoginResponse( + @Schema(description = "관리자 유저 식별자") + Long userId, + @Schema(description = "엑세스 토큰") + String accessToken +) { } From 53d27a8917d3a02f0764a931229b947e05cae71d Mon Sep 17 00:00:00 2001 From: JJUYAAA Date: Sat, 10 Jan 2026 22:14:18 +0900 Subject: [PATCH 16/22] =?UTF-8?q?[Refactor]=20#158=20=EA=B4=80=EB=A6=AC?= =?UTF-8?q?=EC=9E=90=EC=9A=A9=20=EB=A1=9C=EA=B7=B8=EC=9D=B8=20=EC=84=9C?= =?UTF-8?q?=EB=B9=84=EC=8A=A4=20=EC=BD=94=EB=93=9C=20=EC=88=98=EC=A0=95=20?= =?UTF-8?q?-=20refreshToken=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../auth/service/AdminLoginServiceImpl.java | 15 +++++---------- src/main/resources/application.yml | 2 +- 2 files changed, 6 insertions(+), 11 deletions(-) diff --git a/src/main/java/com/kuit/findyou/domain/auth/service/AdminLoginServiceImpl.java b/src/main/java/com/kuit/findyou/domain/auth/service/AdminLoginServiceImpl.java index 6db39f45..f9f8ec61 100644 --- a/src/main/java/com/kuit/findyou/domain/auth/service/AdminLoginServiceImpl.java +++ b/src/main/java/com/kuit/findyou/domain/auth/service/AdminLoginServiceImpl.java @@ -1,7 +1,7 @@ package com.kuit.findyou.domain.auth.service; import com.kuit.findyou.domain.auth.dto.response.AdminLoginResponse; -import com.kuit.findyou.domain.auth.repository.RedisRefreshTokenRepository; +import com.kuit.findyou.domain.user.model.Role; import com.kuit.findyou.domain.user.model.User; import com.kuit.findyou.domain.user.repository.UserRepository; import com.kuit.findyou.global.common.exception.CustomException; @@ -17,25 +17,20 @@ public class AdminLoginServiceImpl implements AdminLoginService{ private final JwtUtil jwtUtil; private final UserRepository userRepository; - private final RedisRefreshTokenRepository redisRefreshTokenRepository; @Value("${admin.admin-user-id}") private Long adminUserId; - @Value("${admin.refresh-ttl-ms}") - private Long adminRefreshTtlMs; + @Value("${admin.access-ttl-ms}") + private Long adminAccessTtlMs; @Override public AdminLoginResponse adminLogin() { User user = userRepository.findById(adminUserId) .orElseThrow(() -> new CustomException(USER_NOT_FOUND)); - String accessToken = jwtUtil.createAccessJwt(user.getId(), user.getRole()); - String refreshToken = jwtUtil.createRefreshJwt(user.getId(), adminRefreshTtlMs); + String accessToken = jwtUtil.createAccessJwt(user.getId(), Role.ADMIN, adminAccessTtlMs); - // 관리자 계정만 TTL 1년으로 저장 - redisRefreshTokenRepository.save(user.getId(), refreshToken, adminRefreshTtlMs); - - return new AdminLoginResponse(accessToken, refreshToken); + return new AdminLoginResponse(user.getId(), accessToken); } } diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 741e14a8..50c4c987 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -196,6 +196,6 @@ management: admin: admin-user-id: ${ADMIN_USER_ID} - refresh-ttl-ms: ${ADMIN_REFRESH_TTL_MS} + access-ttl-ms: ${ADMIN_ACCESS_TTL_MS} api: key: ${ADMIN_API_KEY} From 7f4d66a3e6380888a3e6dea65b0ed4a3540f2a1d Mon Sep 17 00:00:00 2001 From: JJUYAAA Date: Sat, 10 Jan 2026 22:14:51 +0900 Subject: [PATCH 17/22] =?UTF-8?q?[Refactor]=20#158=20RedisRefreshTokenRepo?= =?UTF-8?q?sitory=20=EC=88=98=EC=A0=95=20-=20refreshToken=20=EC=A0=9C?= =?UTF-8?q?=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/auth/repository/RedisRefreshTokenRepository.java | 1 - .../auth/repository/RedisRefreshTokenRepositoryImpl.java | 5 ----- 2 files changed, 6 deletions(-) diff --git a/src/main/java/com/kuit/findyou/domain/auth/repository/RedisRefreshTokenRepository.java b/src/main/java/com/kuit/findyou/domain/auth/repository/RedisRefreshTokenRepository.java index ed57b487..7cec75a2 100644 --- a/src/main/java/com/kuit/findyou/domain/auth/repository/RedisRefreshTokenRepository.java +++ b/src/main/java/com/kuit/findyou/domain/auth/repository/RedisRefreshTokenRepository.java @@ -6,5 +6,4 @@ public interface RedisRefreshTokenRepository { Optional findByUserId(Long userId); void save(Long id, String refreshToken); - void save(Long id, String refreshToken, long ttlMs); } diff --git a/src/main/java/com/kuit/findyou/domain/auth/repository/RedisRefreshTokenRepositoryImpl.java b/src/main/java/com/kuit/findyou/domain/auth/repository/RedisRefreshTokenRepositoryImpl.java index eff77c60..4d077b69 100644 --- a/src/main/java/com/kuit/findyou/domain/auth/repository/RedisRefreshTokenRepositoryImpl.java +++ b/src/main/java/com/kuit/findyou/domain/auth/repository/RedisRefreshTokenRepositoryImpl.java @@ -30,9 +30,4 @@ public Optional findByUserId(Long userId) { public void save(Long userId, String refreshToken) { redisTemplate.opsForValue().set(key(userId), refreshToken, Duration.ofMillis(refreshTokenExpireMs)); } - - @Override - public void save(Long userId, String refreshToken, long ttlMs) { - redisTemplate.opsForValue().set(key(userId), refreshToken, Duration.ofMillis(ttlMs)); - } } From a8f2b1cd70504670832f286e03a9b6dbfdf318ff Mon Sep 17 00:00:00 2001 From: JJUYAAA Date: Sat, 10 Jan 2026 22:15:18 +0900 Subject: [PATCH 18/22] =?UTF-8?q?[Refactor]=20#158=20AdminAllowlistFilter?= =?UTF-8?q?=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../jwt/filter/AdminAllowlistFilter.java | 70 +++++++++++++++++++ .../security/CustomAccessDeniedHandler.java | 5 +- 2 files changed, 74 insertions(+), 1 deletion(-) create mode 100644 src/main/java/com/kuit/findyou/global/jwt/filter/AdminAllowlistFilter.java diff --git a/src/main/java/com/kuit/findyou/global/jwt/filter/AdminAllowlistFilter.java b/src/main/java/com/kuit/findyou/global/jwt/filter/AdminAllowlistFilter.java new file mode 100644 index 00000000..511318ef --- /dev/null +++ b/src/main/java/com/kuit/findyou/global/jwt/filter/AdminAllowlistFilter.java @@ -0,0 +1,70 @@ +package com.kuit.findyou.global.jwt.filter; + +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpMethod; +import org.springframework.security.access.AccessDeniedException; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.stereotype.Component; +import org.springframework.util.AntPathMatcher; +import org.springframework.web.filter.OncePerRequestFilter; + +import java.io.IOException; +import java.util.List; + +@Component +@RequiredArgsConstructor +public class AdminAllowlistFilter extends OncePerRequestFilter { + + private final AntPathMatcher matcher = new AntPathMatcher(); + + // ADMIN에게 허용되는 API + private static final List ADMIN_ALLOWLIST = List.of( + new Allow(HttpMethod.GET.name(), "/api/v2/reports/protecting-reports/random-s3"), + new Allow(HttpMethod.GET.name(), "/api/v2/reports/missing-reports/random-s3"), + new Allow(HttpMethod.POST.name(), "/api/v2/images/upload") + ); + + @Override + protected void doFilterInternal( + HttpServletRequest request, + HttpServletResponse response, + FilterChain filterChain + ) throws ServletException, IOException { + + Authentication auth = SecurityContextHolder.getContext().getAuthentication(); + + if (auth != null && auth.isAuthenticated()) { + boolean isAdmin = auth.getAuthorities().stream() + .anyMatch(a -> a.getAuthority().equals("ROLE_ADMIN")); + + if (isAdmin) { + String method = request.getMethod(); + String path = request.getRequestURI(); + + boolean allowed = ADMIN_ALLOWLIST.stream() + .anyMatch(a -> a.method.equals(method) && matcher.match(a.pathPattern, path)); + + if (!allowed) { + throw new AccessDeniedException("ADMIN은 허용된 API만 호출할 수 있습니다."); + } + } + } + + filterChain.doFilter(request, response); + } + + private static class Allow { + final String method; + final String pathPattern; + + private Allow(String method, String pathPattern) { + this.method = method; + this.pathPattern = pathPattern; + } + } +} diff --git a/src/main/java/com/kuit/findyou/global/jwt/security/CustomAccessDeniedHandler.java b/src/main/java/com/kuit/findyou/global/jwt/security/CustomAccessDeniedHandler.java index e59be912..ea0cc62a 100644 --- a/src/main/java/com/kuit/findyou/global/jwt/security/CustomAccessDeniedHandler.java +++ b/src/main/java/com/kuit/findyou/global/jwt/security/CustomAccessDeniedHandler.java @@ -21,7 +21,10 @@ public class CustomAccessDeniedHandler implements AccessDeniedHandler { @Override public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException { - BaseErrorResponse body = new BaseErrorResponse(FORBIDDEN); + String message = accessDeniedException.getMessage(); + BaseErrorResponse body = (message == null || message.isBlank()) + ? new BaseErrorResponse(FORBIDDEN) + : new BaseErrorResponse(FORBIDDEN, message); String json = objectMapper.writeValueAsString(body); response.setStatus(FORBIDDEN.getCode()); From 40de9147fa626ee736d8dcaaa0b3a854c5297d9f Mon Sep 17 00:00:00 2001 From: JJUYAAA Date: Sat, 10 Jan 2026 22:15:44 +0900 Subject: [PATCH 19/22] =?UTF-8?q?[Test]=20#158=20AdminLoginServiceImplTest?= =?UTF-8?q?=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/AdminLoginServiceImplTest.java | 41 +++++++++---------- 1 file changed, 20 insertions(+), 21 deletions(-) diff --git a/src/test/java/com/kuit/findyou/domain/auth/service/AdminLoginServiceImplTest.java b/src/test/java/com/kuit/findyou/domain/auth/service/AdminLoginServiceImplTest.java index 2e367302..6b6a5bd9 100644 --- a/src/test/java/com/kuit/findyou/domain/auth/service/AdminLoginServiceImplTest.java +++ b/src/test/java/com/kuit/findyou/domain/auth/service/AdminLoginServiceImplTest.java @@ -35,37 +35,41 @@ class AdminLoginServiceImplTest { @Mock UserRepository userRepository; - @Mock - RedisRefreshTokenRepository redisRefreshTokenRepository; - - @DisplayName("관리자 로그인 성공 시 access/refresh 토큰을 반환하고 refresh는 관리자 TTL로 Redis 저장한다") + @DisplayName("관리자 로그인 성공 시 access 토큰을 반환한다") @Test - void adminLogin_shouldReturnTokens_andSaveRefreshWithAdminTtl() { + void adminLogin_shouldReturnAccessToken() { // given Long adminUserId = 9999L; - Long adminRefreshTtlMs = 31_536_000_000L; // 1년 - User user = User.builder().id(adminUserId).role(Role.USER).build(); + Long adminAccessTtlMs = 5_184_000_000L; // 60일 + User admin = User.builder() + .id(adminUserId) + .role(Role.ADMIN) + .build(); String accessToken = "admin access"; - String refreshToken = "admin refresh"; ReflectionTestUtils.setField(adminLoginService, "adminUserId", adminUserId); - ReflectionTestUtils.setField(adminLoginService, "adminRefreshTtlMs", adminRefreshTtlMs); - - when(userRepository.findById(adminUserId)).thenReturn(Optional.of(user)); - when(jwtUtil.createAccessJwt(eq(adminUserId), eq(user.getRole()))).thenReturn(accessToken); + ReflectionTestUtils.setField(adminLoginService, "adminAccessTtlMs", adminAccessTtlMs); - when(jwtUtil.createRefreshJwt(eq(adminUserId), eq(adminRefreshTtlMs))).thenReturn(refreshToken); + when(userRepository.findById(adminUserId)).thenReturn(Optional.of(admin)); + when(jwtUtil.createAccessJwt( + eq(adminUserId), + eq(Role.ADMIN), + eq(adminAccessTtlMs) + )).thenReturn(accessToken); // when AdminLoginResponse response = adminLoginService.adminLogin(); // then + assertThat(response.userId()).isEqualTo(adminUserId); assertThat(response.accessToken()).isEqualTo(accessToken); - assertThat(response.refreshToken()).isEqualTo(refreshToken); - // Redis 저장도 관리자 TTL로 호출되는지 검증 - verify(redisRefreshTokenRepository, times(1)).save(eq(adminUserId), eq(refreshToken), eq(adminRefreshTtlMs)); + verify(jwtUtil).createAccessJwt( + eq(adminUserId), + eq(Role.ADMIN), + eq(adminAccessTtlMs) + ); } @DisplayName("관리자 유저가 존재하지 않으면 USER_NOT_FOUND 예외를 발생시킨다") @@ -73,10 +77,7 @@ void adminLogin_shouldReturnTokens_andSaveRefreshWithAdminTtl() { void adminLogin_shouldThrowException_whenAdminUserNotFound() { // given Long adminUserId = 9999L; - Long adminRefreshTtlMs = 31_536_000_000L; - ReflectionTestUtils.setField(adminLoginService, "adminUserId", adminUserId); - ReflectionTestUtils.setField(adminLoginService, "adminRefreshTtlMs", adminRefreshTtlMs); when(userRepository.findById(adminUserId)).thenReturn(Optional.empty()); @@ -84,7 +85,5 @@ void adminLogin_shouldThrowException_whenAdminUserNotFound() { assertThatThrownBy(() -> adminLoginService.adminLogin()) .isInstanceOf(CustomException.class) .hasMessage(USER_NOT_FOUND.getMessage()); - - verify(redisRefreshTokenRepository, never()).save(anyLong(), anyString(), anyLong()); } } From 3b1282539f5fcc0b7180bfc0932d11926fa193be Mon Sep 17 00:00:00 2001 From: JJUYAAA Date: Sat, 10 Jan 2026 22:15:57 +0900 Subject: [PATCH 20/22] =?UTF-8?q?[Test]=20#158=20AuthControllerTest=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../auth/controller/AuthControllerTest.java | 17 ++++------------- src/test/resources/application-test.yml | 2 +- 2 files changed, 5 insertions(+), 14 deletions(-) diff --git a/src/test/java/com/kuit/findyou/domain/auth/controller/AuthControllerTest.java b/src/test/java/com/kuit/findyou/domain/auth/controller/AuthControllerTest.java index c02432b8..99174bf7 100644 --- a/src/test/java/com/kuit/findyou/domain/auth/controller/AuthControllerTest.java +++ b/src/test/java/com/kuit/findyou/domain/auth/controller/AuthControllerTest.java @@ -270,11 +270,11 @@ void reissueToken_shouldReturnNotFound_WhenRefreshTokenIsNotMatched(){ assertThat(response.getMessage()).isEqualTo(REFRESH_TOKEN_NOT_FOUND.getMessage()); } - @DisplayName("관리자 키가 유효하면 관리자 로그인 성공(토큰 반환 + Redis 저장)") + @DisplayName("관리자 키가 유효하면 관리자 로그인 성공(access 토큰 반환)") @Test - void adminLogin_shouldReturnTokens_WhenValidAdminKey() { + void adminLogin_shouldReturnAccessToken_WhenValidAdminKey() { // given - testInitializer.insertAdminUserWithFixedId(adminUserId, Role.USER); + testInitializer.insertAdminUserWithFixedId(adminUserId, Role.ADMIN); // when BaseResponse response = given() @@ -291,22 +291,13 @@ void adminLogin_shouldReturnTokens_WhenValidAdminKey() { // then String access = response.getData().accessToken(); - String refresh = response.getData().refreshToken(); assertThat(access).isNotBlank(); - assertThat(refresh).isNotBlank(); // access 토큰 검증 assertThat(jwtUtil.getUserId(access)).isEqualTo(adminUserId); + assertThat(jwtUtil.getRole(access)).isEqualTo(Role.ADMIN); assertThat(jwtUtil.getTokenType(access)).isEqualTo(JwtTokenType.ACCESS_TOKEN); - - // refresh 토큰 검증 - assertThat(jwtUtil.getUserId(refresh)).isEqualTo(adminUserId); - assertThat(jwtUtil.getTokenType(refresh)).isEqualTo(JwtTokenType.REFRESH_TOKEN); - - // Redis에 저장됐는지 확인 (현재 저장값 == 발급 refresh) - String saved = redisRefreshTokenRepository.findByUserId(adminUserId).orElse(null); - assertThat(saved).isEqualTo(refresh); } @DisplayName("관리자 키가 틀리면 401을 반환한다") diff --git a/src/test/resources/application-test.yml b/src/test/resources/application-test.yml index 607d7687..edf27feb 100644 --- a/src/test/resources/application-test.yml +++ b/src/test/resources/application-test.yml @@ -67,4 +67,4 @@ admin: api: key: test-admin-key admin-user-id: 9999 - refresh-ttl-ms: 31536000000 \ No newline at end of file + access-ttl-ms: 5184000000 \ No newline at end of file From 64b4e4bc820d42e0bd50f2577a11c6518a7bb969 Mon Sep 17 00:00:00 2001 From: JJUYAAA Date: Sat, 10 Jan 2026 22:47:50 +0900 Subject: [PATCH 21/22] =?UTF-8?q?[Refactor]=20#158=20Image=20=EC=97=85?= =?UTF-8?q?=EB=A1=9C=EB=93=9C=20API=20-=20PreAuthorize=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kuit/findyou/domain/image/controller/ImageController.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/kuit/findyou/domain/image/controller/ImageController.java b/src/main/java/com/kuit/findyou/domain/image/controller/ImageController.java index 20ad9bad..dafe6265 100644 --- a/src/main/java/com/kuit/findyou/domain/image/controller/ImageController.java +++ b/src/main/java/com/kuit/findyou/domain/image/controller/ImageController.java @@ -29,7 +29,7 @@ public class ImageController { @Operation(summary = "신고글 이미지 업로드 API", description = "멀티파트 이미지 업로드 후 CDN URL 리스트 반환") @CustomExceptionDescription(IMAGE_UPLOAD) - @PreAuthorize("hasRole('ROLE_USER')") + @PreAuthorize("hasAnyRole('ROLE_USER','ROLE_ADMIN')") @PostMapping(value = "/upload", consumes = MULTIPART_FORM_DATA_VALUE) public BaseResponse uploadImages(@RequestPart(value = "files", required = false) List files, @LoginUserId Long userId) { List urls = imageUploadService.uploadImages(files); From c25b0f49aa7b15a0a68f68a2b0fb7bc59a625ab8 Mon Sep 17 00:00:00 2001 From: JJUYAAA Date: Sun, 11 Jan 2026 19:55:02 +0900 Subject: [PATCH 22/22] =?UTF-8?q?[Refactor]=20#158=20ExceptionTranslationF?= =?UTF-8?q?ilter=20=EB=92=A4=EC=97=90=20AdminAllowListFilter=EB=A5=BC=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kuit/findyou/global/config/SecurityConfig.java | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/main/java/com/kuit/findyou/global/config/SecurityConfig.java b/src/main/java/com/kuit/findyou/global/config/SecurityConfig.java index 6a566986..5708a572 100644 --- a/src/main/java/com/kuit/findyou/global/config/SecurityConfig.java +++ b/src/main/java/com/kuit/findyou/global/config/SecurityConfig.java @@ -1,10 +1,12 @@ package com.kuit.findyou.global.config; +import com.kuit.findyou.global.jwt.filter.AdminAllowlistFilter; import com.kuit.findyou.global.jwt.security.CustomAccessDeniedHandler; import com.kuit.findyou.global.jwt.security.CustomAuthenticationEntryPoint; import com.kuit.findyou.global.jwt.filter.JwtAuthenticationFilter; import com.kuit.findyou.global.logging.MDCLoggingFilter; import lombok.RequiredArgsConstructor; +import org.springframework.boot.web.servlet.FilterRegistrationBean; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.http.HttpMethod; @@ -13,6 +15,7 @@ import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.http.SessionCreationPolicy; import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.access.ExceptionTranslationFilter; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; @@ -24,6 +27,7 @@ public class SecurityConfig { private final JwtAuthenticationFilter jwtAuthenticationFilter; private final CustomAuthenticationEntryPoint customAuthenticationEntryPoint; private final CustomAccessDeniedHandler customAccessDeniedHandler; + private final AdminAllowlistFilter adminAllowlistFilter; // MDCLoggingFilter 명시적 빈 등록 @Bean @@ -74,6 +78,8 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception{ http .addFilterBefore(mdcLoggingFilter(), JwtAuthenticationFilter.class); + http.addFilterAfter(adminAllowlistFilter, ExceptionTranslationFilter.class); + // 토큰 검증 예외 처리 추가 http .exceptionHandling(configurer -> configurer.authenticationEntryPoint(customAuthenticationEntryPoint) @@ -85,4 +91,10 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception{ return http.build(); } + @Bean + public FilterRegistrationBean adminAllowlistFilterRegistration(AdminAllowlistFilter filter) { + FilterRegistrationBean bean = new FilterRegistrationBean<>(filter); + bean.setEnabled(false); + return bean; + } }