이전 글에서 서비스 레이어에 중복되는 인가 로직을 AoP 로 분리해 주었다.
다른 방법이 없나 찾아보던중@PreAuthorize
라는 어노테이션에 대해 알게되었고 이를 적용해 본 경험을 기록한 글이다.
인가를 처리할 수 있는 다른 방법을 찾아보다 @PreAuthorize
어노테이션을 알게되었다.
AoP로 동작하며 작성한 표현식을 평가해 권한이 있다면 진행을 하고, 권한이 없다면 예외를 던지게 된다.
@PreeAuthorize
와 @PostAuthorize
가 붙은 readCustomer
라는 메서드를 실행시킬때 구조는 아래와 같다.
메서드가 호출이 되면, 어노테이션으로 포인트컷을 잡은 Advice를 호출하고, @PreAuthorize
의 구문을 평가해 권한을 확인한다. 만약 권한이 있다면 통과하고, 없다면 예외를 던지는 식으로 동작한다.
표현식은 SePL을 사용해 작성할 수 있다.
메서드의 권한을 세밀하게 제어해야 할때 사용한다.
인증이 된 사용자는 필터를 넘고, 디스패처 서블릿을 넘어 컨트롤러로 도착한다.
하지만 인증이 된 사용자라 하더라도 아무 자원에 접근할 수 있는건 아니다.
역할에 따라 접근이 제한될 수 있고, 자원의 소유권한에 따라 접근이 제한될 수 있다.
@PreAuthorize
는 이렇게 세밀한 인가 처리에 사용된다.
나는 이전 글 에서 사용했던 AoP 기반 인가 로직 처리를 대신하기 위해 사용했다.
내가 개발한 AoP
대신 @PreAuthorize
를 사용하게 된 이유는 현재 로그인한 유저 ID를 파라미터로 받을 필요가 없어서 였다.
최초 컨트롤러에서 유저 ID를 받은 이유는 자원의 인가 처리를 위해서 였다.
서비스 레이어에서 인가 처리를 할 수 있도록, 컨트롤러에서 서비스로 유저 ID를 전달해 주었고, 이 과정에서 최대한 편하게 전달받기 위해 현재 로그인한 유저 ID를 받을 수 있는 어노테이션을 만들어 사용했다.
하지만 @PreAuthorize
를 사용한다면 현재 로그인한 사용자 정보를 표현식에서 접근할 수 있고 굳이 유저 ID
를 따로 받을 필요가 없어진다.
@PreAuthorize("authentication.name == #username")
위와같이 authentication
라는 인증 정보를 표현식에서 바로 사용할 수 있다. (#username
은 파라미터임)
즉 @Controller
에서 UserId
를 @Service
로 내려줄 필요가 없어지므로 불필요한 파라미터를 줄일 수 있어 가독성과 유지보수가 좋아진다.
@Transactional
public void deleteAddress(Long addressId, Long userId) {
Address address = getAddress(addressId);
if (address.getUser().getId() != userId) {
log.error(ErrorCode.UNAUTHORIZED.getLogMessage(), "address", addressId, userId);
throw JshopException.of(ErrorCode.UNAUTHORIZED);
}
address.delete();
}
@Transactional
public void deleteAddress(Long addressId) {
Address address = getAddress(addressId);
address.delete();
}}
@DeleteMapping("/{id}")
@PreAuthorize("isAuthenticated() && @addressService.checkAddressOwnership(authentication.principal, #addressId)")
public void deleteAddress(@PathVariable("id") @P("addressId") Long addressId) {
addressService.deleteAddress(addressId);
}
@PreAuthorize
메서드를 보면 @addressService.checkAddressOwnership
라는 부분이 존재한다.
해당 표현식은 addressService
라고 등록된 빈을 찾아 checkAddressOwnership
메서드를 실행하라는 의미다.
파라미터로 전달된 authentication.principal
는 현재 인증된 유저 정보를 말하며, #addressId
는 메서드의 파라미터로 전달된 Long addressId
를 말한다.
이때 메서드의 파라미터를 @PreAuthorize
로 전달하기 위해서는 @P("파라미터 이름")
으로 명식적으로 전달해야 하는것 같다. 저 부분이 없으면 null로 전달된다.
public boolean checkAddressOwnership(UserDetails userDetails, Long addressId) {
CustomUserDetails customUserDetails = (CustomUserDetails) userDetails;
Long userId = customUserDetails.getId();
Address address = getAddress(addressId);
if (address.getUser().getId() == userId) {
return true;
}
log.error(ErrorCode.UNAUTHORIZED.getLogMessage(), "Address", addressId, userId);
throw JshopException.of(ErrorCode.UNAUTHORIZED);
}
인가 로직은 결국 기존 서비스 레이어에 있던 회원의 인가 로직을 옮긴것 뿐이다.
스프링 시큐리티가 생각보다 많은 기능을 제공해준다는 점을 알게되었다.
특히 메서드 단위의 접근 제어까지 지원해주는것을 알게되었다.
리팩토링을 해가며 점점 객체지향에 대해 알아가는것 같다. 처음에는 서비스 레이어에 비즈니스 로직뿐만 아니라 온갖 로직들이 들어가 있었는데, 이를 천천히 제거하며 리팩토링 하다보니 점점 객체지향의 매력이 뭔지 알 것 같다.