2024. 3. 1. 12:28ㆍSpring Security
이 장에서는 다음을 다룹니다.
- 권한과 Role 정의하기
- 엔드포인트에 권한 규칙 적용하기
몇 년 전, 저는 아름다운 카르파티아 산맥에서 스키를 타던 중 이 재미있는 장면을 목격했습니다. 대략 열 명, 아니면 열다섯 명의 사람들이 스키 슬로프 꼭대기로 가기 위한 케빈에 탑승하기 위해 줄을 서 있었습니다. 한 유명한 팝 아티스트가 두 명의 보디가드와 함께 나타났습니다. 그는 유명인사라는 이유로 줄을 뛰어넘을 수 있을 것으로 자신하며 당당하게 앞으로 걸어갔습니다. 줄의 맨 앞에 도달했을 때, 그는 놀라움을 받았습니다. "티켓 주세요!"라고 탑승을 관리하는 사람이 말했고, 그 다음에는 "먼저 티켓이 필요하고, 둘째로, 이 탑승에는 우선 순위 줄이 없어요, 죄송합니다. 줄은 저기 끝에 있습니다."라고 설명해야 했습니다. 그는 줄의 끝을 가리켰습니다. 대부분의 경우에서와 마찬가지로, 당신이 누구인지는 중요하지 않습니다. 우리는 소프트웨어 애플리케이션에 대해서도 같은 말을 할 수 있습니다. 특정 기능이나 데이터에 접근하려고 할 때 당신이 누구인지는 중요하지 않습니다!
지금까지 우리는 인증에 대해서만 논의했는데, 여러분이 배운 바와 같이, 이는 우리의 웹 애플리케이션이 리소스의 호출자(User)를 식별하는 과정입니다. 이전 장에서 우리가 작업한 예제들에서는, 요청을 승인할지 결정하는 규칙을 구현하지 않았습니다. 우리는 시스템이 사용자를 알고 있는지 여부만 신경 썼습니다. 대부분의 애플리케이션에서, 시스템에 의해 식별된 모든 사용자가 시스템 내의 모든 리소스에 접근할 수 있는 것은 아닙니다. 이 장에서는 권한 부여에 대해 논의할 것입니다. 권한 부여는 인증된 클라이언트가 요청한 리소스에 이 클라이언트가 접근할 권한이 있는지 시스템이 결정하는 과정입니다(그림 7.1).
그림 7.1 권한 부여[Authorization]는 애플리케이션이 인증된 실체가 리소스에 접근할 수 있는지 여부를 결정하는 과정입니다. 권한 부여는 항상 인증 이후에 발생합니다.
Spring Security에서, 애플리케이션이 인증 흐름을 마친 후, 인증된 클라이언트의 요청을 권한 부여 필터로 위임합니다. 필터는 설정된 권한 부여 규칙에 기반하여 요청을 허용하거나 거부합니다(그림 7.2).
그림 7.2 클라이언트가 요청을 할 때, 인증 필터는 사용자를 인증합니다. 인증이 성공한 후, 인증 필터는 UserDetails를 Security Context에 저장하고 요청을 권한 부여 필터로 전달합니다. 권한 부여 필터는 요청이 허용될지를 결정합니다. 요청에 권한을 부여할지 결정하기 위해, 권한 부여 필터는 보안 컨텍스트의 Details를 사용합니다.
권한 부여에 대한 모든 필수적인 세부 사항을 다루기 위해, 이 장에서는 다음과 같은 단계를 따를 것입니다:
1. 권한이 무엇인지 이해하고 사용자의 권한에 기반하여 모든 엔드포인트에 접근 규칙을 적용합니다.
2. 권한을 역할로 그룹화하는 방법과 사용자의 역할에 기반한 권한 부여 규칙을 적용하는 방법을 배웁니다.
8장에서는 권한 부여 규칙을 적용할 엔드포인트를 선택하는 것을 계속할 것입니다. 지금은, 권한과 역할이 우리의 애플리케이션에 대한 접근을 어떻게 제한할 수 있는지 살펴보겠습니다.
7.1 Restricting access based on authorities and roles
이 섹션에서는 Authorization과 Role의 개념에 대해 배웁니다. 이러한 개념을 사용하여 애플리케이션의 모든 엔드포인트를 보안합니다. 다양한 사용자가 다른 권한을 가진 실제 상황에서 이러한 개념을 적용하기 전에 이해해야 합니다. 사용자가 가진 권한에 따라, 특정 행동만 실행할 수 있습니다. 애플리케이션은 Authorization과 Role로서 privileges[권리] 을 제공합니다.
3장에서, 여러분은 GrantedAuthority 인터페이스를 구현했습니다. 이 계약은 중요한 다른 구성요소인 UserDetails 인터페이스를 논의할 때 소개되었습니다. 그때 GrantedAuthority를 다루지 않았는데, 이 장에서 배우게 될 것처럼, 이 인터페이스는 주로 권한 부여 과정과 관련이 있기 때문입니다. 이제 GrantedAuthority로 돌아가 그 목적을 검토할 수 있습니다. 그림 7.3은 UserDetails 계약과 GrantedAuthority 인터페이스 간의 관계를 보여줍니다. 이 계약에 대한 논의를 마치면, 이러한 규칙을 개별적으로 또는 특정 요청에 대해 어떻게 사용하는지 배울 것입니다.
그림 7.3 사용자는 하나 이상의 권한(사용자가 할 수 있는 행동)을 가지고 있습니다. 인증 과정 중에 UserDetailsService는 권한을 포함한 사용자에 대한 모든 Details를 얻습니다. 애플리케이션은 사용자를 성공적으로 인증한 후 권한 부여를 위해 GrantedAuthority 인터페이스로 표현된 권한을 사용합니다.
리스팅 7.1은 GrantedAuthority 계약의 정의를 보여줍니다. 권한은 사용자가 시스템 리소스로 수행할 수 있는 행동입니다. 권한은 객체의 getAuthority() 메소드가 문자열로 리턴하는 이름을 가지고 있습니다. 우리는 사용자 정의 권한 부여 규칙을 정의할 때 권한의 이름을 사용합니다. 종종 권한 부여 규칙은 다음과 같을 수 있습니다: "Jane은 제품 기록을 Delete할 수 있습니다," 또는 "John은 문서 기록을 Read 할 수 있습니다." 이 경우, Delete와 Read는 부여된 권한입니다. 애플리케이션은 사용자 Jane과 John이 이러한 행동을 수행할 수 있도록 허용하며, 이러한 행동은 종종 read, write, 또는 delete와 같은 이름을 가집니다.
Listing 7.1 The GrantedAuthority contract
public interface GrantedAuthority extends Serializable {
String getAuthority();
}
Spring Security에서 사용자를 설명하는 계약인 UserDetails는 그림 7.3에서 제시된 바와 같이 GrantedAuthority 인스턴스의 Collection을 가지고 있습니다. 사용자에게 하나 이상의 권한을 허용할 수 있습니다. getAuthorities() 메소드는 GrantedAuthority 인스턴스의 컬렉션을 리턴합니다. 리스팅 7.2에서 UserDetails 계약에서 이 메소드를 검토할 수 있습니다. 우리는 이 메소드를 사용자에게 부여된 모든 권한을 반환하도록 구현합니다. 인증이 끝난 후, 권한은 로그인한 사용자에 대한 Details의 일부가 되며, 애플리케이션은 이를 사용하여 권한을 부여할 수 있습니다.
Listing 7.2 The getAuthorities() method from the UserDetails contract
public interface UserDetails extends Serializable {
Collection<? extends GrantedAuthority> getAuthorities();
// Omitted code
}
7.1.1 Restricting access for all endpoints based on user authorities
이 섹션에서는 특정 사용자에게 특정 엔드포인트에 대한 접근을 제한하는 방법에 대해 논의합니다. 지금까지의 예시에서는, 인증된 모든 사용자가 애플리케이션의 모든 엔드포인트를 호출할 수 있었습니다. 이제부터는 이러한 접근을 사용자 정의하는 방법을 배우게 됩니다. 실제 Production 환경에서 찾을 수 있는 앱에서는 인증되지 않은 상태에서도 애플리케이션의 일부 엔드포인트를 호출할 수 있지만, 다른 일부는 특별한 권한이 필요합니다(그림 7.4 참조). 우리는 Spring Security를 사용하여 이러한 제한을 적용할 수 있는 다양한 방법을 배울 수 있도록 여러 예시를 작성할 것입니다.
그림 7.4 권한은 사용자가 애플리케이션에서 수행할 수 있는 행동입니다. 이러한 행동을 기반으로 권한 부여 규칙을 구현합니다. 특정 권한을 가진 사용자만이 엔드포인트에 특정 요청을 할 수 있습니다. 예를 들어, Jane은 엔드포인트에 대해 읽기와 쓰기만 할 수 있는 반면, John은 엔드포인트를 읽기, 쓰기, 삭제, 업데이트할 수 있습니다.
이제 UserDetails와 GrantedAuthority 계약과 그 사이의 관계를 기억했으니, 권한 부여 규칙을 적용하는 작은 앱을 작성할 시간입니다. 이 예제를 통해 사용자의 권한에 기반하여 엔드포인트에 대한 접근을 구성하는 몇 가지 대안을 배우게 됩니다. 우리는 'ssia-ch7-ex1'이라는 이름의 새 프로젝트를 시작합니다. 다음과 같은 방법을 사용하여 접근을 구성하는 세 가지 방법을 보여줍니다:
- hasAuthority() — 애플리케이션이 제한을 구성하는 단 하나의 권한을 파라미터의 아규먼트로 전달 받습니다. 해당 권한을 가진 사용자만이 엔드포인트를 호출할 수 있습니다.
- hasAnyAuthority() — 애플리케이션이 제한을 구성하는 둘 이상의 권한을 받을 수 있습니다. 이 메서드를 "주어진 권한 중 하나라도 가지고 있으면 된다"고 기억합니다. 사용자는 요청을 하기 위해 지정된 권한 중 최소 하나를 가지고 있어야 합니다. 사용자에게 할당하는 권한의 갯수에 따라, 이 메서드 또는 hasAuthority() 메서드 사용을 추천합니다. 이들은 구성에서 읽기 쉽고 코드를 이해하기 쉽게 만듭니다.
- access() — 애플리케이션이 AuthorizationManager라는 사용자 정의 객체를 기반으로 권한 부여 규칙을 구축하기 때문에 접근을 구성하는 데 있어 무한한 가능성을 제공합니다. 사례에 따라 AuthorizationManager 계약에 대한 어떤 구현도 제공할 수 있습니다. Spring Security도 몇 가지 구현을 제공합니다. 가장 일반적인 구현은 Spring Expression Language (SpEL)를 기반으로 권한 부여 규칙을 적용하는 데 도움을 주는 WebExpressionAuthorizationManager입니다. 하지만 access() 메서드를 사용하면 권한 부여 규칙을 읽고 이해하기 더 어려워질 수 있습니다. 이러한 이유로, hasAnyAuthority()나 hasAuthority() 메서드를 적용할 수 없는 경우에만 덜 추천하는 해결책으로 권장합니다.
pom.xml 파일에 필요한 유일한 의존성은 spring-boot-starter-web과 spring-boot-starter-security입니다. 이 의존성들은 이전에 열거된 세 가지 해결책을 접근하는 데 충분합니다. 이 예제는 프로젝트 ssia-ch7-ex1에서 찾을 수 있습니다.
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
또한, 우리의 권한 부여 구성을 테스트하기 위해 애플리케이션에 엔드포인트를 추가합니다:
@RestController
public class HelloController {
@GetMapping("/hello")
public String hello() {
return "Hello!";
}
}
구성 클래스에서, 우리는 InMemoryUserDetailsManager를 UserDetailsService로 선언하고 이 인스턴스에 의해 관리될 두 명의 사용자, John과 Jane을 추가합니다. 각 사용자는 다른 권한을 가지고 있습니다. 다음 리스트에서 이를 어떻게 하는지 볼 수 있습니다.
Listing 7.3 Declaring the UserDetailsService and assigning users
@Configuration
public class ProjectConfig {
@Bean #A
public UserDetailsService userDetailsService() {
var manager = new InMemoryUserDetailsManager(); #B
var user1 = User.withUsername("john") #C
.password("12345")
.authorities("READ")
.build();
var user2 = User.withUsername("jane") #D
.password("12345")
.authorities("WRITE")
.build();
manager.createUser(user1); #E
manager.createUser(user2);
return manager;
}
@Bean #F
public PasswordEncoder passwordEncoder() {
return NoOpPasswordEncoder.getInstance();
}
}
#A 메서드에 의해 반환된 UserDetailsService는 SpringContext에 추가됩니다.
#B 몇몇 사용자를 저장하는 InMemoryUserDetailsManager를 선언합니다
#C 첫 번째 사용자 john은 READ 권한을 가집니다
#D 두 번째 사용자 jane은 WRITE 권한을 가집니다
#E 사용자들은 UserDetailsService에 의해 추가되고 관리됩니다.
#F PasswordEncoder도 필요하다는 것을 잊지 마십시오.
다음으로 우리가 할 일은 권한 부여 구성을 추가하는 것입니다. 2장에서 첫 번째 예제를 다룰 때, 모든 엔드포인트를 모두에게 접근 가능하게 만드는 방법을 보았습니다. 그렇게 하기 위해, 앱의 컨텍스트에 SecurityFilterChain 빈을 생성했는데, 이는 다음 리스트에서 보는 것과 유사합니다.
Listing 7.4 Making all the endpoints accessible for everyone without authentication
@Configuration
public class ProjectConfig {
// Omitted code
// ...
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http)
throws Exception {
http.httpBasic(Customizer.withDefaults());
http.authorizeHttpRequests(
c -> c.anyRequest().permitAll()
); #A
return http.build();
}
}
#A Permits access for all the requests
authorizeHttpRequests() 메서드는 우리가 엔드포인트에 대한 권한 부여 규칙을 지정하는 것을 계속할 수 있게 해줍니다. anyRequest() 메서드는 임의의 URL이나 HTTP 메서드와 관계없이 모든 요청에 규칙이 적용됨을 나타냅니다. permitAll() 메서드는 인증 여부와 관계없이 모든 요청에 대한 접근을 허용합니다.
WRITE 권한을 가진 사용자만 모든 엔드포인트에 접근할 수 있도록 하고 싶다고 가정해 봅시다. 우리 예제에서, 이는 Jane만을 의미합니다. 우리는 우리의 목표를 달성하고 이번에는 사용자의 권한에 기반하여 접근을 제한할 수 있습니다. 다음 리스트에서 코드를 살펴보세요.
Listing 7.5 Restricting access to only users having WRITE authority
@Configuration
public class ProjectConfig {
// Omitted code
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http)
throws Exception {
http.httpBasic(Customizer.withDefaults());
http.authorizeHttpRequests(
c -> c.anyRequest().hasAuthority("WRITE")
); #A
return http.build();
}
}
#A 사용자가 엔드포인트에 접근할 수 있는 조건을 지정합니다
permitAll() 메서드를 hasAuthority() 메서드로 교체한 것을 볼 수 있습니다. hasAuthority() 메서드의 파라미터로 허용된 권한의 이름을 제공합니다. 애플리케이션은 먼저 요청을 Authentication해야 하고, 그 다음 사용자의 Authorities에 기반하여 호출을 허용할지 결정합니다.
이제 두 사용자 각각으로 엔드포인트를 호출하며 애플리케이션을 테스트하기 시작할 수 있습니다. 사용자 Jane으로 엔드포인트를 호출할 때, HTTP 응답 상태는 200 OK이고, 응답 본문에는 “Hello!”가 표시됩니다. 사용자 John으로 호출할 때는 HTTP 응답 상태가 403 Forbidden이고, 응답 본문은 비어 있습니다. 예를 들어, 사용자 Jane으로 이 엔드포인트를 호출할 때,
curl -u jane:12345 http://localhost:8080/hello
we get this response:
Hello!
Calling the endpoint with user John,
curl -u john:12345 http://localhost:8080/hello
we get this response:
{
"status":403,
"error":"Forbidden",
"message":"Forbidden",
"path":"/hello"
}
비슷한 방식으로, hasAnyAuthority() 메서드를 사용할 수 있습니다. 이 메서드는 varargs 파라미터를 가지므로, 여러 개의 권한 이름들을 받을 수 있습니다. 애플리케이션은 메서드에 파라미터로 제공된 권한 중 최소 하나를 사용자가 가지고 있을 경우 요청을 허용합니다. 이전 리스트에서 hasAuthority()를 hasAnyAuthority("WRITE")으로 대체할 경우, 애플리케이션은 정확히 같은 방식으로 작동합니다. 그러나, hasAuthority()를 hasAnyAuthority("WRITE", "READ")로 대체한다면, 해당 권한을 가진 사용자의 요청이 모두 허용됩니다. 우리의 경우, 애플리케이션은 John과 Jane 모두의 요청을 허용합니다. 다음 리스트에서 hasAnyAuthority() 메서드를 어떻게 적용할 수 있는지 볼 수 있습니다.
Listing 7.6 Applying the hasAnyAuthority() method
@Configuration
public class ProjectConfig {
// Omitted code
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http)
throws Exception {
http.httpBasic(Customizer.withDefaults());
http.authorizeHttpRequests(
c -> c.anyRequest()
.hasAnyAuthority("WRITE", "READ");
); #A
return http.build();
}
}
#A WRITE 및 READ 권한을 가진 사용자로부터의 요청을 허용합니다
이제 우리의 두 사용자 중 어느 한 명으로도 엔드포인트를 성공적으로 호출할 수 있습니다. 다음은 John을 위한 호출입니다:
curl -u john:12345 http://localhost:8080/hello
The response body is
Hello!
And the call for Jane:
curl -u jane:12345 http://localhost:8080/hello
The response body is
Hello!
사용자 권한을 기반으로 접근을 지정하는 세 번째 방법은 실제로 access() 메서드입니다. 그러나 access() 메서드는 더 일반적입니다. 이 메서드는 AuthorizationManager 구현을 파라미터로 받습니다. 이 객체에 대해 권한 부여 규칙을 정의하는 어떤 종류의 로직이든 적용할 수 있는 어떤 구현도 제공할 수 있습니다. 이 방법은 강력하며 권한에만 국한되지 않습니다. 하지만, 이 방법은 또한 코드를 읽고 이해하기 더 어렵게 만듭니다. 이러한 이유로, 이전 섹션에서 소개된 hasAuthority() 또는 hasAnyAuthority() 메서드 중 하나를 적용할 수 없는 경우에만, 이 방법을 마지막 옵션으로 추천합니다.
이 방법을 이해하기 쉽게 만들기 위해, 먼저 hasAuthority()와 hasAnyAuthority() 메서드로 권한을 지정하는 대안으로 제시합니다. 이 예제에서 배우듯이, 매개변수로 SpEL 표현식을 제공해야 하는 AuthorizationManager 구현을 사용합니다. 우리가 정의한 권한 부여 규칙은 읽기가 더 어려워지며, 이것이 간단한 규칙에 이 접근 방식을 추천하지 않는 이유입니다. 그러나, access() 메서드는 매개변수로 제공하는 AuthorizationManager 구현을 통해 규칙을 사용자 정의할 수 있는 이점이 있습니다. 그리고 이것은 정말 강력합니다! SpEL 표현식과 마찬가지로, 사실상 어떤 조건이든 정의할 수 있습니다.
참고로 대부분의 상황에서 필요한 제한을 hasAuthority() 및 hasAnyAuthority() 메서드로 구현할 수 있으며, 이를 사용하는 것이 좋습니다. 다른 두 옵션이 적합하지 않고 더 일반적인 권한 부여 규칙을 구현하고 싶을 때만 access() 메서드를 사용하세요.
이전 사례와 동일한 요구 사항을 충족하는 간단한 예제로 시작합니다. 사용자가 특정 권한을 가지고 있는지만 테스트해야 하는 경우, access() 메서드와 함께 사용해야 하는 표현식은 다음 중 하나일 수 있습니다:
- hasAuthority('WRITE') — 사용자가 엔드포인트를 호출하기 위해 WRITE 권한이 필요하다고 명시합니다.
- hasAnyAuthority('READ', 'WRITE') — 사용자가 READ 또는 WRITE 권한 중 하나가 필요하다고 지정합니다. 이 표현식을 사용하면, 접근을 허용하고자 하는 모든 권한을 열거할 수 있습니다.
이 표현식들이 이 섹션 앞부분에서 소개된 메서드와 동일한 이름을 가지고 있다는 것을 관찰하세요. 다음 리스트는 access() 메서드를 어떻게 사용할 수 있는지 보여줍니다.
이 Expression들은 이 섹션 앞부분에서 소개된 메서드와 같은 이름을 가지고 있다는 점을 주목하세요. 다음 목록은 access() 메서드를 어떻게 사용할 수 있는지 보여줍니다.
Listing 7.7 Using the access() method to configure access to the endpoints
@Configuration
public class ProjectConfig {
// Omitted code
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http)
throws Exception {
http.httpBasic(Customizer.withDefaults());
http.authorizeHttpRequests(
c -> c.anyRequest()
.access("hasAuthority('WRITE')")
); #A
return http.build();
}
}
#A WRITE 권한을 가진 사용자로부터의 요청을 승인합니다
리스팅 7.7에 제시된 예제는 직설적인 요구 사항에 access() 메서드를 사용할 경우 문법을 어떻게 복잡하게 만드는지 증명합니다. 이런 경우에는 대신 hasAuthority() 또는 hasAnyAuthority() 메서드를 직접 사용해야 합니다. 하지만 access() 메서드가 전부 나쁜 것은 아닙니다. 앞서 말했듯이, 그것은 유연성을 제공합니다. 실제 상황에서 애플리케이션이 접근을 허용하는 기준에 따라 더 복잡한 표현식을 작성할 수 있는 상황을 발견할 것입니다. 이러한 시나리오를 access() 메서드 없이 구현할 수는 없을 것입니다.
리스팅 7.8에서는 다른 방식으로 쉽게 작성할 수 없는 표현식을 사용하여 access() 메서드가 적용된 것을 찾을 수 있습니다. 정확히, 리스팅 7.8에서 제시된 구성은 서로 다른 권한을 가진 두 명의 사용자, John과 Jane을 정의합니다. 사용자 John은 read 권한만 가지고 있으며, Jane은 read, write, delete 권한을 가지고 있습니다. 엔드포인트는 read 권한을 가진 사용자에게 접근 가능해야 하지만, delete 권한을 가진 사용자에게는 접근할 수 없어야 합니다.
참고로 Spring 앱에서는 권한의 명명에 다양한 스타일과 관례를 발견할 수 있습니다. 일부 개발자들은 모든 글자를 대문자로 사용하고, 다른 개발자들은 모든 글자를 소문자로 사용합니다. 제 의견으로는, 앱 내에서 이러한 선택을 일관되게 유지하는 한 모든 선택이 괜찮습니다. 이 책에서는 실제 상황에서 마주칠 수 있는 더 많은 접근 방식을 관찰할 수 있도록 예제에서 다양한 스타일을 사용합니다.
물론 가상의 예제지만, 이해하기 쉬울 정도로 단순하면서도 access() 메서드가 더 강력한 이유를 증명하기에 충분히 복잡합니다. 이를 access() 메서드로 구현하기 위해, SpEL 표현식을 취하는 AuthorizationManager 구현을 사용할 수 있습니다. SpEL 표현식은 요구 사항을 반영해야 합니다. 예를 들어:
"hasAuthority('read') and !hasAuthority('delete')"
다음 리스팅은 더 복잡한 표현식을 사용하여 access() 메서드를 적용하는 방법을 보여줍니다. 이 예제는 'ssia-ch7-ex2'라는 프로젝트에서 찾을 수 있습니다.
Listing 7.8 Applying the access() method with a more complex expression
@Configuration
public class ProjectConfig {
@Bean
public UserDetailsService userDetailsService() {
var manager = new InMemoryUserDetailsManager();
var user1 = User.withUsername("john")
.password("12345")
.authorities("read")
.build();
var user2 = User.withUsername("jane")
.password("12345")
.authorities("read", "write", "delete")
.build();
manager.createUser(user1);
manager.createUser(user2);
return manager;
}
@Bean
public PasswordEncoder passwordEncoder() {
return NoOpPasswordEncoder.getInstance();
}
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http)
throws Exception {
http.httpBasic(Customizer.withDefaults());
String expression =
"""hasAuthority('read') and #A
!hasAuthority('delete') #A
"""; #A
http.authorizeHttpRequests(
c -> c.anyRequest()
.access(new WebExpressionAuthorizationManager(expression));
);
return http.build();
}
}
#A 사용자는 read 권한은 있어야 하지만 delete 권한은 없어야 한다고 명시합니다
Let’s test our application now by calling the /hello endpoint for user John:
curl -u john:12345 http://localhost:8080/hello
The body of the response is
Hello!
And calling the endpoint with user Jane:
curl -u jane:12345 http://localhost:8080/hello
The body of the response is
{
"status":403,
"error":"Forbidden",
"message":"Forbidden",
"path":"/hello"
}
사용자 John은 read 권한만 가지고 있으며 엔드포인트를 성공적으로 호출할 수 있습니다. 하지만 Jane은 delete 권한도 가지고 있으며 엔드포인트를 호출할 수 있는 권한이 없습니다. Jane에 의한 호출에 대한 HTTP 상태는 403 Forbidden입니다.
이 예제들을 통해 사용자가 특정 엔드포인트에 접근하기 위해 가지고 있어야 하는 권한에 대한 제약을 설정하는 방법을 볼 수 있습니다. 물론, 우리는 아직 경로나 HTTP 메서드를 기반으로 보안을 적용할 요청을 선택하는 것에 대해 논의하지 않았습니다. 대신, 우리는 애플리케이션이 노출하는 엔드포인트와 관계없이 모든 요청에 대한 규칙을 적용했습니다. 사용자 역할에 대한 동일한 구성을 완료하고 나면, 권한 부여 구성을 적용할 엔드포인트를 선택하는 방법에 대해 논의합니다.
7.1.2 Restricting access for all endpoints based on user roles
이 섹션에서는 Roles을 기반으로 엔드포인트에 대한 접근을 제한하는 것에 대해 논의합니다. Roles은 사용자가 할 수 있는 것을 나타내는 또 다른 방법입니다(그림 7.5 참조). 실제 세계의 애플리케이션에서도 이를 발견할 수 있기 때문에, Roles과 권한 사이의 차이점을 이해하는 것이 중요합니다. 이 섹션에서는 애플리케이션이 Roles을 사용하는 모든 실제 시나리오를 알 수 있도록 Roles을 사용하는 여러 예제를 적용하며 이러한 경우에 대한 구성을 작성하는 방법을 배웁니다.
그림 7.5 역할은 대략적으로 구성되어 있습니다. 특정 Roles을 가진 각 사용자는 해당 Roles에 의해 부여된 작업만 수행할 수 있습니다. 이 철학을 권한 부여에 적용하면 시스템 내에서 사용자의 목적에 따라 요청이 허용됩니다. 특정 Roles을 가진 사용자만 특정 엔드포인트를 호출할 수 있습니다.
Spring Security는 권한을, 제한을 적용하는 세분화된 권한으로 이해합니다. Roles은 사용자에게 뱃지와 같습니다. 이는 사용자에게 일련의 행동에 대한 권한을 부여합니다. 일부 애플리케이션은 항상 특정 사용자에게 동일한 권한 그룹을 제공합니다. 예를 들어, 귀하의 애플리케이션에서 사용자는 읽기 권한만 가질 수 있거나 모두 가질 수 있습니다: 읽기, 쓰기, 삭제 권한. 이 경우, 읽기만 할 수 있는 사용자들이 READER라는 역할을 가지고 있다고 생각하는 것이 더 편할 수 있고, 다른 사용자들은 ADMIN 역할을 가집니다. ADMIN 역할을 가지고 있다는 것은 애플리케이션이 읽기, 쓰기, 업데이트, 삭제 권한을 부여한다는 것을 의미합니다. 더 많은 역할을 가질 수도 있습니다. 예를 들어, 특정 시점에 요청에서 읽기 및 쓰기만 허용된 사용자도 필요하다고 지정하는 경우 애플리케이션에 대해 MANAGER라는 세 번째 Roles을 생성할 수 있습니다.
참고 애플리케이션에서 Roles이 포함된 접근 방식을 사용할 때 더 이상 권한을 정의할 필요가 없습니다. 권한은 이 경우 개념으로 존재하며 구현 요구 사항에 나타날 수 있습니다. 그러나 애플리케이션에서는 사용자가 수행할 수 있는 권한이 있는 하나 이상의 작업을 처리하는 Roles만 정의하면 됩니다.
Roles에 부여하는 이름은 권한의 이름과 유사하며 사용자가 선택합니다. 권위와 비교할 때 역할이 거칠다고 말할 수 있습니다.
어쨌든 그 뒤에서 Roles은 Spring Security의 GrantedAuthority와 동일한 인터페이스를 사용하여 표현됩니다. Roles을 정의할 때 해당 이름은 ROLE_ 접두사로 시작해야 합니다. 구현 레벨에서 이 접두사는 Role과 권한 간의 차이를 지정합니다. 이 섹션에서 우리가 작업하는 예제는 ssia-ch7-ex3 프로젝트에서 찾을 수 있습니다.
다음 리스팅에서는 이전 예에서 변경한 사항을 살펴보세요.
Listing 7.9 Setting roles for users
@Configuration
public class ProjectConfig {
@Bean
public UserDetailsService userDetailsService() {
var manager = new InMemoryUserDetailsManager();
var user1 = User.withUsername("john")
.password("12345")
.authorities("ROLE_ADMIN") #A
.build();
var user2 = User.withUsername("jane")
.password("12345")
.authorities("ROLE_MANAGER")
.build();
manager.createUser(user1);
manager.createUser(user2);
return manager;
}
// Omitted code
}
#A authorities 메서드에 Roles을 설정하려면, ROLE_ 접두사를 같이 사용(ROLE_ADMIN)해야 합니다, 이제 GrantedAuthority는 Role을 나타냅니다.
사용자 Roles에 대한 제약을 설정하려면 다음 중 하나의 방법을 사용할 수 있습니다:
- hasRole() 메서드는 애플리케이션이 요청을 승인하는 Roles 이름을 파라미터로 받습니다.
- hasAnyRole() 메서드는 애플리케이션이 요청을 승인하는 Roles 이름들을 파라미터로 받습니다.
- access() - AuthorizationManager를 사용하여 애플리케이션이 요청을 승인하는 Roles을 지정합니다.
Role 측면에서 WebExpressionAuthorizationManager 구현과 함께 SpEL 표현식으로 hasRole() 또는 hasAnyRole()을 사용할 수 있습니다.
관찰한 대로 Roles 이름은 섹션 7.1.1에 제시된 메서드와 유사합니다. 이를 동일한 방식으로 사용하지만 권한 대신 Roles에 대한 구성을 적용합니다. 제 권장 사항도 비슷합니다. 첫 번째 옵션으로 hasRole() 또는 hasAnyRole() 메서드를 사용하고 이전 두 가지가 적용되지 않는 경우에만 access()를 사용하는 것으로 대체합니다. 다음 목록에서는 이제 구성() 메서드가 어떻게 보이는지 확인할 수 있습니다.
Listing 7.10 Configuring the app to accept only requests from admins
@Configuration
public class ProjectConfig {
// Omitted code
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http)
throws Exception {
http.httpBasic(Customizer.withDefaults());
http.authorizeHttpRequests(
c -> c.anyRequest().hasRole("ADMIN")
); #A
return http.build();
}
}
#A hasRole() 메서드는 이제 엔드포인트 접근을 허용하는 Roles을 지정합니다. 여기서 ROLE_ 접두사는 사용하지 않습니다.
참고 관찰해야 할 중요한 점은 Roles을 선언하는 데에만 ROLE_ 접두사를 사용한다는 것입니다. 하지만 Role을 사용할 때는 이름만 사용합니다.
애플리케이션을 테스트할 때 사용자 John이 엔드포인트에 액세스할 수 있는 반면 Jane은 HTTP 403 Forbidden을 수신하는 것을 관찰해야 합니다.
이 섹션 예제에서처럼 User 빌더 클래스를 사용하여 사용자를 생성할 때, roles() 메서드를 사용하여 Roles을 지정합니다. 이 메서드는 GrantedAuthority 객체를 생성하고 제공한 이름에 자동으로 ROLE_ 접두사를 추가합니다.
참고 roles() 메소드에 제공하는 파라미터에 ROLE_ 접두사가 포함되지 않는지 확인하십시오. 해당 접두사가 실수로 role() 메서드의 파라미터에 포함된 경우 메서드에서 예외가 발생합니다. 즉, authorities() 메소드를 사용할 때 ROLE_ 접두어를 포함하십시오. Role() 메서드를 사용할 때 ROLE_ 접두사를 포함하지 마세요.
다음 리스팅에서는 Roles을 기반으로 액세스를 설계할 때 authorities() 메서드 대신 roles() 메서드를 올바르게 사용하는 방법을 볼 수 있습니다. 또한 리스팅 7.11과 리스팅 7.9를 비교하여 권한과 Roles을 사용하는 차이를 관찰할 수 있습니다.
Listing 7.11 Setting up roles with the roles() method
@Configuration
public class ProjectConfig {
@Bean
public UserDetailsService userDetailsService() {
var manager = new InMemoryUserDetailsManager();
var user1 = User.withUsername("john")
.password("12345")
.roles("ADMIN") #A
.build();
var user2 = User.withUsername("jane")
.password("12345")
.roles("MANAGER")
.build();
manager.createUser(user1);
manager.createUser(user2);
return manager;
}
// Omitted code
}
#A roles() 메서드는 사용자의 역할을 지정합니다.
ACCESS() 메서드에 대해 더 알아보기
7.1.1 및 7.1.2 절에서는 access() 메서드를 사용하여 권한과 Roles에 따른 권한 부여 규칙을 적용하는 방법을 배웠습니다. 일반적으로 애플리케이션에서는 권한과 Roles과 관련된 권한 부여 제한이 있습니다. 그러나 access() 메서드는 일반적이며, 파라미터로 제공하는 AuthorizationManager 인터페이스의 구현에만 의존합니다. 또한, 우리의 예제에서는 SpEL expression을 기반으로 권한 부여 제한을 적용하는 WebExpressionAuthorizationManager 구현만 사용했습니다. 제시한 예제를 통해 권한과 Role에 대한 적용 방법을 가르치고 있지만,
실제로 WebExpressionAuthorizationManager는 어떤 SpEL 표현식이든 받을 수 있습니다. 이는 권한과 Role과 관련이 없어도 됩니다.
간단한 예로서, 엔드포인트에 대한 액세스를 오후 12시 이후에만 허용되도록 구성하는 것을 들 수 있습니다. 이와 같은 문제를 해결하기 위해 다음과 같은 SpEL 표현식을 사용할 수 있습니다:
"T(java.time.LocalTime).now().isAfter(T(java.time.LocalTime).of(12, 0))"
SpEL 표현식에 대한 자세한 내용은 Spring Framework 문서를 참조하세요
access() 메서드를 사용하면 기본적으로 어떤 종류의 규칙이든 구현할 수 있습니다. 가능성은 무한합니다. 그러나 애플리케이션에서는 항상 구문을 가능한 한 간단하게 유지하려고 노력해야 합니다. 다른 선택지가 없는 경우에만 구성을 복잡하게 만드세요. 이 예제는 ssia-ch7-ex4 프로젝트에서 적용된 것을 찾을 수 있습니다.
※ 위 제공된 코드에서 대문자 'T'는 SpEL(스프링 표현 언어)에서 사용되는 특수한 키워드입니다. 'T'는 타입을 참조하기 위한 키워드로, Java 클래스 또는 인터페이스를 참조할 때 사용됩니다. 이 코드에서 'T(java.time.LocalTime)'은 java.time 패키지의 LocalTime 클래스를 참조하고 있습니다. 따라서 'T(java.time.LocalTime)'은 LocalTime 클래스를 나타냅니다. 이러한 참조는 SpEL에서 자주 사용되며, 특히 Java의 클래스나 메서드를 SpEL 표현식 내에서 호출할 때 사용됩니다.
7.1.3 Restricting access to all endpoints
이 섹션에서는 모든 요청에 대한 액세스를 제한하는 방법에 대해 설명합니다. 섹션 5.2에서 permitAll() 메서드를 사용하여 모든 요청에 대한 액세스를 허용할 수 있다는 것을 배웠습니다. 또한 권한과 역할을 기반으로 액세스 규칙을 적용할 수 있다는 것도 배웠습니다. 그러나 모든 요청을 거부할 수도 있습니다. denyAll() 메서드는 permitAll() 메서드의 정반대입니다. 다음 목록에서는 denyAll() 메서드를 사용하는 방법을 볼 수 있습니다.
Listing 7.12 Using the denyAll() method to restrict access to endpoints
@Configuration
public class ProjectConfig {
// Omitted code
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http)
throws Exception {
http.httpBasic(Customizer.withDefaults());
http.authorizeHttpRequests(
c -> c.anyRequest().denyAll()
); #A
return http.basic();
}
}
#A Uses denyAll() to restrict access for everyone
그렇다면 이러한 제한을 어디에 사용할 수 있을까요? 다른 메서드만큼 자주 사용되는 것은 아니지만, 요구 사항에 따라 필요한 경우가 있습니다. 몇 가지 사례를 통해 이 점을 명확히 설명해 드리겠습니다.
url 템플릿 path 변수로 이메일 주소를 받는 엔드포인트가 있다고 가정해 보겠습니다. 변수 값이 .com으로 끝나는 주소에 대한 요청을 허용하고자 합니다. 다른 형식의 이메일 주소를 애플리케이션이 수용하지 않도록 원합니다. (다음 장에서는 path 및 HTTP 메서드를 기반으로 요청 그룹에 제한을 적용하는 방법 및 path 변수에 대한 제한을 적용하는 방법을 배우게 될 것입니다.) 이러한 요구 사항에 대해 규칙에 일치하는 요청을 그룹화하기 위해 정규 표현식을 사용한 다음 denyAll() 메서드를 사용하여 모든 이러한 요청을 거부하는 방식으로 응용 프로그램에 지시합니다. (그림 7.6).
그림 7.6 사용자가 .com으로 끝나는 파라미터 값을 사용하여 엔드포인트를 호출하면, 애플리케이션이 해당 요청을 수락합니다. 사용자가 .net으로 끝나는 이메일 주소를 제공하고 엔드포인트를 호출할 때는 애플리케이션이 호출을 거부합니다. 이와 같은 동작을 달성하려면 파라미터 값이 .com으로 끝나지 않는 모든 엔드포인트에 대해 denyAll() 메서드를 사용할 수 있습니다.
또한 그림 7.7과 같이 설계된 응용 프로그램을 상상해 볼 수 있습니다. 몇 가지 서비스는 응용 프로그램의 사용 케이스를 구현하며, 이는 서로 다른 path에서 사용 가능한 엔드포인트를 호출하여 액세스할 수 있습니다. 그러나 엔드포인트를 호출하려면 클라이언트가 게이트웨이라고 할 수 있는 다른 서비스를 요청해야 합니다. 이 아키텍처에서 이러한 유형의 두 개별 서비스가 있습니다. 그림 7.7에서 이를 게이트웨이 A 및 게이트웨이 B라고 부르겠습니다. 클라이언트가 /products path에 액세스하려면 게이트웨이 A를 요청합니다. 그러나 /articles path에 대해 클라이언트는 게이트웨이 B를 요청해야 합니다. 각 게이트웨이 서비스는 자신이 제공하지 않는 다른 path에 대한 모든 요청을 거부하기 위해 설계되었습니다. 이 간소화된 시나리오는 denyAll() 메서드를 쉽게 이해하는 데 도움이 됩니다. 실제 응용 프로그램에서는 더 복잡한 아키텍처에서 비슷한 경우를 발견할 수 있습니다.
그림 7.7에서 액세스는 게이트웨이 A와 게이트웨이 B를 통해 이루어집니다. 각각의 게이트웨이는 특정 경로에 대한 요청만 처리하고 나머지는 모두 거부합니다.
운영[production] 중인 애플리케이션은 때로는 이상해 보일 수 있는 다양한 아키텍처 요구 사항을 직면합니다. 프레임워크는 만날 수 있는 모든 상황에 필요한 유연성을 제공해야 합니다. 이러한 이유로 denyAll() 메서드는 이 장에서 배운 다른 옵션들만큼 중요합니다.
7.2 Summary
- 권한 부여는 인증된 요청이 허용되는지 여부를 애플리케이션이 결정하는 과정입니다. 권한 부여[인가]는 항상 인증 후에 발생합니다.
- 응용 프로그램이 인증된 사용자의 권한과 Role을 기반으로 요청을 인증하는 방법을 구성합니다.
- 응용 프로그램에서 인증되지 않은 사용자도 특정 요청을 할 수 있도록 설정할 수 있습니다.
- 앱을 구성하여 denyAll() 메서드를 사용해 모든 요청을 거부하거나 permitAll() 메서드를 사용해 모든 요청을 허용할 수 있습니다.
'Spring Security' 카테고리의 다른 글
ch09 Configuring Cross-Site Request Forgery(CSRF) protection (0) | 2024.03.03 |
---|---|
ch08 Configuring endpoint-level authorization: Applying restrictions (0) | 2024.03.02 |
ch06 Implementing authenticaiton (0) | 2024.02.26 |
ch05 A web app's security begins with filters (0) | 2024.02.25 |
ch04 Managing passwords (0) | 2024.02.25 |