Spring Security의 권한(Authorities) 개념과 구현 방식
GrantedAuthority란?Spring Security에서 GrantedAuthority는 인증된 사용자가 가진 개별적인 권한을 나타냅니다.
주요 역할:
GrantedAuthority의 구조Spring Security의 GrantedAuthority는 다음과 같은 단순한 인터페이스입니다:
public interface GrantedAuthority {
String getAuthority();
}
getAuthority() 메서드: 권한을 문자열(String)로 반환합니다. ROLE_USER, ROLE_ADMIN, 또는 사용자 정의 권한 문자열.Spring Security의 인증 및 권한 부여 과정에서 GrantedAuthority는 다음과 같은 흐름으로 사용됩니다:
AuthenticationManager에서 권한 생성
AuthenticationManager는 사용자의 권한 정보를 GrantedAuthority로 변환하여 Authentication 객체에 저장합니다. AccessDecisionManager에서 권한 확인
GrantedAuthority 목록을 검사하여 사용자가 특정 요청에 대해 권한이 있는지 판단합니다.SimpleGrantedAuthoritySpring Security에서 기본적으로 제공하는 GrantedAuthority의 구현체는 SimpleGrantedAuthority입니다.
GrantedAuthority로 변환합니다. new SimpleGrantedAuthority("ROLE_USER");Complex GrantedAuthority일부 상황에서는 권한이 문자열로 간단히 표현될 수 없을 수도 있습니다.
예:
이런 경우, getAuthority() 메서드는 null을 반환해야 합니다.
Spring Security는 기본적으로 역할(Role) 기반 권한에 접두사 ROLE_을 사용합니다.
GrantedAuthority의 값으로 ROLE_USER를 기대합니다. 이를 변경하고 싶다면 GrantedAuthorityDefaults를 사용하여 접두사를 커스터마이징할 수 있습니다.
@Bean
static GrantedAuthorityDefaults grantedAuthorityDefaults() {
return new GrantedAuthorityDefaults("MYPREFIX_");
}
ROLE_ 대신 MYPREFIX_를 접두사로 사용합니다.MYPREFIX_ADMIN 또는 MYPREFIX_USER와 같은 권한 이름을 사용할 수 있습니다.Spring Security에서 권한 부여는 크게 3가지 수준에서 처리됩니다:
http
.authorizeRequests()
.antMatchers("/admin/**").hasAuthority("ROLE_ADMIN")
.antMatchers("/user/**").hasAuthority("ROLE_USER")
.anyRequest().authenticated();@PreAuthorize("hasAuthority('ROLE_ADMIN')")
public void adminMethod() {
// 관리자만 접근 가능
}Spring Security는 각 권한 검사를 이벤트로 발행하며, 이를 통해 특정 권한 부여 로직을 모니터링하거나 커스터마이징할 수 있습니다.
GrantedAuthority는 사용자의 권한을 나타내는 기본 단위입니다. SimpleGrantedAuthority를 사용하지만, 복잡한 구현도 지원합니다. ROLE_는 GrantedAuthorityDefaults를 사용해 변경할 수 있습니다. 이렇게 설계된 Spring Security의 권한 구조는 유연하면서도 강력한 권한 부여 메커니즘을 제공합니다.
Spring Security는 메서드 호출이나 웹 요청과 같은 보안 객체에 대한 접근을 제어하는 인터셉터를 제공합니다.
AuthorizationManager 인스턴스가 판단합니다. AuthorizationManager 인스턴스가 판단합니다.AuthorizationManager 상세 설명AuthorizationManager란?Spring Security의 AuthorizationManager는 최종적인 권한 부여 결정을 내리는 컴포넌트입니다.
기존의 AccessDecisionManager 및 AccessDecisionVoter와 동일한 역할을 수행하지만, 더 유연하고 통합된 설계를 제공합니다.
새로운 권한 부여 구조
AccessDecisionManager와 AccessDecisionVoter를 대체하며, 더 간단한 API를 제공합니다. 유연성
권한 부여 방식
secureObject)를 기반으로 수행됩니다. AuthorizationManager의 메서드 설명check 메서드AuthorizationDecision check(Supplier<Authentication> authentication, Object secureObject);
역할
매개변수
Supplier<Authentication>
Authentication 객체에서 현재 사용자의 권한, 역할, 사용자 이름 등을 확인할 수 있습니다.Object secureObject
MethodInvocation 객체 (메서드 이름 및 매개변수 포함)리턴값
AuthorizationDecision 객체를 반환하며, 권한 부여 결과를 나타냅니다:AuthorizationDecision 객체가 허용(true) 값을 포함하여 반환됩니다.AuthorizationDecision 객체가 거부(false) 값을 포함하여 반환됩니다.null 반환.verify 메서드default void verify(Supplier<Authentication> authentication, Object secureObject)
throws AccessDeniedException {
// ...
}
역할
check 메서드를 호출하여 권한을 검사한 후, 결과가 거부(Negative)일 경우 AccessDeniedException을 발생시킵니다. 동작 순서
check 메서드를 호출하여 권한 부여 결과를 확인합니다.AccessDeniedException을 발생시켜 애플리케이션에서 적절한 조치를 취할 수 있도록 합니다.AuthorizationManager사용자가 특정 고객(Customer) 데이터를 조회할 권한이 있는지 확인하는 로직입니다.
보호 대상: secureObject
MethodInvocation) 객체를 통해 메서드 이름 및 매개변수를 확인합니다.getCustomerDetails(Long customerId)권한 부여 로직:
코드 예제:
public class CustomerAuthorizationManager implements AuthorizationManager<MethodInvocation> {
@Override
public AuthorizationDecision check(Supplier<Authentication> authentication, MethodInvocation invocation) {
// 인증 정보 가져오기
Authentication auth = authentication.get();
String principal = auth.getName(); // 현재 사용자 이름 가져오기
// 호출된 메서드의 인수 확인
Object[] args = invocation.getArguments();
Long customerId = (Long) args[0]; // 고객 ID 추출
// 현재 사용자가 고객 데이터를 조회할 권한이 있는지 검사
boolean hasPermission = checkPermission(principal, customerId);
return new AuthorizationDecision(hasPermission); // 권한 결과 반환
}
private boolean checkPermission(String principal, Long customerId) {
// 실제 권한 검사 로직
return "admin".equals(principal) || isCustomerOwner(principal, customerId);
}
private boolean isCustomerOwner(String principal, Long customerId) {
// 사용자가 고객 데이터를 소유하고 있는지 확인하는 로직
return true; // 예시를 위한 간단한 로직
}
}
단순화된 설계
AccessDecisionManager 및 AccessDecisionVoter에서는 복잡한 투표 기반 권한 부여 로직이 필요했지만, AuthorizationManager는 이를 단일 메서드로 처리.유연성 향상
secureObject를 통해 요청 및 메서드 호출의 모든 세부 정보를 직접 확인 가능.권한 부여 및 예외 처리 통합
verify 메서드를 통해 권한 검사의 결과를 기반으로 간단하게 예외 처리 가능.HTTP 요청: URL 기반 권한 부여.
http.authorizeRequests()
.antMatchers("/admin/**").access(new AuthorizationManager<>(...))
.anyRequest().authenticated();
메서드 호출:
메서드 수준의 권한 검사.
@PreAuthorize("hasAuthority('ROLE_ADMIN')")
public void adminMethod() {
// 관리자만 접근 가능
}
AuthorizationManager는 Spring Security의 새로운 권한 부여 표준입니다.check와 verify를 통해 권한 검사를 간단하고 유연하게 처리.AuthorizationManager 구현Delegate 기반 접근 방식에서는 여러 AuthorizationManager 인스턴스를 조합하여 권한 부여를 처리합니다.
AuthorizationManager는 특정 조건 또는 맥락에 따라 독립적으로 작동합니다. AuthorizationManager는 이를 조정하고, 요청이나 호출된 메서드에 따라 적절한 AuthorizationManager를 선택하여 사용합니다.RequestMatcherDelegatingAuthorizationManager이 구현체는 HTTP 요청 권한 부여를 위해 설계되었습니다.
역할: HTTP 요청을 특정 조건(RequestMatcher)과 비교한 후, 가장 적합한 AuthorizationManager를 사용해 권한 부여를 처리합니다.
작동 방식:
RequestMatcher가 요청과 일치하는지 검사합니다. RequestMatcher가 있으면, 해당 조건에 연결된 AuthorizationManager가 호출됩니다. 사용 예제:
var authorizationManager = RequestMatcherDelegatingAuthorizationManager.builder()
.add(RequestMatchers.antMatcher("/admin/**"), new AuthorizationManager<>() {
// 관리자 영역에 대한 권한 부여 로직
@Override
public AuthorizationDecision check(Supplier<Authentication> authentication, Object request) {
return new AuthorizationDecision(authentication.get().getAuthorities()
.contains(new SimpleGrantedAuthority("ROLE_ADMIN")));
}
})
.add(RequestMatchers.antMatcher("/user/**"), new AuthorizationManager<>() {
// 사용자 영역에 대한 권한 부여 로직
@Override
public AuthorizationDecision check(Supplier<Authentication> authentication, Object request) {
return new AuthorizationDecision(authentication.get().isAuthenticated());
}
})
.build();
AuthorizationManagerBeforeMethodInterceptor와 AuthorizationManagerAfterMethodInterceptorSpring Security는 메서드 호출 시 권한 부여를 제어하기 위해 사전/사후 인터셉터를 제공합니다.
AuthorizationManagerBeforeMethodInterceptor
AuthorizationManagerBeforeMethodInterceptor beforeInterceptor =
AuthorizationManagerBeforeMethodInterceptor.builder()
.expression("hasRole('ROLE_ADMIN')") // 관리자 권한 확인
.build();
AuthorizationManagerAfterMethodInterceptor
AuthorizationManagerAfterMethodInterceptor afterInterceptor =
AuthorizationManagerAfterMethodInterceptor.builder()
.expression("returnObject.owner == authentication.name") // 반환값 소유권 검사
.build();
Delegate 기반 AuthorizationManager를 사용하면 여러 권한 부여 로직을 조합하여 유연한 보안 정책을 구현할 수 있습니다.
AuthorizationManager는 특정 맥락이나 조건에 따라 독립적으로 작동합니다. AuthorizationManager가 이들을 호출하여 권한 부여 결정을 내립니다. @Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
var authorizationManager = RequestMatcherDelegatingAuthorizationManager.builder()
// 관리자 요청 처리
.add(RequestMatchers.antMatcher("/admin/**"), new AuthorizationManager<>() {
@Override
public AuthorizationDecision check(Supplier<Authentication> authentication, Object request) {
return new AuthorizationDecision(authentication.get().getAuthorities()
.contains(new SimpleGrantedAuthority("ROLE_ADMIN")));
}
})
// 사용자 요청 처리
.add(RequestMatchers.antMatcher("/user/**"), new AuthorizationManager<>() {
@Override
public AuthorizationDecision check(Supplier<Authentication> authentication, Object request) {
return new AuthorizationDecision(authentication.get().isAuthenticated());
}
})
// 기본 정책 (접근 불가)
.add(RequestMatchers.anyRequest(), new AuthorizationManager<>() {
@Override
public AuthorizationDecision check(Supplier<Authentication> authentication, Object request) {
return new AuthorizationDecision(false); // 접근 불가
}
})
.build();
http.authorizeHttpRequests(auth -> auth.requestMatcherDelegatingAuthorizationManager(authorizationManager));
return http.build();
}
RequestMatcherDelegatingAuthorizationManager) AuthorizationManagerBeforeMethodInterceptor) 요청 → RequestMatcherDelegatingAuthorizationManager → 적절한 AuthorizationManager 호출
메서드 → AuthorizationManagerBeforeMethodInterceptor → 권한 확인
결과 → AuthorizationManagerAfterMethodInterceptor → 반환값 확인
Delegate 기반의 장점
AuthorizationManager는 독립적이고 재사용 가능.주요 Delegate 구현체
RequestMatcherDelegatingAuthorizationManager: HTTP 요청 권한 부여 처리. AuthorizationManagerBeforeMethodInterceptor / AuthorizationManagerAfterMethodInterceptor: 메서드 호출 전/후 권한 부여 처리.적용 사례
AuthorityAuthorizationManager가장 일반적으로 사용되는 권한 관리자이며, 사용자의 권한(Authority)을 기반으로 권한 부여를 결정합니다.
Authentication)의 권한 정보와 비교됩니다.Authentication 객체를 확인합니다. GrantedAuthority)에 구성된 권한 중 하나라도 존재하면 긍정적(Positive) 결정을 반환합니다. // ROLE_ADMIN 또는 ROLE_USER 권한이 있어야 접근 가능
AuthorityAuthorizationManager<Object> authorityManager =
AuthorityAuthorizationManager.hasAnyAuthority("ROLE_ADMIN", "ROLE_USER");
AuthorizationDecision decision = authorityManager.check(() -> authentication, secureObject);
if (decision.isGranted()) {
System.out.println("Access Granted");
} else {
System.out.println("Access Denied");
}
http.authorizeRequests()
.antMatchers("/admin/**")
.access(AuthorityAuthorizationManager.hasAuthority("ROLE_ADMIN"));AuthenticatedAuthorizationManager사용자가 인증 상태인지, 그리고 인증 방식에 따라 권한을 부여합니다.
Remember-Me 인증 사용자: 제한된 인증 상태로, 일부 기능만 사용 가능.Remember-Me 인증 사용자 처리AuthenticatedAuthorizationManager<Object> authenticatedManager =
AuthenticatedAuthorizationManager.authenticated();
AuthorizationDecision decision = authenticatedManager.check(() -> authentication, secureObject);
if (decision.isGranted()) {
System.out.println("Authenticated User Access Granted");
} else {
System.out.println("Access Denied");
}
Remember-Me 인증 처리:http.authorizeRequests()
.antMatchers("/limited/**")
.access(AuthenticatedAuthorizationManager.rememberMe());AuthorizationManagers (정적 팩토리 메서드)Spring Security는 여러 AuthorizationManager를 조합하여 더 복잡한 권한 부여 로직을 작성할 수 있도록 정적 팩토리 메서드를 제공합니다.
AuthorizationManager를 AND 또는 OR 조건으로 조합.// ROLE_ADMIN 권한을 가진 인증된 사용자만 접근 가능
AuthorizationManager<Object> composedManager =
AuthorizationManagers.allOf(
AuthorityAuthorizationManager.hasAuthority("ROLE_ADMIN"),
AuthenticatedAuthorizationManager.authenticated()
);
AuthorizationDecision decision = composedManager.check(() -> authentication, secureObject);
if (decision.isGranted()) {
System.out.println("Access Granted");
} else {
System.out.println("Access Denied");
}
allOf: 모든 조건이 만족해야 함 (AND).anyOf: 하나의 조건만 만족해도 됨 (OR).Spring Security는 사용자가 직접 커스텀 AuthorizationManager를 구현할 수 있는 강력한 유연성을 제공합니다.
AuthorizationManager.public class AccountStatusAuthorizationManager implements AuthorizationManager<Object> {
@Override
public AuthorizationDecision check(Supplier<Authentication> authentication, Object secureObject) {
Authentication auth = authentication.get();
String username = auth.getName();
// 계정 상태 확인 (예: 정지된 계정)
if (isAccountSuspended(username)) {
return new AuthorizationDecision(false); // 접근 거부
}
return new AuthorizationDecision(true); // 접근 허용
}
private boolean isAccountSuspended(String username) {
// 외부 데이터베이스나 API를 통해 계정 상태 확인
return false; // 예시로 항상 정상 계정으로 처리
}
}
http.authorizeRequests()
.antMatchers("/secure/**")
.access(new AccountStatusAuthorizationManager());AccessDecisionVoter와의 차이점Spring Security 블로그에서 언급된 계정 상태 검사(AccessDecisionVoter)를 AuthorizationManager로 변환하는 방법은 다음과 같습니다:
이전 방식에서는 여러 AccessDecisionVoter를 조합하여 투표 기반으로 권한을 결정했습니다.
AuthorizationManager.allOf(AND), anyOf(OR)를 통해 조건 결합.Spring Security의 AuthorizationManager는 기존 구조를 대체하면서도 더 간단하고 유연한 방식으로 권한 부여 로직을 처리할 수 있게 합니다.
AuthorizationManager로 변환하기Spring Security는 AuthorizationManager가 도입되기 이전에 AccessDecisionManager와 AccessDecisionVoter를 사용하여 권한 부여를 처리했습니다.
AccessDecisionManager 또는 AccessDecisionVoter를 호출하는 AuthorizationManager를 작성할 수 있습니다.AccessDecisionManager를 호출하는 AuthorizationManager 구현@Component
public class AccessDecisionManagerAuthorizationManagerAdapter implements AuthorizationManager<Object> {
private final AccessDecisionManager accessDecisionManager;
private final SecurityMetadataSource securityMetadataSource;
@Override
public AuthorizationDecision check(Supplier<Authentication> authentication, Object object) {
try {
// 보호 객체의 보안 메타데이터 가져오기
Collection<ConfigAttribute> attributes = this.securityMetadataSource.getAttributes(object);
// AccessDecisionManager를 호출하여 권한 결정
this.accessDecisionManager.decide(authentication.get(), object, attributes);
return new AuthorizationDecision(true); // 접근 허용
} catch (AccessDeniedException ex) {
return new AuthorizationDecision(false); // 접근 거부
}
}
@Override
public void verify(Supplier<Authentication> authentication, Object object) {
Collection<ConfigAttribute> attributes = this.securityMetadataSource.getAttributes(object);
// AccessDecisionManager를 호출하여 검증
this.accessDecisionManager.decide(authentication.get(), object, attributes);
}
}
check 메서드
SecurityMetadataSource를 통해 보호 객체(object)의 보안 메타데이터를 가져옵니다.AccessDecisionManager의 decide 메서드를 호출하여 권한을 검사합니다.AuthorizationDecision을 반환합니다.verify 메서드
check와 동일한 방식으로 동작하지만, 권한이 없을 경우 AccessDeniedException을 던져 예외 처리를 제공합니다.AccessDecisionVoter를 호출하는 AuthorizationManager 구현@Component
public class AccessDecisionVoterAuthorizationManagerAdapter implements AuthorizationManager<Object> {
private final AccessDecisionVoter<Object> accessDecisionVoter;
private final SecurityMetadataSource securityMetadataSource;
@Override
public AuthorizationDecision check(Supplier<Authentication> authentication, Object object) {
// 보호 객체의 보안 메타데이터 가져오기
Collection<ConfigAttribute> attributes = this.securityMetadataSource.getAttributes(object);
// AccessDecisionVoter 호출하여 투표 결과 결정
int decision = this.accessDecisionVoter.vote(authentication.get(), object, attributes);
switch (decision) {
case AccessDecisionVoter.ACCESS_GRANTED:
return new AuthorizationDecision(true); // 접근 허용
case AccessDecisionVoter.ACCESS_DENIED:
return new AuthorizationDecision(false); // 접근 거부
}
return null; // 기권(Abstain)
}
}
check 메서드 SecurityMetadataSource를 통해 보호 객체의 메타데이터를 가져옵니다.AccessDecisionVoter의 vote 메서드를 호출하여 투표 결과를 확인합니다.ACCESS_GRANTED이면 접근을 허용하고, ACCESS_DENIED이면 접근을 거부합니다.ABSTAIN인 경우, null을 반환하여 권한 결정을 하지 않습니다.SecurityFilterChain에 연결하기AccessDecisionManager 또는 AccessDecisionVoter를 사용하는 방식위에서 작성한 AuthorizationManager를 Spring Security의 SecurityFilterChain에 연결하여 활용할 수 있습니다.
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http,
AccessDecisionManagerAuthorizationManagerAdapter adapter) throws Exception {
http.authorizeRequests()
.antMatchers("/secure/**")
.access(adapter) // Adapter를 권한 부여 로직으로 사용
.anyRequest()
.authenticated();
return http.build();
}
AuthorizationManager의 차이| 특징 | AccessDecisionManager/Voter | AuthorizationManager |
|---|---|---|
| 구조 | 복잡한 투표 기반 설계. 다수의 Voter 조합 필요. | 단순한 인터페이스로 권한 검사 로직 작성. |
| 유연성 | 제한적. 특정 역할에 집중. | 커스터마이징 및 다양한 접근 방식 지원. |
| 사용성 | 새로운 애플리케이션에는 부적합. | 최신 권장 방식. 유지보수와 확장이 용이. |
| 이전 방식과의 호환성 | 기존 코드 재사용 가능. | 어댑터를 통해 이전 방식과 호환 가능. |
AccessDecisionManager 또는 AccessDecisionVoter를 AuthorizationManager로 감쌀 수 있는 어댑터를 작성하면, 기존 애플리케이션에서 최신 권한 관리 방식을 도입할 수 있습니다.SecurityFilterChain에 연결하여 권한 부여를 처리하며, 기존 구조와 새로운 구조를 유연하게 통합할 수 있습니다.AuthorizationManager를 사용하는 것이며, 이전 구조와의 호환성이 필요한 경우 위와 같은 어댑터 패턴을 활용할 수 있습니다.역할 계층(Hierarchical Roles)은 상위 역할이 하위 역할의 권한을 자동으로 포함하도록 정의하는 메커니즘입니다.
이를 통해 애플리케이션의 역할 관리와 접근 제어를 단순화할 수 있습니다.
역할 계층을 설정하면, ROLE_ADMIN은 자동으로 ROLE_USER의 권한을 포함합니다.
역할 계층이 없을 경우, 권한을 처리하려면 다음 중 하나를 선택해야 합니다:
1. 모든 상위 역할에 하위 역할을 수동으로 추가
Spring Security에서는 RoleHierarchy를 사용하여 역할 계층을 정의합니다.
RoleHierarchyImpl을 통해 계층을 설정하며, 기본적으로 ROLE_ 접두사를 사용합니다.
@Bean
static RoleHierarchy roleHierarchy() {
return RoleHierarchyImpl.withDefaultRolePrefix() // ROLE_ 접두사 자동 추가
.role("ADMIN").implies("STAFF") // ROLE_ADMIN은 ROLE_STAFF 포함
.role("STAFF").implies("USER") // ROLE_STAFF는 ROLE_USER 포함
.role("USER").implies("GUEST") // ROLE_USER는 ROLE_GUEST 포함
.build();
}
RoleHierarchyImpl:role("A").implies("B")는 A 역할이 B 역할의 권한을 포함함을 의미합니다.withDefaultRolePrefix():role("ADMIN")은 내부적으로 ROLE_ADMIN으로 처리.@PreAuthorize, @Secured, 또는 JSR-250 어노테이션(@RolesAllowed)을 사용하는 경우, 역할 계층을 메서드 보안에 적용해야 합니다.
@Bean
static MethodSecurityExpressionHandler methodSecurityExpressionHandler(RoleHierarchy roleHierarchy) {
DefaultMethodSecurityExpressionHandler expressionHandler = new DefaultMethodSecurityExpressionHandler();
expressionHandler.setRoleHierarchy(roleHierarchy); // 역할 계층 설정
return expressionHandler;
}
MethodSecurityExpressionHandler:@PreAuthorize("hasRole('USER')")
public void userOnlyMethod() {
// ROLE_USER 권한이 필요한 메서드
// ROLE_ADMIN도 접근 가능 (계층 덕분)
}
HttpSecurity의 권한 부여 로직에서도 역할 계층이 적용됩니다.
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http, RoleHierarchy roleHierarchy) throws Exception {
http
.authorizeHttpRequests()
.requestMatchers("/admin/**").hasRole("ADMIN") // ROLE_ADMIN만 접근 가능
.requestMatchers("/user/**").hasRole("USER") // ROLE_USER 이상 접근 가능
.anyRequest().authenticated();
return http.build();
}
역할과 권한 간 논리적 매핑 정의:
외부 시스템 연동:
RoleHierarchy를 통해 계층 정의.HttpSecurity)과 메서드 보안(@PreAuthorize 등)에 적용.Spring Security의 역할 계층은 복잡한 역할 기반 접근 제어를 단순화하고, 유지보수를 쉽게 만들어 줍니다.