spring security에서 기본적으로 제공하는 권한 설정 방법은 2 가지가 있다.
http
.authorizeHttpRequests((authorize) -> authorize
.requestMatchers("/resource/{name}").access(new WebExpressionAuthorizationManager("#name == authentication.name"))
.anyRequest().authenticated()
)
@Service
public class MyCustomerService {
@PreAuthorize("hasAuthority('permission:read')")
@PostAuthorize("returnObject.owner == authentication.name")
public Customer readCustomer(String id) { ... }
}
기본적으로 Authorize HTTP Request 방법으로 대부분의 권한 설정을 할 수 있지만 Method Security 방법이 더 유용할 때가 있다.
Method Security는 기본적으로 Spring AOP기반으로 구축 되어 있다.
Method Security를 시작하려면 @Configuration
이 있는 곳에 @EnableMethodSecurity
를 추가하면 된다.
@EnableMethodSecurity
@Configuration
public class SecurityConfig {
public SecurityFilterChain config(HttpSecurity http) throws Exception{
return http.build();
}
}
1. #readCustomer
함수를 호출하면 Spring AOP가 @PreAuthorize
의 pointcut과 일치하는 advisor인 AuthorizationManagerBeforeMethodInterceptor
를 호출합니다.
2. AuthorizationManagerBeforeMethodInterceptor
는 PreAuthorizeAuthorizationManager#check
를 호출 합니다.
3. PreAuthorizeAuthorizationManager#check
는MethodSecurityExpressionHandler
를 호출하여 SpEL 구문식을 분석합니다. SpEL 구문식을 처리하는 EvaluationContext
를 생성하면서 분석한 구문을 처리하는 것과 일치하는 MethodSecurityExpressionRoot
를 생성하여 넘겨줍니다.
ex) hasRole에 경우 SecurityExpressionRoot를 생성해 EvaluationContext에 넘겨준다.
4. EvaluationContext
를 이용하여 SpEL 구문식이 올바른지 평가한다.
5. 평가를 통과하면 Sprin AOP는 target method를 호출한다.
6. 그렇지 않는 경우, 인터셉터는 AuthorizationDeniedEvent
를 발생시키고 AccessDeniedException
예외를 던진다. ExceptionTranslationFilter
가 catch하여 403 status code를 response에 리턴한다.
7. method가 리턴되면 Spring AOP가 @PostAuthorize
의 pointcut과 일치하는 advisor인 AuthorizationManagerBeforeMethodInterceptor
를 호출한고 이 인터셉터는PostAuthorizeAuthorizationManager#check
호출한다.
8. 통과시 일반적인 과정으로 진행된다.
9. 그렇지 않는 경우, 인터셉터는 AuthorizationDeniedEvent
를 발생시키고 AccessDeniedException
예외를 던진다.ExceptionTranslationFilter
가 catch하여 403 status code를 response에 리턴한다.
method 실행전 권한을 평가한다.
public class BankService {
@PreAuthorize("hasRole('ADMIN')")
public Account readAccount(Long id) {
// ... is only invoked if the `Authentication` has the `ROLE_ADMIN` authority
}
}
method의 parameter를 평가하는데 사용할 수 있다.
@PreAuthorize("hasPermission(#c, 'write')")
public void updateContact(@P("c") Contact contact);
method 실행후 권한을 평가한다.
@PostAhuthorize는 returnObject를 통해 메소드의 반환값을 평가 할 수 있다.
@Component
public class BankService {
@PostAuthorize("returnObject.owner == authentication.name")
public Account readAccount(Long id) {
// ... is only returned if the `Account` belongs to the logged in user
}
}
@PreAuthorize의 인터셉터는 100번, @PostAuthorize의 인터셉트는 200번이다. 이게 중요한 이유는 @Transaction이 기본 설정인 @EnableTransactionManagement(Integer.MAX_VALUE) 설정으로 인해 제일 마지막에 실행되는 AOP이기 때문이다. 따라서 @PostAuthorize의 경우 권한통과 실패해도 이미 Transaction이 종료된 후라 Rollback 되지 않는다. @PostAuthorize가 @Transaction안에서 실행 되게 할려면 @Configure annotaion이 있는 곳에 다음과 같이 설정하면 된다.
@EnableTransactionManagement(order = 0)
여러개의 값을 @PreFilter를 통해 검증할 수 있다.
@Component
public class BankService {
@PreFilter("filterObject.owner == authentication.name")
public Collection<Account> updateAccounts(Account... accounts) {
// ... `accounts` will only contain the accounts owned by the logged-in user
return updated;
}
}
@PreFilter는 Array, Collection, Map, Stream를 지원합니다.
@PreFilter("filterObject.owner == authentication.name")
public Collection<Account> updateAccounts(Account[] accounts)
@PreFilter("filterObject.owner == authentication.name")
public Collection<Account> updateAccounts(Collection<Account> accounts)
@PreFilter("filterObject.value.owner == authentication.name")
public Collection<Account> updateAccounts(Map<String, Account> accounts)
@PreFilter("filterObject.owner == authentication.name")
public Collection<Account> updateAccounts(Stream<Account> accounts)
method 호출 종료 후 반환 값들을 여러개 검증할 수 있다.
@Component
public class BankService {
@PostFilter("filterObject.owner == authentication.name")
public Collection<Account> readAccounts(String... ids) {
// ... the return value will be filtered to only contain the accounts owned by the logged-in user
return accounts;
}
}
@Controller
@PreAuthorize("hasAuthority('ROLE_USER')")
public class MyController {
@GetMapping("/endpoint")
public String endpoint() { ... }
}
class 수준과 method 수준 모두에서 선언 된 경우 method 수준에서 선언된 annotaion이 class 수준에 정의된 annotaion을 재정의 합니다.
@Controller
@PreAuthorize("hasAuthority('ROLE_USER')")
public class MyController {
@GetMapping("/endpoint")
@PreAuthorize("hasAuthority('ROLE_ADMIN')") //재정의됨
public String endpoint() { ... }
}
SpEL 표현식에서 기본적으로 제공하는 다양한 표현식들 존재합니다.
그리고 설정된 변수
사용예시
@Component
public class MyService {
@PreAuthorize("denyAll")
MyResource myDeprecatedMethod(...);
@PreAuthorize("hasRole('ADMIN')")
MyResource writeResource(...)
@PreAuthorize("hasAuthority('db') and hasRole('ADMIN')")
MyResource deleteResource(...)
@PreAuthorize("principal.claims['aud'] == 'my-audience'")
MyResource readResource(...);
}
먼저 MethodSecurityExpressionOperations의 인스턴스를 매개변수로 하는 method를 가지는 bean을 만드세요
@Component("authz")
public class AuthorizationLogic {
public boolean decide(MethodSecurityExpressionOperations operations) {
// ... authorization logic
}
}
그런 다음 해당 bean을 사용하세요
@Controller
public class MyController {
@PreAuthorize("@authz.decide(#root)")
@GetMapping("/endpoint")
public String endpoint() {
// ...
}
}
잘 읽었습니다. 좋은 정보 감사드립니다.