-
Notifications
You must be signed in to change notification settings - Fork 1
[Refactor] 내부 자동화를 위한 관리자용 로그인 API 추가 (장기 refresh 토큰 기반) #161
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 21 commits
33b16b8
879fe81
91c6b73
d6ca881
b719b48
6415657
848cc4c
243f444
cb8d305
dca8923
ca4909e
ecf97e3
274e51a
284159f
51f4224
53d27a8
7f4d66a
a8f2b1c
40de914
3b12825
64b4e4b
c25b0f4
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,12 @@ | ||
| package com.kuit.findyou.domain.auth.dto.response; | ||
|
|
||
| import io.swagger.v3.oas.annotations.media.Schema; | ||
|
|
||
| @Schema(description = "관리자 로그인 응답 DTO") | ||
| public record AdminLoginResponse( | ||
| @Schema(description = "관리자 유저 식별자") | ||
| Long userId, | ||
|
Comment on lines
+7
to
+8
Collaborator
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. 식별자를 반환하는 이유가 있나요?
Collaborator
Author
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. 지금은 관리자 허용 액션이 한정되어있어서 userID 를 필요로하지는 않지만, 추후 관리자 기능이 늘어날 것을 대비했을 때 확장성이 더 좋다고 생각합니다. 이 외에도, 관리자 유저가 둘 이상이 되는 경우 디버깅도 유리할 것 같네요.
Collaborator
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. 개인적으로는 API 사용자에게 당장 필요하지 않은 정보는 제공하지 않는 게 낫지 않나 싶지만 알겠습니다...!
Collaborator
Author
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. 디버깅 측면에서도 식별자가 불필요하다고 생각하시나요? |
||
| @Schema(description = "엑세스 토큰") | ||
| String accessToken | ||
| ) { | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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(); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,36 @@ | ||
| package com.kuit.findyou.domain.auth.service; | ||
|
|
||
| import com.kuit.findyou.domain.auth.dto.response.AdminLoginResponse; | ||
| 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 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; | ||
|
|
||
| @Value("${admin.admin-user-id}") | ||
| private Long adminUserId; | ||
|
|
||
| @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(), Role.ADMIN, adminAccessTtlMs); | ||
|
|
||
| return new AdminLoginResponse(user.getId(), accessToken); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 { | ||
|
Comment on lines
+17
to
+21
Collaborator
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. 필터를 @component로 등록하셨더라고요. AccessDeniedException은 시큐리티의 ExceptionTranslationFilter에서 잡기 때문에 이 필터 뒤에서 예외를 던져야 해요. 지금 방식처럼 필터를 등록하면 서블릿 컨테이너가 ExceptionTranslationFilter -> AdminAllowListFilter 같은 순서를 보장하지는 않아요. 일단 제 로컬에서는 필터가 우리가 기대한대로 등록되어서 잘 돌아가긴 합니다. 근데 잠재적인 오작동 가능성을 생각하면 시큐리티 설정에서 ExceptionTranslationFilter 뒤에 AdminAllowListFilter를 추가하거나, AuthorizationManager를 등록하는 방법이 있다고 하네요. 저는 급한대로 전자를 추천합니다..!
Collaborator
Author
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. 추가하겠습니다. |
||
|
|
||
| private final AntPathMatcher matcher = new AntPathMatcher(); | ||
|
|
||
| // ADMIN에게 허용되는 API | ||
| private static final List<Allow> 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; | ||
| } | ||
| } | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.
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.
서버 간에 인증을 위해서 client credentials라는 방법을 사용하기도 하던데 이렇게 구현하신 이유가 있나요?
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.
외부 다수 클라이언트가 붙는 구조가 아니라 내부 자동화 주체 1개만 인증하면 되는 요구사항이라, 인증 인프라를 과도하게 키우기보다는 로그인 단계에서만 API Key를 사용하고, 이후에는 짧은 만료의 access token으로 요청을 처리해 보안 수준은 유지하면서 구현과 운영 복잡도를 낮추는 방식이 적절하다고 생각했습니다.
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.
보안 관점에서는 아쉽지만 그렇게 생각하신다면 알겠습니다