@PreAuthorize, @PostAuthorize
Pre/PostAuthorize 어노테이션을 이용해서 현재 로그인한 유저가 data를 생성한 주체(data owner)인지, ADMIN 권한을 가진 사용자인지 검사해보려고 합니다.
@PreAuthorize, @PostAuthorize는 SpEL(Spring Expression Language)을 사용해서 권한 검사를 지원하는 어노테이션입니다.
어노테이션을 메소드에 붙이면 메소드의 시작, 끝 시점에 주어진 조건식(SpEL)을 기반으로 권한을 검사합니다.
어노테이션에 매개변수로 담는 문자열에서 SpEL을 지원하기 때문에 @Secured 보다 세밀한 권한 검사가 가능합니다.
Request level 권한 인증 VS Method level 권한 인증
글을 읽게 되신 분들 중에서는 configuration class에 권한 설정을 하는 걸 생각하신 분도 있을 것 같습니다.
configuration class에서 HttpSecurity 인스턴스를 사용해서 설정하는게 Request level 권한 인증이고, Pre/PostAuthorize 어노테이션은 Method level 권한 인증을 진행합니다.
두 가지를 어떤 상황에 사용할 때 강점을 갖는지 Spring Security 공식 문서에 나와있습니다.
request level | method level | |
authorization type | 큰 범위로 인증 처리할 때 | 더 작은(세밀한) 범위로 인증 처리할 때 |
configuration location | config 클래스 | 메소드 선언부 |
configuration style | DSL(Spring Security 특화 언어) | Annotation(어노테이션) |
authorization definitions | programmatic(프로그래밍 방식) | SpEL |
HTTP 요청이 온 이후 바로 검사를 해서 걸러내는지, 아니면 비즈니스 로직에서 메서드 실행 중에 검사하는지의 차이입니다.
이 글은 어노테이션 기반 Method level 권한 인증 방식에 대해 사용해 본 후기입니다.
사용하기 위한 설정
Method level 어노테이션을 사용하려면 @EnableMethodSecurity 어노테이션을 @Configuration 어노테이션이 붙은 클래스에 붙여주어야 합니다.
Spring Security 관련 설정을 했던 SecurityConfig 클래스에 @EnableMethodSecurity 를 붙여주었습니다.
💡 @EnableGlobalMethodSecurity 어노테이션을 사용했을 때는 prePostEnabled옵션을 직접 활성화 해주어야 했지만 @EnableMethodSecurity 어노테이션을 사용하는 지금은 기본이 활성되어있는 상태입니다.
- prePostEnabled : 기본이 true.(Pre/PostAuthorize 어노테이션 활성화)
적용하기
현재 진행하고 있는 Pinterest clone 프로젝트에는 메소드 레벨에서 권한 체크가 필요한 몇 가지 기능이 있습니다.
🌟 Method level 권한 인증 필요 기능
- delete image
- delete reply
- delete save image
이 중에서도 이미지를 삭제할 때 권한을 검사하도록 어노테이션을 적용해보겠습니다.
이미지 삭제 시 권한 조건 추가
이미지를 삭제할 수 있는 유저의 권한 조건은 다음과 같습니다.
- 로그인 된 유저일 것.
- ADMIN 역할을 보유하거나 image owner(image를 생성한 user)일 것.
@Transactional
@PostAuthorize("(isAuthenticated() and (returnObject.getUserEmail() == principal.email))")
public ImageDeleteResponse deleteImage(int imageId) {
log.info("delete image");
Image image = imageRepository.findById(imageId);
validateImage(image);
deleteS3Image(image);
deleteSaveImage(image);
imageRepository.deleteImage(image);
return new ImageDeleteRepository(imageId, image.getUser().getEmail());
}
처음 생각했던 적용 위치는 ImageService 레이어였습니다. @PostAuthorize 어노테이션을 deleteImage 메서드에 붙여 이미지를 생성한 유저 email을 반환 값으로 받아 현재 로그인 된 유저 email과 비교하는 방법을 사용했습니다.
하지만 이 방법에는 문제가 있었습니다.
🌟 문제점
- 클라이언트에서 사용되지 않는 email 데이터를 권한 인증을 위해 반환합니다.
- DB에서 데이터가 삭제된 후 권한 검사를 수행해서 권한이 없는 유저라면 다시 데이터 삭제 연산이 취소되어야 하기에 불필요한 로직 실행이 이루어집니다.
- 트랜잭션이 제대로 실행되는지도 알 수 없습니다.
기능을 테스트해보니 이미지 데이터가 지워진 이후에 복구가 되지 않고 403에러가 반환됐습니다.
@PostAuthorize가 실행되는 시점에서는 이미 @Transactional 어노테이션이 붙은 메소드가 끝나고 난 이후이기 때문인 것으로 추측했습니다.
- 분명 공식 문서에서는 @Transactional, @PostAuthorize 어노테이션을 함께 붙여서 AccessDeniedException을 발생시켜서 rollback이 발생되도록 할 수 있다고 하던데..
@Transactional 어노테이션에 대해 제대로 이해하지 못한 부분이 있는 것으로 보여집니다.
정확히 @Transactional 단위가 적용되는 범위가 어디까지인지 알아보는 과정은 다음 글에서 다루어보겠습니다.
위의 문제들을 해결하기 위해 ImageRepository 레이어에 @PreAuthorize를 사용해서 권한 검사를 진행하기로 했습니다.
아래 코드처럼 처리될 경우 2, 3번 문제를 해결할 수 있습니다.
@PreAuthorize("isAuthenticated() and (principal.isDataOwner(#imageCreatorEmail) or principal.isAdmin())")
public void deleteImage(Image image, String imageCreatorEmail) {
em.remove(image);
}
기능을 테스트해보니 ADMIN 역할 유저가 로그인 했을 때는 삭제가 잘되는데, 이미지 생성자(data owner)가 이미지를 삭제하려고 하면 안되는 문제가 있었습니다.
로그를 찍어보며 확인해 본 결과 SpEL 표현식에 전달된 imageCreatorEmail 변수에 값이 없는걸 확인했습니다.
@P 어노테이션
공식문서를 보고 SpEL 표현식에서 사용하려는 변수 값은 등록이 필요한 것을 알게 되었습니다.
Spring Security에서는 DefaultSecurityParameterNameDiscoverer를 이용해서 메소드 매개변수를 검색해 SpEL 표현식에서 사용할 수 있는 기능을 제공해줍니다.
모든 매개변수를 사용할 수 있는 건 아니고, 메소드의 매개변수에 @P 어노테이션, Spring Data의 @Param이 붙어있으면 표현식에서 변수를 사용할 수 있습니다.
MethodSecurityEvaluationContext 가 생성될 때 DefaultSecurityParameterNameDiscoverer 가 생성됩니다.
MethodSecurityEvaluationContext의 역할은 아래의 동작과정 내용 중에 써있습니다.
SpEL 표현식에서는 기본적으로 SecurityContext 안의 Authentication 객체의 값을 사용할 수 있습니다.
위의 생성자 코드를 보면 Authentication, MethodInvocation 객체 또한 MethodSecurityEvaluationContext 가 생성될 때 매개변수로 전달되어 표현식에서 사용할 수 있는 것임을 할 수 있습니다.
@PreAuthorize("isAuthenticated() and (principal.isDataOwner(#imageCreatorEmail) or principal.isAdmin())")
public void deleteImage(Image image, @P("imageCreatorEmail") String imageCreatorEmail) {
em.remove(image);
}
@P 어노테이션을 통해 SpEL 표현식에 데이터를 전달했고, 직접 구현한 CustomUserDetails(Principal) 클래스에서 권한을 검사하는 메소드를 추가해서 isDataOwner, isAdmin 메소드를 통해 권한 검사를 진행하도록 짜주었습니다.
Method Security 동작 과정
Method Security는 Spring AOP를 사용해서 구축되었다고 합니다.
String AOP는 @PreAuthorize 어노테이션이 붙은 deleteImage 메소드의 프록시 메소드를 호출합니다.
어노테이션이 붙은 point cut(advice가 적용될 지점)을 찾아와서 AuthorizationManagerBeforeMethodInterceptor를 생성합니다. → AuthorizationMethodPointcuts.forAnnotations(new Class[]{PreAuthorize.class})
각 어노테이션마다 사용하는 메소드 인터셉터가 있는데, Spring Security에서 @PreAuthorize 어노테이션은 AuthorizationManagerBeforeMethodInterceptor를 사용합니다.
AuthorizationManagerBeforeMethodInterceptor는 PreAuthorizeAuthorizationManager의 check 메소드를 호출합니다.
check 메소드를 살펴보면 MethodSecurityExpressionHandler가 입력받은 SpEL 표현식의 구문을 파싱합니다.
SecurityExpressionHandler 인터페이스를 구현하는 DefaultMethodSecurityExpressionHandler 의 createEvaluationContext 메소드가 실행됩니다.
내부 코드를 살펴보면 createSecurityExpressionRoot 메소드를 실행해서 MethodSecurityExpressionRoot 인스턴스를 생성하는 것을 볼 수 있습니다.
Spring Security에서 모든 인증 필드와 메소드를 캡슐화해서 만든 객체가 MethodSecurityExpressionRoot 의 인스턴스라고 합니다.
그리고 MethodSecurityExpressionRoot 인스턴스는 EvaluationContext를 만들때 사용됩니다.
EvaluationContext는 MethodSecurityEvaluationContext 인스턴스로 이 값이 반환됩니다.
EvaluationContext는 권한을 평가할 때 사용됩니다.
반환받은 EvaluationContext와 실제 입력한 표현식(expression)을 통해 권한 인증을 진행합니다. 인증된 결과를 바탕으로 ExpressionAuthorizationDecision 인스턴스를 생성해 반환합니다. 권한 인증에 성공하면 granted값은 true입니다.
다시 check 메소드를 호출했던 AuthorizationManagerBeforeMethodInterceptor의 attemptAuthorization 메소드로 돌아가 봅시다.
check 메소드의 결과 값으로 AuthorizationDecision 객체를 받습니다.
그리고 publishAuthorizationEvent 메소드에 매개변수로 객체를 넘겨 이벤트를 발행합니다.
AuthorizationDecision객체를 통해 권한 인증이 되었는지 확인하고 인증이 되지 않았다면 AccessDeniedException이 발생하는데 Exception을 처리하는 역할은 ExceptionTranslationFilter에서 잡아서 http status 403의 결과를 클라이언트에서 반환합니다.
권한인증에 성공했다면 Spring AOP는 deleteImage 메소드를 실행할 것입니다.
모든 과정은 Spring Security의 공식 문서와 실제 코드를 디버깅 해본 후 작성했습니다.
잘못된 부분이 있다면 댓글로 남겨주시면 감사드리겠습니다.
자세한 구조 그림은 Spring Security 공식 사이트에 나와있으니 참고해보시면 좋을 것 같습니다.
참고자료
Method Security :: Spring Security
6. Spring Expression Language (SpEL)
'Spring' 카테고리의 다른 글
Spring boot 프로젝트에 Sentry 연결하기 (0) | 2024.06.12 |
---|