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);
}
}
}