프로젝트 개발 중, 관리자 권한이 필요한 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
으로 시작하는 엔드포인트에서 일관된 검증이 이루어져 코드 유지보수성이 높아졌습니다.이 리팩토링을 통해 관리자 권한이 필요한 기능에 대해 필터를 활용해 중앙에서 검증하는 방식을 채택함으로써, 코드 일관성 및 유지보수성을 크게 개선할 수 있었습니다.