개발 낙서장

[Spring] AOP로 권한 체크하기 본문

Java

[Spring] AOP로 권한 체크하기

권승준 2024. 3. 21. 20:07

스프링 AOP

스프링 AOP는 관심사를 분리하고 모듈화하는 프로그래밍 기법(관점 지향 프로그래밍)으로 스프링 프레임워크에서 제공한다.

Aspect로 정의된 클래스 내부에서 특정 시점에 특정 로직을 수행하게 한다.

현재 진행하고 있는 팀 프로젝트의 ERD의 일부이다. 유저와 보드가 존재하고 보드에는 사용자를 초대하는 기능이 있어 N:N 관계이기 때문에 BoardUser 테이블을 추가로 만들어 보드 권한을 관리하고 있다.

권한 체크

기존에는 각 서비스 로직마다 BoardUser에 접근해 해당 유저가 해당 보드에 권한을 갖고 있는지 체크한 후 로직을 진행했다.

    @Transactional
    public void createComment(Long cardId, Long boardId, CommentRequest request, User user) {
        workspaceUser(user, boardId);
		
        // Comment 추가 로직 ...
    }    
    
    public void workspaceUser(User user, Long bordId) {
        Optional<BoardUser> board = boardUserJpaRepository.findById(bordId);
        
        // BoardUser에 없다면 예외 throw ...
    }

이렇게 보드에 초대되지 않은 유저가 보드 내에서 작업(보드 수정, 삭제, 컬럼 추가 등)을 진행하려고 하면 예외를 던져 사전에 방지한다.

하지만 이런 방식으로 구현할 경우 검증 로직이 바뀔 경우 모든 서비스 메소드를 찾아 바꿔줘야 하기에 유지보수 측면에서 매우 나쁘다.

그래서 스프링 AOP를 활용해 검증이 필요한 서비스 로직이 실행되기 전에 검증을 진행한 후 서비스 로직이 진행되도록 했다.

스프링 AOP 적용

먼저 BoardUser에서 데이터를 찾아야 하므로 BoardUserRepository를 주입받는다.
AOP는 Spring Component로 등록되기에 다른 Bean을 주입받을 수 있다.

@Aspect
@Component
@RequiredArgsConstructor
public class BoardUserValidateAspect {

    private final BoardUserJpaRepository boardUserJpaRepository;
}

그리고 PointCut을 지정해줘야 한다. 보드 권한 검증이 필요한 부분은 유저 서비스와 보드 생성 메소드를 제외한 모든 서비스 메소드에서 필요하다.

    @Pointcut("execution(* com.sparta.trello.domain.board.service.*.*(..)) "
        + "&& !execution(* com.sparta.trello.domain.board.service.BoardService.createBoard(..))")
    private void boardTransaction() {
    }

    @Pointcut("execution(* com.sparta.trello.domain.card.service.*.*(..))")
    private void cardTransaction() {
    }

    @Pointcut("execution(* com.sparta.trello.domain.column.service.*.*(..))")
    private void columnTransaction() {
    }

    @Pointcut("execution(* com.sparta.trello.domain.comment.service.*.*(..))")
    private void commentTransaction() {
    }

마지막으로 권한 검증 로직을 구현했다.
유저 인증 및 인가는 Spring Security Filter에서 JWT 검증을 통해 이뤄지는데 인증 및 인가에 성공하면 헤더에 추가되고 SecurityContext에 인증 객체가 등록된다.
따라서 SecurityContextHolder에서 인증 객체를 가져와 유저 정보를 추출하고 JointPoint를 통해 현재 접근한 메소드에서 boardId를 가져와 검증하게 된다.

    @Before("boardTransaction() || cardTransaction() || columnTransaction() || commentTransaction()")
    private void validateBoardUser(JoinPoint joinPoint) {
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        User user = ((UserDetailsImpl) authentication.getPrincipal()).getUser();

        long boardId = 0L;

        MethodSignature methodSignature = ((MethodSignature) joinPoint.getSignature());
        if (!methodSignature.getMethod().getName()
            .equals("createBoard")) {
            String[] parameterNames = methodSignature.getParameterNames();
            Object[] args = joinPoint.getArgs();
            if (args.length < 1) {
                log.info("No Parameter");
                return;
            } else {
                for (int i = 0; i < parameterNames.length; i++) {
                    if (parameterNames[i].equals("boardId")) {
                        boardId = (long) args[i];
                        break;
                    }
                }
            }

            workspaceUser(user, boardId);
        }
    }

    public void workspaceUser(User user, Long boardId) {
        BoardUser boardUser = boardUserJpaRepository.findByBoardIdAndUserId(boardId, user.getId());
        if (boardUser == null) {
            throw new NoSuchElementException("워크스페이스 권한이 없는 사용자입니다.");
        }
    }

디버깅을 해보면 MethodSignature에 메소드의 이름, 반환 타입, 파라미터의 이름, 타입 등의 정보가 들어있기 때문에 어떤 메소드에서 어떤 파라미터들이 있는지 알 수 있다.

    @Transactional
    public ColumnResponse createColumn(Long boardId, CreateColumnRequest request) {
        Board board = validateExistBoard(boardId);

        Columns createdColumn = columnRepository.save(
            Columns.builder().columnName(request.getColumnName()).sequence(
                request.getSequence()).board(board).build());

        return ColumnResponse.builder()
            .columnId(createdColumn.getColumnId())
            .columnName(createdColumn.getColumnName())
            .sequence(createdColumn.getSequence())
            .build();
    }

현재 컬럼을 생성하는 createColumn에는 따로 유저 권한을 검증하는 로직이 없지만 스프링 AOP로 인해

권한이 없는 사용자는 예외로 인해 작업을 수행할 수 없게 된다.

하지만 보드에 초대된 유저는 정상적으로 작업을 진행할 수 있다.

스프링 AOP로 이런 공통 로직을 처리하면 유지 보수 측면에도 매우 유리하고 이곳저곳 퍼져있는 공통 로직을 한 곳에 모아두기 때문에 코드 가독성도 올라가게 된다.
물론 무조건 공통으로 처리하는 것이 능사는 아니기에 적절한 때에 상황에 맞게 잘 활용해야 할 것 같다.

'Java' 카테고리의 다른 글

JDK 17을 쓰는 이유  (1) 2024.05.02
[Spring] WebSocket 통신  (0) 2024.03.28
[Spring] QueryDSL 페이징  (0) 2024.03.12
Name 필드 네이밍에 대한 고찰  (0) 2024.03.05
[Spring] @DataJpaTest 사용 시 UnsatisfiedDependencyException 발생  (0) 2024.02.20
Comments