1. 개요
스프링 시큐리티는 method level에서도 접근 제어가 가능하며 Service layer 에서의 보안을 강제할 수 있다.
`@PreAuthorize` `@PostAuthroize` `@PreFilter` `@PostFilter`
@Configuration
@EnableMethodSecurity(prePostEnabled = true, securedEnabled = true)
public class MethodSecurityConfig {
}
2. 사용
- Spring AOP 이용하여 동작하며, 다음과 같이 사용할 수 있다.
@Service
public class MyCustomerService {
@PreAuthorize("hasAuthority('permission:read')")
@PostAuthorize("returnObject.owner == authentication.name")
public Customer readCustomer(String id) { ... }
}
1. @PreAuthorize 실행지점을 찾음
2-4. 인증되었는지 체크 후 SpEL 표현식을 통해 권한 리스트에서 표현식에 부합하는 권한이 있는지 체크
5. 표현식에 부합하면 Spring AOP는 이를 실행함
6. 그렇지 않다면 `AccessDeniedException` 예외 발생 및 403 에러
7. 메서드 수행 후 Spring AOP는 @PostAuthorize 포인트컷 실행지점을 찾음
8. @PostAuthorize의 SpEL 표현식 검증을 하고 이를 통과한다면 메서드 실행 결과를 반환한다.
9. 그렇지 않으면 함수는 수행되었으나 AccessDeniedException이 발생하고 실행결과 대신 403 에러를 발생 시킨다.
3. SpEL 표현식
a) permitAll - 인증 없이 모두 허용
b) denyAll - 모두 접근 불가
c) hasAuthority - 인증되었고, GrantedAuhority에 부여된 권한 중 주어진 권한 값과 일치하는 권한만 허용
d) hasRole - 'ROLE_' 로 시작하는 권한 중 hasRole에 지정된 것과 일치하는 권한만 허용
e) hasAnyAuthority - 권한 하나라도 있으면 허용
f) hasAnyRole - 주어진 ROLE 중 하나라도 있으면 허용
@Component
public class MyService {
-- 누구든 접근 불가
@PreAuthorize("denyAll")
MyResource myDeprecatedMethod(...);
-- ROLE_ADMIN만 허용
@PreAuthorize("hasRole('ADMIN')")
MyResource writeResource(...)
-- GrantedAuthority 중 'db'를 가지고 있으며 ROLE_ADMIN만 허용
@PreAuthorize("hasAuthority('db') and hasRole('ADMIN')")
MyResource deleteResource(...)
-- 유저객체 principal의 aud claim이 'my-audience'인 경우는 허용
@PreAuthorize("principal.claims['aud'] == 'my-audience'")
MyResource readResource(...);
-- authz 이름을 가진 컴포넌트(bean)의 check 메서드의 결과가 true여야 허용
@PreAuthorize("@authz.check(authentication, #root)")
MyResource shareResource(...);
}
4. Method Security Annotation
a) @PreAuthorize
- 메서드 수행 전 권한을 체크한다.
@Component
public class BankService {
@PreAuthorize("hasRole('ADMIN')")
public Account readAccount(Long id) {
// ROLE_ADMIN 권한으로 인증된 경우에만 수행가능
}
}
- 테스트는 @WithMockUser를 이용한다.
@Autowired
BankService bankService;
@WithMockUser(roles="ADMIN")
@Test
void readAccountWithAdminRoleThenInvokes() {
Account account = this.bankService.readAccount("12345678");
// ... assertions
}
@WithMockUser(roles="WRONG")
@Test
void readAccountWithWrongRoleThenAccessDenied() {
assertThatExceptionOfType(AccessDeniedException.class).isThrownBy(
() -> this.bankService.readAccount("12345678"));
}
b) @PostAuthorize
- 메서드 수행 후 값을 반환하기 전에 검증한다.
@Component
public class BankService {
@PostAuthorize("returnObject.owner == authentication.name")
public Account readAccount(Long id) {
// 반환된 Account의 owner가 현재 인증된 사용자의 name과 일치해야만 값을 리턴해준다.
}
}
- 보통 위 SpEL 표현식은 별도의 meta annotation을 만들기도 한다.
@Target({ ElementType.METHOD, ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
@PostAuthorize("returnObject.owner == authentication.name")
public @interface RequireOwnership {}
- 마찬가지로 테스트는 아래와 같다.
@Autowired
BankService bankService;
@WithMockUser(username="owner")
@Test
void readAccountWhenOwnedThenReturns() {
Account account = this.bankService.readAccount("12345678");
// ... assertions
}
@WithMockUser(username="wrong")
@Test
void readAccountWhenNotOwnedThenAccessDenied() {
assertThatExceptionOfType(AccessDeniedException.class).isThrownBy(
() -> this.bankService.readAccount("12345678"));
}
c) @PreFilter
- 메서드로 들어온 매개변수에 대해 어노테이션 내 조건식을 검증한다.
@Component
public class BankService {
@PreFilter("filterObject.owner == authentication.name")
public Collection<Account> updateAccounts(Account... accounts) {
// 매개변수 accounts 내의 owner와 인증된 유저 name과 비교한다.
// Collections, Stream, Map도 가능하다.
return updated;
}
}
d) @PostFilter
- 반환되는 내용들(Collectionsm, Map, List 등등)에 대해 어노테이션 내 조건식으로 검증한다.
@Component
public class BankService {
@PostFilter("filterObject.owner == authentication.name")
public Collection<Account> readAccounts(String... ids) {
// 반환되는 컬렉션 객체 accounts 순회하면서 인증된 사용자 이름과 같은지 확인한다.
return accounts;
}
}
5. Custom Annotation
- 어노테이션으로 직접 만들어 조금 더 직관적으로 표현이 가능하다.
- 아래는 관리자만 수행 가능한 메서드에 대해 직접 정의한 예제이다.
@Target({ ElementType.METHOD, ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
@PreAuthorize("hasRole('ADMIN')")
public @interface IsAdmin {}
@Component
public class BankService {
@IsAdmin
public Account readAccount(Long id) {
}
}
6. 적용
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@PreAuthorize("@clubAuth.decide(#root, T(com.tofu.club.enums.ClubRole).MANAGER.name())")
public @interface IsManager {
}
- 위와 같이 어노테이션을 만들었다.
- 메서드 실행 전 ClubRole이 Manager인지 여부를 검사하는 어노테이션이다.
@Component("clubAuth")
public class ClubAuthorizationLogic {
public boolean decide(MethodSecurityExpressionOperations root, ClubStatus clubStatus) {
var userPrincipal = (CustomUserDetails) root.getAuthentication().getPrincipal();
var clubAuthorizationList = userPrincipal.getClubAuthorizationList();
// .. 중략 ..
return decision;
}
}
- 해당 권한을 갖고 있거나 혹은 그 이상인지 체크하는 로직을 두었다.
@PostMapping("/api/clubs")
@IsManager
@Operation(summary = "동호회장, 총무만 사용 가능한 API")
public BaseHttpResponse<String> addClub() {
return BaseHttpResponse.success(clubService.addClub());
}
- 어노테이션을 적용하였고 동호회장과 총무가 아니라면 해당 메서드는 접근이 불가능하다.
Spring Security 5 버전이지만 개념 참고하기 좋았다. [토리맘의 한글라이즈 프로젝트 - 스프링 시큐리티 5]
https://godekdls.github.io/Spring%20Security/contents/
'Spring' 카테고리의 다른 글
Spring Boot + Nuxt.js 환경에서의 FCM 웹 푸시 구현 (1/2) - Spring Boot편 (0) | 2023.04.15 |
---|---|
[Mybatis] Mybatis에서 DTO로 분리하기 (0) | 2023.03.16 |
@Transactional Annotation 정리 (0) | 2022.09.13 |
빈 등록 어노테이션 @Configuration, @Component, @Bean에 대해서 (2) | 2022.08.27 |
세션 vs JWT (0) | 2022.06.21 |