카테고리 없음

@PreAuthorize()의 한계(Custom Annotation생성 + AOP적용)

Seung__Yong 2024. 1. 24. 22:42

1. 목표

  • 로그인에 실패했을 때 권한이 없을 때 구체적인 예외처리
    ex) "수정 권한이 없습니다.", "삭제 권한이 없습니다.", "로그인 후 이용할 수 있습니다."
  • 공통 처리(코드 중복 제거를 통한 가독성 향상)

2. @PreAuthorize()의 한계

  • PreAuthorize() 는 로그인 여부나 권한체킹하는 것을 도와줍니다. success or fail로 동작하며 인증/인가 실패 시AccessDeniedException을 반환합니다.
  • 여기서 문제점은 실패의 원인이 로그인을 안해서인지 권한이 없어서인지 
    등을 구분하기 어렵고 따로 구체적인 예외처리가 불가능합니다.
  • 권한 체킹을 할 때 preAuthorize로 Role에 대해서 체크할 수 있지만 자신의 게시글인지와 같은 추가적인 권한 검증이 필요할 수 있습니다.

3. 로그인 여부 검증(어느테이션 생성 후 AOP 적용)

  • 로그인 여부는 어노테이션을 만든 뒤 AOP를 적용시켜 로그인 여부를 확인하는 로직을
    공통화 합니다.

LoginChecking

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface LoginChecking {
}

 

LoginCheckingAop

@Aspect
@Component
@RequiredArgsConstructor
public class LoginCheckingAop {

    @Pointcut("@annotation(com.api.farmingsoon.common.annotation.LoginChecking)")
    private void enableLoginChecking(){}

    @Around("enableLoginChecking()")
    public void around(ProceedingJoinPoint joinPoint) throws Throwable {
        
        if(SecurityContextHolder.getContext().getAuthentication().getPrincipal().equals("anonymousUser"))
            throw new ForbiddenException(ErrorCode.NOT_LOGIN);
        
        joinPoint.proceed();
    }
}

 

4. 권한 체킹 공통 처리

권한 체킹도 컨트롤러에서 AOP를 적용할 수 있다면 좋겠지만 단순히 Role만 보는것이 아닌
자신의 게시글인지에 대해서도 확인하려면 서비스 단에서의 검증이 추가로 필요합니다.

 

ex) 아이템 수정 삭제를 할 때 아래와 같은 조건이 여러개 붙을 수 있음

1. Role은 Member or Admin
2. item.getMember().getName() == authentication.getName()

 

==> 자신의 상품이거나 관리자만이 삭제 가능

이를 검증하는 코드를 SecurityUtils에서 공통으로 처리하도록 했습니다.

 

개선 전(도메인 별로 checkDelete, checkUpdate같은 메서드가 중복됨)

    @Transactional
    public void delete(Long itemId) {
        Item item = itemRepository.findById(itemId).orElseThrow(() -> new NotFoundException(ErrorCode.NOT_FOUND_ITEM));
        checkDeletePermission(item.getMember());

        itemRepository.deleteById(itemId);
    }
    
 	public void checkDeletePermission(Member member){
    	if(!member.getName.equals(SecurityContext.getAuthentication.getName))
        	throw new ForbiddenException(ErrorCode.FORBIDDEN_DELETE);
        if(!member.getRole().getValue().equals(MemberRole.ADMIN))
        	throw new ForbiddenException(ErrorCode.FORBIDDEN_DELETE);
    }

개선 후

@Transactional
public void delete(Long itemId) {
    Item item = itemRepository.findById(itemId).orElseThrow(() -> new NotFoundException(ErrorCode.NOT_FOUND_ITEM));
    AuthenticationUtils.checkDeletePermission(item.getMember()); // service단 코드가 한 줄로 대체

    itemRepository.deleteById(itemId);
}

AuthenticationUtils

public class AuthenticationUtils {

    public static void checkUpdatePermission(Member member) {
        String authenticationMemberName = SecurityContextHolder.getContext().getAuthentication().getName();
        if(!authenticationMemberName.equals(member.getEmail()) || !member.getRole().getValue().equals(MemberRole.ADMIN))
        {
            throw new ForbiddenException(ErrorCode.FORBIDDEN_UPDATE);
        }
    }

    public static void checkDeletePermission(Member member) {
        String authenticationMemberName = SecurityContextHolder.getContext().getAuthentication().getName();
        if (!authenticationMemberName.equals(member.getEmail()) || !member.getRole().getValue().equals(MemberRole.ADMIN)) {
            throw new ForbiddenException(ErrorCode.FORBIDDEN_DELETE);
        }
    }
}