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() {
// ...
}
}
잘 읽었습니다. 좋은 정보 감사드립니다.