Spring Security는 요청 수준에서 권한을 모델링할 수 있도록 지원합니다. 예를 들어, Spring Security를 사용하면 /admin 경로 아래의 모든 페이지는 특정 권한이 필요하고, 다른 모든 페이지는 단순히 인증만 필요하도록 설정할 수 있습니다.
기본적으로 Spring Security는 모든 요청이 인증을 필요로 한다고 요구합니다. 따라서 HttpSecurity 인스턴스를 사용할 때는 항상 권한 부여 규칙을 선언해야 합니다.
HttpSecurity 인스턴스를 사용할 경우 최소한 다음을 설정해야 합니다:
authorizeHttpRequests 사용http
.authorizeHttpRequests((authorize) -> authorize
.anyRequest().authenticated()
);
위 코드는 애플리케이션의 모든 엔드포인트가 최소한 인증된 보안 컨텍스트를 요구하도록 Spring Security에 알리는 설정입니다.
대부분의 경우, 권한 부여 규칙은 위보다 더 복잡할 수 있습니다. 따라서 다음의 사용 사례를 고려해 보세요:
authorizeRequests를 사용하는 앱을 authorizeHttpRequests로 마이그레이션하고 싶습니다.AuthorizationFilter 컴포넌트가 작동하는 방식을 이해하고 싶습니다.
Spring Security는 요청(Request) 수준에서 권한 부여를 관리하는 구조를 갖고 있습니다. 이를 구현하는 핵심 요소 중 하나가 AuthorizationFilter입니다. 아래에서 AuthorizationFilter가 요청을 어떻게 처리하는지 자세히 살펴보겠습니다.
AuthorizationFilter는 요청이 들어올 때 요청에 대한 권한 부여 과정을 관리합니다. 다음 단계별로 작동합니다.
Authentication 객체를 가져오는 Supplier를 생성합니다.Authentication 객체는 현재 사용자 또는 시스템의 인증 상태를 나타내며, 사용자의 권한(Authorities) 정보도 포함합니다.Supplier<Authentication>와 HttpServletRequest를 AuthorizationManager에 전달합니다.authorizeHttpRequests 메서드로 설정된 패턴과 요청을 매칭합니다./admin/** 경로는 ROLE_ADMIN 권한이 필요하다고 설정할 수 있습니다.AuthorizationDeniedEvent 이벤트가 발행됩니다.AccessDeniedException 예외가 발생합니다.ExceptionTranslationFilter에 의해 처리됩니다.AuthorizationGrantedEvent 이벤트가 발행됩니다.Spring Security는 AuthorizationFilter를 필터 체인의 마지막에 위치시킵니다.
이 설계는 다음과 같은 의미를 가집니다:
Spring Security에서 Spring MVC의 엔드포인트는 DispatcherServlet을 통해 처리됩니다.
하지만 DispatcherServlet은 AuthorizationFilter 이후에 실행되므로, 엔드포인트에 접근하려면 별도로 권한 규칙을 정의해야 합니다.
/admin 경로 설정http
.authorizeHttpRequests((authorize) -> authorize
.requestMatchers("/admin/**").hasRole("ADMIN") // /admin 경로에 ROLE_ADMIN 필요
.anyRequest().authenticated() // 나머지 요청은 인증 필요
);
/admin 경로가 authorizeHttpRequests에 포함되지 않았다면, 요청은 기본 규칙에 따라 인증만 요구하게 됩니다./admin/dashboard로 요청을 보냅니다./admin/** 패턴과 매칭되며, ROLE_ADMIN 권한이 있는지 확인합니다.ROLE_ADMIN 권한이 없으면 403 Forbidden 응답을 반환합니다.ROLE_ADMIN 권한을 가지고 있으면, 요청은 필터 체인을 계속 진행하며 컨트롤러로 전달됩니다.authorizeHttpRequests로 요청 권한을 세밀하게 설정할 수 있다.Spring Security를 사용하면 특정 엔드포인트에 대해 세밀한 권한 설정을 할 수 있습니다. 이 과정은 패턴(경로)과 규칙(필요 권한)을 쌍으로 작성하여 이루어지며, 규칙은 우선순위에 따라 적용됩니다. 아래에서 단계별로 자세히 설명합니다.
@Bean
public SecurityFilterChain web(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests((authorize) -> authorize
.requestMatchers("/endpoint").hasAuthority("USER") // /endpoint는 USER 권한 필요
.anyRequest().authenticated() // 나머지 요청은 인증만 필요
)
// 추가적인 보안 설정...
.and()
.formLogin(); // 로그인 페이지 설정
return http.build();
}
requestMatchers("/endpoint"):
/endpoint 경로에 대해 특정 권한(USER)이 필요합니다.anyRequest().authenticated():
규칙 처리 순서:
/endpoint는 첫 번째 규칙에서 처리되므로, 이후의 anyRequest() 규칙에는 영향을 받지 않습니다.Spring Security의 규칙은 우선순위에 따라 처리됩니다. 이를 고려하지 않으면 예상치 못한 동작이 발생할 수 있습니다.
@Bean
public SecurityFilterChain web(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests((authorize) -> authorize
.anyRequest().authenticated() // 모든 요청을 인증 요구
.requestMatchers("/endpoint").hasAuthority("USER") // /endpoint는 USER 권한 필요
);
return http.build();
}
/endpoint도 anyRequest() 규칙에 의해 단순 인증만 요구됩니다./endpoint에 USER 권한을 요구하려면 anyRequest()보다 앞에 배치해야 합니다.Spring Security는 애플리케이션의 권한 설정을 테스트할 수 있는 도구를 제공합니다. 아래는 /endpoint에 대한 권한 검증을 테스트하는 코드입니다.
@WithMockUser(authorities = "USER")
@Test
void endpointWhenUserAuthorityThenAuthorized() {
this.mvc.perform(get("/endpoint"))
.andExpect(status().isOk()); // USER 권한이 있으면 200 OK
}
@WithMockUser
@Test
void endpointWhenNotUserAuthorityThenForbidden() {
this.mvc.perform(get("/endpoint"))
.andExpect(status().isForbidden()); // USER 권한 없으면 403 Forbidden
}
@Test
void anyWhenUnauthenticatedThenUnauthorized() {
this.mvc.perform(get("/any"))
.andExpect(status().isUnauthorized()); // 인증되지 않은 경우 401 Unauthorized
}
@WithMockUser(authorities = "USER"):
USER 권한을 가진 가상의 사용자를 시뮬레이션합니다./endpoint 요청 결과는 200 OK여야 합니다.@WithMockUser:
ROLE_USER)만 가진 가상의 사용자를 시뮬레이션합니다./endpoint 요청은 USER 권한이 없으므로 403 Forbidden을 반환합니다.인증되지 않은 요청:
/any 요청은 401 Unauthorized를 반환합니다.Spring Security는 다양한 패턴과 규칙을 제공하며, 필요에 따라 사용자 정의 규칙도 추가할 수 있습니다.
requestMatchers("/exact/path")requestMatchers("/admin/**") // /admin 하위 모든 경로requestMatchers(HttpMethod.POST, "/create").hasAuthority("ADMIN")requestMatchers(Pattern.compile("/regex/.*"))Spring Security는 기본 제공 규칙 외에도 프로그래밍 방식으로 사용자 정의 권한 로직을 작성할 수 있습니다.
@Bean
public SecurityFilterChain web(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests((authorize) -> authorize
.requestMatchers("/custom").access((authentication, object) -> {
// 사용자 정의 로직
return authentication.get().getAuthorities().stream()
.anyMatch(grantedAuthority -> grantedAuthority.getAuthority().equals("CUSTOM_ROLE"));
})
)
.anyRequest().authenticated();
return http.build();
}
규칙 설정 시 우선순위가 중요:
Spring Security는 다양한 패턴과 규칙을 지원:
테스트를 통해 설정 검증:
@WithMockUser를 사용해 다양한 사용자 권한을 시뮬레이션할 수 있습니다.간단한 규칙 예제:
http
.authorizeHttpRequests((authorize) -> authorize
.requestMatchers("/admin/**").hasRole("ADMIN")
.requestMatchers(HttpMethod.POST, "/create").hasAuthority("USER")
.anyRequest().authenticated()
);
Spring Security는 규칙의 유연성과 강력한 테스트 지원을 통해 권한 관리를 효과적으로 처리할 수 있습니다.
위에서 이미 요청을 매칭하는 두 가지 방법을 보았습니다.
첫 번째는 모든 요청을 매칭하는 가장 간단한 방법입니다.
두 번째는 URI 패턴을 사용하여 매칭하는 방법입니다. Spring Security는 URI 패턴 매칭을 위해 두 가지 언어를 지원합니다:
Ant 스타일 패턴(위에서 본 것처럼)과 정규식(Regular Expressions)입니다.
Spring Security는 요청 매칭을 위해 기본적으로 Ant 스타일 패턴을 사용합니다. Ant는 직관적이고 간단한 방식으로 특정 경로(엔드포인트) 또는 디렉토리 구조를 매칭할 수 있도록 설계되었습니다. 또한, 요청에서 경로 값을 추출하거나 HTTP 메서드별로 매칭을 세분화할 수 있습니다.
?: 한 글자를 대체./file?.txt → /file1.txt, /file2.txt 매칭.*: 경로의 일부를 대체./files/* → /files/a.txt, /files/b.png 매칭./files/** → /files/a.txt, /files/subdir/b.png 등 모든 하위 경로 매칭./resource 하위 모든 경로 매칭http
.authorizeHttpRequests((authorize) -> authorize
.requestMatchers("/resource/**").hasAuthority("USER")
.anyRequest().authenticated()
);
/resource 또는 /resource 하위 경로(/resource/file, /resource/subdir/file 등)에 대한 요청은 USER 권한을 요구합니다.위 코드는 다음과 같이 읽을 수 있습니다:
"요청이 /resource 또는 하위 디렉토리라면 USER 권한이 필요하며, 나머지 요청은 인증만 필요하다."
Spring Security에서는 요청 경로에서 경로 변수(Path Variable) 값을 추출해 사용할 수 있습니다. 이를 통해 요청 조건을 세밀하게 설정할 수 있습니다.
name 값을 추출http
.authorizeHttpRequests((authorize) -> authorize
.requestMatchers("/resource/{name}")
.access(new WebExpressionAuthorizationManager("#name == authentication.name"))
.anyRequest().authenticated()
);
/resource/{name}에서 {name} 값을 추출합니다.#name == authentication.name:authentication.name)와 요청 경로의 name 값이 동일한 경우만 접근 허용.위 코드는 다음과 같이 읽을 수 있습니다:
"요청 경로 /resource/{name}에서 {name} 값이 인증된 사용자의 이름과 일치하면 요청을 허용한다."
특정 HTTP 메서드(GET, POST 등)에 대해서만 조건을 설정할 수 있습니다.
http
.authorizeHttpRequests((authorize) -> authorize
.requestMatchers(HttpMethod.POST, "/resource/**").hasAuthority("USER")
.anyRequest().authenticated()
);
/resource 경로와 하위 경로에서 POST 요청은 USER 권한을 요구합니다.Spring Security는 설정된 권한이 제대로 동작하는지 확인하기 위해 테스트 도구를 제공합니다.
@WithMockUser(authorities = "USER")
@Test
void endpointWhenUserAuthorityThenAuthorized() {
this.mvc.perform(get("/endpoint/jon"))
.andExpect(status().isOk()); // USER 권한이 있으면 200 OK
}
@WithMockUser
@Test
void endpointWhenNotUserAuthorityThenForbidden() {
this.mvc.perform(get("/endpoint/jon"))
.andExpect(status().isForbidden()); // USER 권한 없으면 403 Forbidden
}
@Test
void anyWhenUnauthenticatedThenUnauthorized() {
this.mvc.perform(get("/any"))
.andExpect(status().isUnauthorized()); // 인증되지 않은 경우 401 Unauthorized
}
@WithMockUser(authorities = "USER"):
USER 권한을 가진 가상의 사용자를 생성합니다./endpoint/jon 요청이 200 OK인지 테스트합니다.@WithMockUser:
/endpoint/jon 요청이 USER 권한 부족으로 403 Forbidden을 반환하는지 테스트합니다.인증되지 않은 요청:
/any 요청을 보냅니다.Spring Security는 기본적으로 요청의 경로(Path)만 매칭합니다.
쿼리 파라미터(Query Parameters)를 매칭하려면 사용자 정의 요청 매처(Custom Request Matcher)를 작성해야 합니다.
http
.authorizeHttpRequests((authorize) -> authorize
.requestMatchers(new CustomRequestMatcher()) // 사용자 정의 매처 사용
.anyRequest().authenticated()
);
public class CustomRequestMatcher implements RequestMatcher {
@Override
public boolean matches(HttpServletRequest request) {
String param = request.getParameter("param");
return "value".equals(param); // 특정 파라미터 조건
}
}
Ant 패턴은 직관적인 매칭 도구
*, **, {name} 등을 사용해 단순하거나 계층적인 경로를 매칭 가능.HTTP 메서드별 세부 조건 설정
쿼리 파라미터 매칭은 기본적으로 지원하지 않음
RequestMatcher를 작성해 구현 가능합니다.테스트로 설정 검증
Spring Security는 유연한 요청 매칭 방식과 다양한 확장 가능성을 제공해 애플리케이션 보안을 정밀하게 제어할 수 있습니다.
Spring Security는 요청을 정규식(Regular Expression)을 사용해 매칭하는 기능을 지원합니다. 이는 ``**을 사용하는 디렉토리 매칭보다 더 엄격한 매칭 기준을 적용해야 할 때 유용합니다.
사용자 이름이 반드시 영문자와 숫자로만 이루어져야 한다는 규칙이 있을 경우, 정규식을 사용해 이를 처리할 수 있습니다.
http
.authorizeHttpRequests((authorize) -> authorize
.requestMatchers(RegexRequestMatcher.regexMatcher("/resource/[A-Za-z0-9]+")).hasAuthority("USER") // 정규식 매칭
.anyRequest().denyAll() // 그 외의 모든 요청은 거부
);
/resource/[A-Za-z0-9]+:
/resource/ 뒤에 하나 이상의 영문자 또는 숫자가 오는 경로만 매칭합니다./resource/john123, /resource/abc123는 매칭됨./resource/john_123는 매칭되지 않음.hasAuthority("USER"):
USER 권한이 있어야만 접근 가능합니다.anyRequest().denyAll():
Spring Security에서는 HTTP 메서드(GET, POST 등)에 따라 권한을 설정할 수 있습니다.
특히, 읽기(read) 또는 쓰기(write)와 같은 권한에 따라 요청을 구분할 때 유용합니다.
http
.authorizeHttpRequests((authorize) -> authorize
.requestMatchers(HttpMethod.GET).hasAuthority("read") // GET 요청은 read 권한 필요
.requestMatchers(HttpMethod.POST).hasAuthority("write") // POST 요청은 write 권한 필요
.anyRequest().denyAll() // 그 외의 요청은 거부
);
HttpMethod.GET:
read 권한을 요구합니다.HttpMethod.POST:
write 권한을 요구합니다.anyRequest().denyAll():
위 코드는 다음과 같이 읽을 수 있습니다:
read 권한 필요.write 권한 필요.Spring Security의 테스트 지원 기능을 사용하여 권한 설정이 올바르게 동작하는지 확인할 수 있습니다.
@WithMockUser(authorities="read")
@Test
void getWhenReadAuthorityThenAuthorized() {
this.mvc.perform(get("/any"))
.andExpect(status().isOk()); // read 권한으로 GET 요청이 성공(200 OK)
}
@WithMockUser
@Test
void getWhenNoReadAuthorityThenForbidden() {
this.mvc.perform(get("/any"))
.andExpect(status().isForbidden()); // read 권한 없이 GET 요청이 거부(403 Forbidden)
}
@WithMockUser(authorities="write")
@Test
void postWhenWriteAuthorityThenAuthorized() {
this.mvc.perform(post("/any").with(csrf()))
.andExpect(status().isOk()); // write 권한으로 POST 요청이 성공(200 OK)
}
@WithMockUser(authorities="read")
@Test
void postWhenNoWriteAuthorityThenForbidden() {
this.mvc.perform(post("/any").with(csrf()))
.andExpect(status().isForbidden()); // read 권한으로 POST 요청이 거부(403 Forbidden)
}
@WithMockUser(authorities="read"):
read 권한을 가진 사용자로 GET 요청을 테스트합니다.@WithMockUser(authorities="write"):
write 권한을 가진 사용자로 POST 요청을 테스트합니다.CSRF 토큰 추가 (.with(csrf())):
HTTP 메서드별 권한 설정:
기본 거부 정책:
anyRequest().denyAll()를 사용해 기본적으로 요청을 거부하는 것은 보안상 권장됩니다.테스트로 검증:
@WithMockUser와 CSRF 토큰 설정을 활용하여 다양한 시나리오를 테스트할 수 있습니다.이 기능은 현재 XML에서는 지원되지 않습니다.
앞서 설명한 것처럼, Spring Security는 기본적으로 모든 Dispatcher Type에 대해 권한을 검사합니다.
REQUEST 디스패치에서 설정된 보안 컨텍스트는 이후의 디스패치(FORWARD, ERROR 등)로도 전달되지만, 미묘한 불일치로 인해 예기치 않은 AccessDeniedException이 발생할 수 있습니다.
이를 해결하기 위해, Java 설정을 사용하여 FORWARD 및 ERROR와 같은 Dispatcher Type을 허용하도록 설정할 수 있습니다.
http
.authorizeHttpRequests((authorize) -> authorize
.dispatcherTypeMatchers(DispatcherType.FORWARD, DispatcherType.ERROR).permitAll() // FORWARD와 ERROR 허용
.requestMatchers("/endpoint").permitAll() // 특정 엔드포인트 허용
.anyRequest().denyAll() // 그 외의 요청은 거부
);
dispatcherTypeMatchers(DispatcherType.FORWARD, DispatcherType.ERROR):requestMatchers("/endpoint").permitAll():/endpoint 요청을 모든 사용자에게 허용합니다.anyRequest().denyAll():일반적으로는 위에서 설명한 것처럼 requestMatchers(String)을 사용할 수 있습니다.
하지만 Spring MVC를 기본 경로(/)가 아닌 다른 서블릿 경로로 매핑하는 경우, 보안 설정에서 이를 고려해야 합니다.
Spring MVC가 기본 경로(/) 대신 /spring-mvc로 매핑된 경우,
/spring-mvc/my/controller와 같은 엔드포인트에 대해 권한을 설정하려면 아래와 같이 설정해야 합니다.
@Bean
MvcRequestMatcher.Builder mvc(HandlerMappingIntrospector introspector) {
return new MvcRequestMatcher.Builder(introspector).servletPath("/spring-mvc"); // 서블릿 경로 지정
}
@Bean
SecurityFilterChain appEndpoints(HttpSecurity http, MvcRequestMatcher.Builder mvc) {
http
.authorizeHttpRequests((authorize) -> authorize
.requestMatchers(mvc.pattern("/my/controller/**")).hasAuthority("controller") // 컨트롤러 경로 권한 설정
.anyRequest().authenticated() // 나머지 요청은 인증만 필요
);
return http.build();
}
다음과 같은 상황에서 MvcRequestMatcher를 사용할 필요가 생깁니다:
1. spring.mvc.servlet.path 속성을 사용해 기본 경로(/)를 다른 경로로 변경한 경우.
2. Spring MVC DispatcherServlet을 여러 개 등록하여 기본 경로가 아닌 경로를 사용하는 경우.
Spring Security에서 기본 제공 RequestMatcher 이외에도 사용자 정의 RequestMatcher를 만들어 특정 요청에 대해 권한 부여를 제어할 수 있습니다.
이는 요청의 특정 조건(예: 쿼리 파라미터 존재 여부)에 따라 권한을 동적으로 부여할 때 유용합니다.
RequestMatcher는 요청(HttpServletRequest)이 특정 조건을 만족하는지 확인하는 함수형 인터페이스입니다.
Spring Security의 authorizeHttpRequests() DSL에 전달하여 요청 조건을 기반으로 권한을 설정할 수 있습니다.
@FunctionalInterface
public interface RequestMatcher {
boolean matches(HttpServletRequest request);
}
matches(HttpServletRequest request):람다를 활용하여 간단히 RequestMatcher를 정의할 수 있습니다.
RequestMatcher printview = (request) -> request.getParameter("print") != null;
http
.authorizeHttpRequests((authorize) -> authorize
.requestMatchers(printview).hasAuthority("print") // print 파라미터가 있는 경우 print 권한 필요
.anyRequest().authenticated() // 나머지 요청은 인증만 필요
);
printview Matcher:
print가 있는지 확인합니다./any?print=1 요청은 matches()가 true를 반환.requestMatchers(printview).hasAuthority("print"):
printview 조건을 만족하는 요청은 print 권한이 있어야만 접근 허용.anyRequest().authenticated():
보다 복잡한 조건을 처리하거나 상태를 유지해야 하는 경우, RequestMatcher를 구현한 구체 클래스를 작성할 수 있습니다.
public class PrintRequestMatcher implements RequestMatcher {
@Override
public boolean matches(HttpServletRequest request) {
return request.getParameter("print") != null; // print 파라미터가 있는지 확인
}
}
이후 설정:
RequestMatcher printview = new PrintRequestMatcher();
http
.authorizeHttpRequests((authorize) -> authorize
.requestMatchers(printview).hasAuthority("print")
.anyRequest().authenticated()
);
Spring Security는 요청 조건 및 권한 설정을 검증하기 위한 테스트 도구를 제공합니다.
@WithMockUser(authorities="print")
@Test
void printWhenPrintAuthorityThenAuthorized() {
this.mvc.perform(get("/any?print")) // print 파라미터가 포함된 요청
.andExpect(status().isOk()); // print 권한이 있으면 200 OK
}
@WithMockUser
@Test
void printWhenNoPrintAuthorityThenForbidden() {
this.mvc.perform(get("/any?print")) // print 파라미터가 포함된 요청
.andExpect(status().isForbidden()); // print 권한이 없으면 403 Forbidden
}
@WithMockUser(authorities="print"):
print 권한을 가진 가상의 사용자를 생성합니다.GET /any?print 요청은 조건에 맞으므로 200 OK를 반환해야 합니다.권한 없는 사용자 테스트:
@WithMockUser로 기본 권한 사용자 또는 권한 없는 사용자로 요청을 보냅니다.CSRF 고려:
.with(csrf())를 추가해야 합니다.RequestMatcher adminParam = (request) -> "admin".equals(request.getParameter("role"));
http
.authorizeHttpRequests((authorize) -> authorize
.requestMatchers(adminParam).hasRole("ADMIN")
.anyRequest().authenticated()
);
?role=admin 파라미터가 있을 때만 ADMIN 역할 필요.RequestMatcher customHeader = (request) -> "special-value".equals(request.getHeader("X-Custom-Header"));
http
.authorizeHttpRequests((authorize) -> authorize
.requestMatchers(customHeader).hasAuthority("special")
.anyRequest().authenticated()
);
X-Custom-Header에 특정 값이 있을 때만 권한 필요.RequestMatcher ipMatcher = (request) -> "192.168.1.1".equals(request.getRemoteAddr());
http
.authorizeHttpRequests((authorize) -> authorize
.requestMatchers(ipMatcher).permitAll() // 특정 IP에서의 요청은 모두 허용
.anyRequest().authenticated()
);
RequestMatcher는 요청의 특정 조건을 기반으로 권한을 설정할 수 있는 인터페이스입니다.
사용 사례
테스트 검증
보안 모범 사례
요청이 매칭되면, 이미 본 것처럼 여러 방식으로 권한을 부여할 수 있습니다. 예를 들어 permitAll, denyAll, hasAuthority를 사용할 수 있습니다.
permitAll:
denyAll:
hasAuthority:
hasRole:
ROLE_ 접두사(또는 설정된 접두사)를 추가합니다.hasAnyAuthority:
hasAnyRole:
ROLE_ 접두사(또는 설정된 접두사)를 추가합니다.access:
아래는 패턴과 규칙을 결합하여 작성된 더 복잡한 예제입니다:
import static jakarta.servlet.DispatcherType.*;
import static org.springframework.security.authorization.AuthorizationManagers.allOf;
import static org.springframework.security.authorization.AuthorityAuthorizationManager.hasAuthority;
import static org.springframework.security.authorization.AuthorityAuthorizationManager.hasRole;
@Bean
SecurityFilterChain web(HttpSecurity http) throws Exception {
http
// ...
.authorizeHttpRequests(authorize -> authorize
.dispatcherTypeMatchers(FORWARD, ERROR).permitAll() // FORWARD 및 ERROR 디스패치 허용
.requestMatchers("/static/**", "/signup", "/about").permitAll() // 공용 URL
.requestMatchers("/admin/**").hasRole("ADMIN") // /admin/ 경로에 ROLE_ADMIN 필요
.requestMatchers("/db/**").access(allOf(hasAuthority("db"), hasRole("ADMIN"))) // db 권한과 ROLE_ADMIN 필요
.anyRequest().denyAll() // 그 외의 요청은 거부
);
return http.build();
}
여러 권한 규칙이 지정됨
FORWARD 및 ERROR 디스패치 허용
공용 URL 패턴
/static/로 시작하거나 /signup, /about 경로는 모든 사용자에게 허용./admin/ 경로
ROLE_ADMIN 역할을 가진 사용자만 접근 가능. hasRole을 호출하므로 ROLE_ 접두사를 명시하지 않아도 됩니다./db/ 경로
db 권한이 부여되고, 동시에 ROLE_ADMIN 역할이 있어야 접근 가능. hasRole을 사용하므로 ROLE_ 접두사를 명시하지 않아도 됩니다.기본 거부
Spring Security는 권한 부여를 설정할 때, 일반적으로 AuthorizationManager를 사용하는 것을 권장합니다. 하지만 특정 상황에서는 SpEL(Spring Expression Language)을 사용해 권한을 표현해야 할 때가 있습니다.
이러한 경우는 다음과 같습니다:
<intercept-url>을 사용하는 경우.SpEL을 사용하면 표현식 기반으로 권한 조건을 유연하게 정의할 수 있습니다.
Spring Security는 권한 부여 표현식을 평가할 때 루트 객체(root object)를 제공합니다.
이 객체는 SpEL 표현식에서 사용할 수 있는 필드와 메서드를 포함하며, 주로 다음 두 가지 객체를 사용합니다:
SecurityExpressionRoot
WebSecurityExpressionRoot
Spring Security의 SpEL은 권한 부여 표현식을 평가하기 위해 다양한 필드와 메서드를 제공합니다.
아래는 SpEL에서 사용할 수 있는 주요 필드와 메서드입니다:
authentication
authentication.name → 사용자 이름.principal
principal.username.roles
hasRole('ADMIN').hasRole(String role)
hasRole('ADMIN').hasAuthority(String authority)
hasAuthority('WRITE_PRIVILEGE').hasAnyRole(String... roles)
hasAnyRole('ADMIN', 'USER').hasAnyAuthority(String... authorities)
hasAnyAuthority('READ_PRIVILEGE', 'WRITE_PRIVILEGE').permitAll() / denyAll()
isAuthenticated()
isAnonymous()
isRememberMe()
<intercept-url> 설정<http use-expressions="true">
<intercept-url pattern="/admin/**" access="hasRole('ADMIN')" />
<intercept-url pattern="/user/**" access="hasAuthority('READ_PRIVILEGE')" />
<intercept-url pattern="/public/**" access="permitAll()" />
</http>
http
.authorizeHttpRequests(authorize -> authorize
.requestMatchers("/admin/**").access("hasRole('ADMIN')") // ADMIN 역할 필요
.requestMatchers("/user/**").access("hasAuthority('READ_PRIVILEGE')") // READ_PRIVILEGE 권한 필요
.requestMatchers("/public/**").access("permitAll()") // 모든 사용자 접근 허용
);
<sec:authorize access="hasRole('ADMIN')">
<p>관리자만 볼 수 있는 내용</p>
</sec:authorize>
<sec:authorize access="isAuthenticated()">
<p>로그인된 사용자만 볼 수 있는 내용</p>
</sec:authorize>
SecurityExpressionRoot 및 WebSecurityExpressionRoot 사용
SecurityExpressionRoot 또는 WebSecurityExpressionRoot를 SpEL의 컨텍스트로 제공합니다.StandardEvaluationContext
StandardEvaluationContext를 사용하여 권한 부여 표현식을 평가합니다.SecurityExpressionRoot 객체를 설정하여 평가를 수행합니다.http
.authorizeHttpRequests(authorize -> authorize
.requestMatchers("/secure/**").access("hasRole('ADMIN') and hasAuthority('WRITE_PRIVILEGE')")
.requestMatchers("/audit/**").access("hasRole('AUDITOR') or hasAuthority('VIEW_PRIVILEGE')")
);
/secure/**:
ADMIN 역할과 WRITE_PRIVILEGE 권한을 모두 가진 사용자만 접근 가능./audit/**:
AUDITOR 역할 또는 VIEW_PRIVILEGE 권한 중 하나를 가진 사용자는 접근 가능.SpEL을 통해 복잡한 권한 조건 설정 가능
다양한 환경에서 활용
복합 조건 처리
사용 권장 상황
Spring Security는 SpEL(Spring Expression Language)을 활용하여 권한 부여를 유연하게 설정할 수 있습니다. 이를 위해 Authorization Expression 필드와 메서드를 제공합니다.
이 섹션에서는 SpEL에서 사용할 수 있는 주요 필드, 메서드와 복합적인 예제에 대해 설명합니다.
SpEL 표현식에서 자주 사용되는 메서드는 다음과 같습니다:
permitAll
denyAll
hasAuthority(String authority)
hasAuthority('WRITE_PRIVILEGE').hasRole(String role)
ROLE_ 접두사가 추가됩니다.hasRole('ADMIN') → hasAuthority('ROLE_ADMIN').hasAnyAuthority(String... authorities)
hasAnyAuthority('READ_PRIVILEGE', 'WRITE_PRIVILEGE').hasAnyRole(String... roles)
ROLE_ 접두사가 추가됩니다.hasAnyRole('ADMIN', 'USER') → hasAnyAuthority('ROLE_ADMIN', 'ROLE_USER').hasPermission(Object target, String permission)
hasPermission(#document, 'read').SpEL에서 사용할 수 있는 주요 필드는 다음과 같습니다:
authentication
authentication.name → 현재 사용자의 이름.principal
principal.username → 현재 사용자의 사용자 이름.<http>
<intercept-url pattern="/static/**" access="permitAll"/>
<intercept-url pattern="/admin/**" access="hasRole('ADMIN')"/>
<intercept-url pattern="/db/**" access="hasAuthority('db') and hasRole('ADMIN')"/>
<intercept-url pattern="/**" access="denyAll"/>
</http>
`/static/`**
/static/로 시작하는 모든 요청은 공용입니다. `/admin/`**
/admin/로 시작하는 모든 요청은 ROLE_ADMIN 역할을 가진 사용자만 접근 가능합니다. hasRole 메서드를 사용하므로 ROLE_ 접두사를 명시하지 않아도 됩니다.`/db/`**
/db/로 시작하는 요청은 다음 조건을 모두 충족해야 접근할 수 있습니다:db 권한을 가진 사용자.ROLE_ADMIN 역할을 가진 사용자.기본 거부 (`/`)**
<intercept-url pattern="/secure/**" access="hasAuthority('WRITE_PRIVILEGE') and hasRole('MANAGER')"/>
WRITE_PRIVILEGE 권한이 있어야 함.ROLE_MANAGER 역할이 있어야 함./secure/ 경로에 접근 가능.유연성:
and, or, not)를 활용해 권한 조건을 결합 가능.다양한 활용:
객체 수준 권한 부여:
복잡성:
보안 문제:
SpEL의 주요 메서드와 필드
permitAll, denyAll, hasRole, hasAuthority, hasPermission 등. authentication, principal.복합적인 조건 설정
and, or, not)를 활용해 여러 조건을 결합 가능.보안 모범 사례
denyAll)하고, 필요한 요청만 명시적으로 허용하는 Allow List 방식을 따르세요.적용 방식
Spring Security는 요청 경로에서 경로 변수(Path Parameters)를 추출하여 SpEL 표현식에서 사용할 수 있는 메커니즘을 제공합니다. 이를 통해 경로 변수의 값에 따라 동적으로 권한을 설정할 수 있습니다.
<http>
<intercept-url pattern="/resource/{name}" access="#name == authentication.name"/>
<intercept-url pattern="/**" access="authenticated"/>
</http>
/resource/{name}:
/resource/{name}에서 {name} 값이 SpEL 표현식의 #name으로 바인딩됩니다.SpEL 조건:
#name == authentication.name: #name)이 현재 인증된 사용자의 이름(authentication.name)과 동일해야 접근을 허용합니다.기본 규칙:
authenticated)만 접근할 수 있습니다.Spring Security는 경로 변수를 SpEL에서 사용할 수 있도록 자동으로 제공하며, 이를 통해 경로 값 기반의 동적 권한 부여가 가능합니다.
/resource/{name})을 매칭합니다.{name}의 값을 추출하여 SpEL 표현식의 #name에 바인딩합니다.#name 값을 사용해 권한 조건을 평가합니다.<http>
<intercept-url pattern="/resource/{name}" access="#name == authentication.name and hasRole('USER')"/>
<intercept-url pattern="/admin/{adminName}" access="#adminName == authentication.name and hasRole('ADMIN')"/>
<intercept-url pattern="/**" access="authenticated"/>
</http>
/resource/{name}:
name이 인증된 사용자의 이름(authentication.name)과 같아야 함. USER 역할(ROLE_USER)을 가져야 접근 허용./admin/{adminName}:
adminName이 인증된 사용자의 이름과 같아야 함. ADMIN 역할(ROLE_ADMIN)을 가져야 접근 허용.기본 조건:
Spring Security에서 Java DSL을 사용하여 경로 변수 기반의 권한 설정을 정의할 수도 있습니다.
http
.authorizeHttpRequests(authorize -> authorize
.requestMatchers("/resource/{name}").access("#name == authentication.name")
.requestMatchers("/admin/{adminName}").access("#adminName == authentication.name and hasRole('ADMIN')")
.anyRequest().authenticated()
);
SpEL 표현식의 정확성:
권한 조건의 가독성:
보안 모범 사례:
denyAll)하고, 필요한 요청만 명시적으로 허용하는 방식을 따르세요.경로 변수 추출:
동적 권한 부여:
authentication)를 비교하여 조건부로 접근을 허용할 수 있습니다.XML과 Java DSL 모두 지원:
실제 사용 사례:
Spring Security의 경로 변수 활용은 경로 값 기반의 세밀한 보안 정책을 구현하는 데 매우 유용합니다.
Spring Security에서 권한 부여를 위해 별도의 서비스(예: 외부 정책 에이전트, 데이터베이스)를 사용할 수 있습니다. 이를 위해 커스텀 AuthorizationManager를 작성하여 권한 부여 로직을 정의하고 Spring Security에 통합할 수 있습니다.
Spring Security는 AuthorizationManager를 통해 권한을 결정합니다. 커스텀 AuthorizationManager를 작성하여 외부 서비스나 정책 에이전트를 활용할 수 있습니다.
@Component
public final class OpenPolicyAgentAuthorizationManager implements AuthorizationManager<RequestAuthorizationContext> {
@Override
public AuthorizationDecision check(Supplier<Authentication> authentication, RequestAuthorizationContext context) {
// Open Policy Agent에 요청을 보내 권한을 확인
// 예: 외부 서비스에서 권한 확인 후 결과 반환
boolean isAuthorized = makeRequestToPolicyAgent(authentication.get(), context);
return new AuthorizationDecision(isAuthorized);
}
private boolean makeRequestToPolicyAgent(Authentication authentication, RequestAuthorizationContext context) {
// 외부 서비스 호출 로직
// 인증 정보와 요청 정보를 전달하여 권한 결과 확인
return true; // 권한 부여 결과 반환
}
}
check 메서드:
Authentication 객체와 요청 컨텍스트를 사용하여 외부 서비스에 요청을 보냅니다.makeRequestToPolicyAgent 메서드:
커스텀 AuthorizationManager를 작성한 후, Spring Security의 권한 설정에 추가합니다.
@Bean
SecurityFilterChain web(HttpSecurity http, AuthorizationManager<RequestAuthorizationContext> authz) throws Exception {
http
.authorizeHttpRequests((authorize) -> authorize
.anyRequest().access(authz) // 모든 요청을 커스텀 AuthorizationManager로 처리
);
return http.build();
}
anyRequest().access(authz): permitAll 사용 권장정적 리소스(예: CSS, JS, 이미지 파일 등)를 처리할 때, 필터 체인에서 무시(ignore)하도록 설정할 수 있습니다. 그러나 Spring Security에서는 permitAll을 사용하는 것이 더 안전한 방법이라고 권장합니다.
permitAll로 허용http
.authorizeHttpRequests((authorize) -> authorize
.requestMatchers("/css/**").permitAll() // CSS 파일에 대한 모든 요청 허용
.anyRequest().authenticated() // 나머지 요청은 인증 필요
);
permitAll이 더 안전한가?보안 헤더 적용 가능:
permitAll을 사용하면 보안 헤더가 정적 리소스에도 적용됩니다.Spring Security 6에서 성능 개선:
permitAll을 사용할 수 있습니다.permitAll로 처리하세요.http
.authorizeHttpRequests((authorize) -> authorize
.anyRequest().denyAll()
);커스텀 AuthorizationManager 작성
Spring Security 통합
정적 리소스 처리
permitAll을 사용해 허용하며, 보안 헤더를 적용합니다.보안 설정
이 설정 방식은 권한 부여의 유연성과 보안을 모두 충족하는 데 효과적입니다.
authorizeRequests에서 authorizeHttpRequests로 마이그레이션Spring Security는 authorizeHttpRequests를 도입하면서 기존의 authorizeRequests를 대체할 수 있는 더 간결하고 유연한 방식을 제공합니다. AuthorizationFilter는 기존의 FilterSecurityInterceptor를 대체하며, authorizeHttpRequests를 사용하면 보다 효율적이고 단순화된 권한 부여 구성을 구현할 수 있습니다.
authorizeHttpRequests의 특징authorizeHttpRequests는 다음과 같은 이점을 제공합니다:
간소화된 AuthorizationManager API 사용
Authentication 조회 지연
Bean 기반 구성 지원
authorizeHttpRequests를 사용하면 AuthorizationFilter가 활성화됩니다. authorizeRequests)@Bean
SecurityFilterChain web(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.anyRequest().authenticated();
return http.build();
}
authorizeHttpRequests)@Bean
SecurityFilterChain web(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests((authorize) -> authorize
.anyRequest().authenticated() // 모든 요청은 인증 필요
);
return http.build();
}
Spring Security에서는 SpEL을 사용하여 권한을 정의할 수 있습니다. 하지만 권장되는 방식은 타입 안전한 AuthorizationManager를 사용하는 것입니다.
기존의 SpEL 표현식을 WebExpressionAuthorizationManager로 마이그레이션할 수 있습니다.
.requestMatchers("/test/**")
.access(new WebExpressionAuthorizationManager("hasRole('ADMIN') && hasRole('USER')"))
WebExpressionAuthorizationManager는 기존 SpEL 표현식을 처리하기 위한 도우미 클래스입니다.SpEL에서 Bean 참조를 포함하는 표현식을 사용하는 경우, Bean을 직접 호출하는 방식으로 변경하는 것이 권장됩니다.
.access("@webSecurity.check(authentication, request)")
.requestMatchers("/test/**")
.access((authentication, context) ->
new AuthorizationDecision(webSecurity.check(authentication.get(), context.getRequest())))
webSecurity.check: Bean 메서드를 직접 호출하여 권한 부여를 수행합니다.AuthorizationDecision: 권한 부여 결과를 캡슐화하여 반환합니다.SpEL 표현식이 복잡하고 Bean 참조 및 기타 논리가 결합된 경우, 커스텀 AuthorizationManager를 구현하는 것이 좋습니다.
public class CustomAuthorizationManager implements AuthorizationManager<RequestAuthorizationContext> {
@Override
public AuthorizationDecision check(Supplier<Authentication> authentication, RequestAuthorizationContext context) {
// Bean 호출 및 기타 논리를 구현
boolean result = someBean.check(authentication.get(), context.getRequest());
return new AuthorizationDecision(result);
}
}
.requestMatchers("/test/**").access(new CustomAuthorizationManager())
SpEL 표현식에 Bean 참조가 포함되며 이를 직접 대체할 수 없는 경우, DefaultHttpSecurityExpressionHandler를 활용해 Bean 리졸버를 설정해야 합니다.
DefaultHttpSecurityExpressionHandler expressionHandler = new DefaultHttpSecurityExpressionHandler();
expressionHandler.setBeanResolver(applicationContext.getBeanResolver());
WebExpressionAuthorizationManager manager = new WebExpressionAuthorizationManager("expression");
manager.setExpressionHandler(expressionHandler);
DefaultHttpSecurityExpressionHandler: SpEL 표현식에서 Bean을 찾을 수 있도록 구성.setExpressionHandler: WebExpressionAuthorizationManager에 핸들러를 설정.정적 리소스(/css/**, /js/**)를 무시하도록 구성하는 방식:
http
.ignoring()
.requestMatchers("/css/**", "/js/**");
permitAll로 변경더 안전한 방법은 permitAll로 처리하여 Spring Security의 보안 헤더가 적용되도록 하는 것입니다:
http
.authorizeHttpRequests((authorize) -> authorize
.requestMatchers("/css/**", "/js/**").permitAll()
.anyRequest().authenticated()
);
permitAll을 사용하면 정적 리소스에도 보안 헤더가 적용됩니다.authorizeHttpRequests 도입
authorizeRequests를 대체하며 더 간결하고 유연한 권한 설정을 지원.SpEL 마이그레이션
WebExpressionAuthorizationManager로 이전 가능.정적 리소스 처리
permitAll을 사용하여 보안 헤더를 적용.모범 사례
denyAll)하고 필요한 요청만 명시적으로 허용하는 방식 권장.Spring Security에서 RequestMatcher는 요청이 특정 규칙과 일치하는지 판단하는 인터페이스입니다.
securityMatcher와 requestMatchers는 각각 다음을 결정하는 데 사용됩니다:
securityMatcher: requestMatchers: @Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.securityMatcher("/api/**") // /api/로 시작하는 URL에만 HttpSecurity 적용
.authorizeHttpRequests(authorize -> authorize
.requestMatchers("/user/**").hasRole("USER") // /user/로 시작하는 요청은 USER 역할 필요
.requestMatchers("/admin/**").hasRole("ADMIN") // /admin/로 시작하는 요청은 ADMIN 역할 필요
.anyRequest().authenticated() // 나머지 요청은 인증 필요
)
.formLogin(withDefaults()); // 기본 로그인 설정
return http.build();
}
}
`securityMatcher("/api/")`**:
/api/로 시작하는 URL에만 적용됩니다. /api/ 외의 요청은 이 설정에 영향을 받지 않습니다.`requestMatchers("/user/").hasRole("USER")`**:
/user/로 시작하는 요청은 USER 역할을 가진 사용자만 접근할 수 있습니다.`requestMatchers("/admin/").hasRole("ADMIN")`**:
/admin/로 시작하는 요청은 ADMIN 역할을 가진 사용자만 접근할 수 있습니다.anyRequest().authenticated():
Spring Security는 요청을 매칭하기 위해 다음 두 가지 기본 구현을 제공합니다:
AntPathRequestMatcher:
/user/**, /admin/*.RegexRequestMatcher:
/admin/.*.@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.securityMatcher(antMatcher("/api/**")) // AntPathRequestMatcher로 /api/** 매칭
.authorizeHttpRequests(authorize -> authorize
.requestMatchers(antMatcher("/user/**")).hasRole("USER") // /user/**는 USER 역할 필요
.requestMatchers(regexMatcher("/admin/.*")).hasRole("ADMIN") // 정규식으로 /admin/* 매칭
.requestMatchers(new MyCustomRequestMatcher()).hasRole("SUPERVISOR") // 사용자 정의 Matcher 적용
.anyRequest().authenticated() // 나머지는 인증 필요
)
.formLogin(withDefaults());
return http.build();
}
}
public class MyCustomRequestMatcher implements RequestMatcher {
@Override
public boolean matches(HttpServletRequest request) {
// 커스텀 매칭 로직 구현
String headerValue = request.getHeader("X-Custom-Header");
return "special-value".equals(headerValue);
}
}
AntPathRequestMatcher
/user/**와 /api/**처럼 와일드카드를 사용한 경로 패턴 매칭에 적합.RegexRequestMatcher
/admin/.*처럼 복잡한 정규식을 이용한 매칭이 필요한 경우 사용.MyCustomRequestMatcher
특정 RequestMatcher를 사용하고 싶다면, securityMatcher 또는 requestMatchers 메서드에 직접 구현체를 전달할 수 있습니다.
http
.securityMatcher(antMatcher("/api/**")) // AntPathRequestMatcher 사용
.authorizeHttpRequests((authorize) -> authorize
.requestMatchers(antMatcher("/user/**")).hasRole("USER") // /user/**는 USER 역할 필요
.requestMatchers(regexMatcher("/admin/.*")).hasRole("ADMIN") // /admin/*는 정규식 매칭
.requestMatchers(new MyCustomRequestMatcher()).hasRole("SUPERVISOR") // 사용자 정의 Matcher
);
securityMatcher:
requestMatchers:
권한 규칙 처리:
기본 동작:
anyRequest().authenticated()).최소 권한 부여:
기본 거부 정책:
정적 리소스 처리:
permitAll을 사용해 처리.RequestMatcher 활용:
securityMatcher와 requestMatchers:
securityMatcher: 특정 요청이 HttpSecurity 설정의 대상인지 결정. requestMatchers: 요청에 대한 권한 규칙을 정의.RequestMatcher 구현:
권한 규칙 적용 흐름:
securityMatcher로 설정 대상 요청 필터링 → requestMatchers로 권한 규칙 적용.모범 사례:
permitAll 적용. 이 방식은 애플리케이션의 요청 흐름에 대해 세밀하고 유연한 보안 제어를 가능하게 합니다.