관리자 권한 검증 로직의 분산 문제 리팩토링

coldrice99·2024년 11월 7일
0
post-thumbnail

트러블 슈팅 TIL: 관리자 권한 검증 로직의 리팩토링 결정

문제 상황

프로젝트 개발 중, 관리자 권한이 필요한 API의 접근 권한을 검증하는 로직이 서비스인가 필터에 분산되어 있다는 피드백을 받았습니다. 이로 인해 코드의 일관성이 떨어지고, 관리 기능이 추가될수록 검증 로직이 각기 다른 위치에서 처리될 가능성이 있었습니다.

특히, 현재 전체 회원 조회 기능을 위한 getAllMemberInfo 메서드에 관리자 권한을 검증하는 if문이 추가되어 있었는데, 이 조건문을 필터로 이동시켜 엔드포인트 단위에서 권한을 일관되게 검증하는 구조로 변경할지 고민하게 되었습니다.

초기 서비스 로직 검토

getAllMemberInfo 메서드의 초기 코드에서 권한 검증 로직은 다음과 같이 서비스 내에 단순 if문으로 구현되어 있었습니다:

public List<AdminMemberInfoResponse> getAllMemberInfo(int page, int size) {
    if (!userDetails.getMemberRole().equals(MemberRole.ADMIN)) {
        throw new MemberException("관리자 권한이 필요한 기능입니다.");
    }

    Pageable pageable = PageRequest.of(page, size);
    return memberRepository.findAll(pageable).stream()
            .map(member -> new AdminMemberInfoResponse(
                    member.getEmail(),
                    member.getPassword(),
                    member.getNickname(),
                    member.getPhoneNumber(),
                    member.getMemberRole(),
                    member.getMemberStatus(),
                    member.getCreatedAt(),
                    member.getUpdatedAt(),
                    member.getKakaoId()
            ))
            .collect(Collectors.toList());
}

리팩토링 고려사항

  • 서비스에 권한 검증을 유지하면 코드가 간결해지며, 당장 필요한 검증 로직만을 추가할 수 있었습니다.
  • 그러나, 필터로 검증 로직을 통합하면 유지보수성과 확장성이 높아집니다. 필터를 통해 관리자 권한이 필요한 엔드포인트를 통합 관리하면, 앞으로 관리 기능이 추가될 때도 일관성 있는 검증이 가능해집니다.

결국, 유지보수성과 확장성을 고려해 인가 필터에 검증 로직을 통합하는 방법을 선택했습니다.

인가 필터 리팩토링

선택한 방식에서는 JwtAuthorizationFilter에 두 개의 관리자 검증 메서드if문을 추가하여 권한을 검증합니다. 이를 통해 /api/admin으로 시작하는 모든 엔드포인트에서 일관된 관리자 권한 검증을 진행합니다.

@Slf4j(topic = "JWT 검증 및 인가")
@RequiredArgsConstructor
public class JwtAuthorizationFilter extends OncePerRequestFilter {

    private final JwtUtil jwtUtil;
    private final UserDetailsService userDetailsService;

    @Override
    protected void doFilterInternal(HttpServletRequest request,
                                    HttpServletResponse response,
                                    FilterChain filterChain) throws ServletException, IOException {
        String tokenValue = jwtUtil.getJwtFromHeader(request);

        if (StringUtils.hasText(tokenValue)) {
            if (!jwtUtil.validateToken(tokenValue)) {
                log.error("Token Error");
                sendErrorResponse(response, HttpServletResponse.SC_UNAUTHORIZED, "유효하지 않은 토큰입니다.");
                return;
            }

            Claims info = jwtUtil.getMemberInfoFromToken(tokenValue);

            // ADMIN 권한이 필요한 경우 권한 검증
            if (requiresAdminRole(request) && !isAdmin(info)) {
                log.error("접근 권한이 없습니다.");
                sendErrorResponse(response, HttpServletResponse.SC_FORBIDDEN, "접근 권한이 없습니다. : 관리자 권한 필요");
                return;
            }

            setAuthentication(info.getSubject());
        }
        filterChain.doFilter(request, response);
    }

    private void sendErrorResponse(HttpServletResponse response, int statusCode, String message) throws IOException {
        response.setStatus(statusCode);
        response.setContentType("application/json;charset=UTF-8");
        response.getWriter().write(String.format("{\"error\": \"%s\"}", message));
        response.getWriter().flush();
        response.getWriter().close();
    }

    // ADMIN 권한 검증 메서드
    private boolean isAdmin(Claims info) {
        String role = info.get("auth", String.class);
        return MemberRole.ADMIN.name().equals(role);
    }

    // 특정 요청이 ADMIN 권한이 필요한지 확인하는 메서드
    private boolean requiresAdminRole(HttpServletRequest request) {
        String uri = request.getRequestURI();
        return uri.startsWith("/api/admin");
    }

    private void setAuthentication(String email) {
        SecurityContext context = SecurityContextHolder.createEmptyContext();
        Authentication authentication = createAuthentication(email);
        context.setAuthentication(authentication);
        SecurityContextHolder.setContext(context);
    }

    private Authentication createAuthentication(String email) {
        UserDetails userDetails = userDetailsService.loadUserByUsername(email);
        return new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
    }
}

리팩토링 후 장점

  • 일관성 확보: 필터를 통해 /api/admin으로 시작하는 엔드포인트에서 일관된 검증이 이루어져 코드 유지보수성이 높아졌습니다.
  • 확장성: 향후 관리 기능이 추가될 때도 엔드포인트 URI 패턴을 통해 일관된 검증을 적용할 수 있습니다.

결론

이 리팩토링을 통해 관리자 권한이 필요한 기능에 대해 필터를 활용해 중앙에서 검증하는 방식을 채택함으로써, 코드 일관성 및 유지보수성을 크게 개선할 수 있었습니다.

profile
서두르지 않으나 쉬지 않고

0개의 댓글