주의
이 책은 Spring Security 5 버전을 기준으로 작성되었으므로, Spring Boot 3.X 버전에서는 일부 클래스가 더 이상 사용되지(deprecated) 않을 수 있습니다.
서론
- 스프링 시큐리티는 앱이 HTTP 엔드포인트를 이용하지 않는 시나리오에도 문제없이 잘 쓸 수 있음
- 메서드 수준에서 권한 부여를 구성하는 방법을 `전역 메서드 보안`이라고 지칭
- 해당 방법으로 웹 애플리케이션과 웹이 아닌 애플리케이션의 권한 부여를 구성할 수 있음

- 웹이 아닌 애플리케이션에서 전역 메서드 보안은 엔드포인트 없이도 권한 부여 규칙을 구현할 수 있게 해 줌
- 웹 애플리케이션에서는 엔드포인트 수준만이 아니라 앱의 다양한 계층에 권한 부여 규칙을 적용하는 유연성을 제공
1. 전역 메서드 보안 활성화
- 전역 메서드 보안은 기본적으로 비활성화 상태이므로 해당 기능을 이용하려면 먼저 활성화해야 함
- 전역 메서드 보안은 또한 여러 방식으로 권한 부여를 적용할 수 있게 해 줌
- 전역 메서드 보안으로 할 수 있는 일은 크게 두 가지
- 호출 권한 부여: 정립된 여러 이용 권리 규칙에 따라 누군가가 메서드를 호출할 수 있는지 (사전 권한 부여) 또는 메서드가 실행된 후 메서드가 반환하는 것에 접근할 수 있는지 (사후 권한 부여) 결정
- 필터링: 메서드가 매개변수를 통해 받을 수 있는 것 (사전 필터링)과 메서드가 실행된 후 호출자가 메서드에서 다시 받을 수 있는 것 (사후 필터링)을 결정
1.1 호출 권한 부여의 이해
- 호출 권한 부여는 전역 메서드 보안과 함께 이용하는 권한 부여 규칙을 구성하기 위한 접근 방식 중 하나
- 호출 권한 부여 방식은 메서드를 호출할 수 있는지를 결정하거나 메서드를 호출하도록 허용한 후 호출자가 메서드에서 반환된 값에 접근할 수 있는지를 결정하는 권한 부여 규칙을 적용하는 것을 말함
- 애플리케이션에서 전역 메서드 보안을 활성화하면 스프링 Aspect 하나가 활성화됨
- 해당 Aspect는 우리가 권한 부여 규칙을 적용하는 메서드에 대한 호출을 가로채고 권한 부여 규칙을 바탕으로 가로챈 메서드로 호출을 전달할지 결정함

- 스프링 프레임워크의 구현은 상당수가 AOP에 의존하는데 전역 메서드 보안 또한 Aspect에 의존하는 스프링 애플리케이션의 많은 구성 요소 중 하나
- 호출 권한 부열을 다음과 같이 간단하게 분류할 수 있음
- 사전 권한 부여 (Preauthorization): 메서드 호출 전에 권한 부여 규칙을 검사하는 프레임워크
- 사후 권한 부여 (Postauthorization): 메서드 호출 후에 권한 부여 규칙을 검사하는 프레임워크
사전 권한 부여로 메서드에 대한 접근 보호
- 누군가가 특정 상황에서 메서드를 호출하는 것을 완전히 금지하는 권한 부여 규칙을 적용할 때 이를 사전 권한 부여라고 함
- 호출자가 우리가 정의한 권한 부여 규칙에 따른 사용 권한이 없으면 프레임워크는 메서드 호출하는 대신 오류를 내뱉음

사후 권한 부여로 메서드 호출 보호
- 메서드를 호출하도록 허용하지만 메서드가 반환한 결과를 얻기 위해 권한 부여가 필요한 방식을 사후 권한 부여라고 함
- 사후 권한 부여에서 스프링 시큐리티는 메서드 호출 후에 권한 부여 규칙을 검사함
- 이러한 유형의 권한 부여를 통해 특정 조건에서 메서드 반환에 대한 접근을 제한할 수 있음
- 사후 권한 부여는 메서드 실행 후에 수행되기 때문에 메서드에서 반환된 결과를 바탕으로 권한 부여 규칙을 적용할 수 있음

- 사후 권한 부여를 적용했을 때 다음과 같은 내용을 주의해야 함
- 메서드가 실행 중에 무엇인가를 변경하면 권한 부여의 성공 여부와 관계없이 이러한 변경 사항이 적용됨
1.2 프로젝트에서 전역 메서드 보안 활성화
- 스프링 시큐리티에서 전역 메서드 보안을 활성화하기 위해서는 구성 클래스에 @EnableGlobalMethodSecurity 어노테이션을 추가하면 됨
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
@Configuration | |
@EnableGlobalMethodSecurity(prePostEnabled = true) | |
public class ProjectConfig { | |
} |
- 전역 메서드 보안에 권한 부여 규칙을 정의하는 세 가지 접근 방식이 있음
- 사전/사후 권한 부여 어노테이션
- JSR 250 어노테이션 (@RolesAllowed)
- @Secured 어노테이션
- HTTP Basic 인증에서 OAuth 2에 이르기까지 모든 인증 방식에 전역 메서드 보안을 사용할 수 있음
2. 권한과 역할에 사전 권한 부여 적용
- 복습하자면 사전 권한 부여는 스프링 시큐리티가 특정 메서드를 호출하기 전에 적용하는 권한 부여 규칙을 정의하는 것
- 해당 규칙이 준수되지 않으면 프레임워크는 메서드를 호출하지 않음
- 이 절에서 구현하는 애플리케이션은 "Hello, "에 이름을 더한 문자열을 반환하는 /hello 엔드포인트를 노출
- 컨트롤러가 이름을 얻으려면 서비스 메서드를 호출해야 함
- 해당 메서드는 사용자에게 쓰기 권한이 있는지 확인하는 사전 권한 부여 규칙을 적용
- 사용자 엠마로 인증하면 정상적으로 응답을 반환하지만 사용자 나탈리로 인증하면 403 Forbidden 오류 발생
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
@Configuration | |
// 사전/사후 권한 부여를 위한 전역 메서드 보안 활성화 | |
@EnableGlobalMethodSecurity(prePostEnabled = true) | |
public class ProjectConfig { | |
@Bean | |
public UserDetailsService userDetailsService() { | |
var service = new InMemoryUserDetailsManager(); | |
var u1 = User.withUsername("natalie") | |
.password("12345") | |
.authorities("read") | |
.build(); | |
var u2 = User.withUsername("emma") | |
.password("12345") | |
.authorities("write") | |
.build(); | |
service.createUser(u1); | |
service.createUser(u2); | |
return service; | |
} | |
@Bean | |
public PasswordEncoder passwordEncoder() { | |
return NoOpPasswordEncoder.getInstance(); | |
} | |
} | |
@Service | |
public class NameService { | |
// 권한 부여 규칙 정의 | |
// 쓰기 권하이 있는 사용자만 메서드 호출 가능 | |
@PreAuthorize("hasAuthority('write')") | |
public String getName() { | |
return "Fantastico"; | |
} | |
} | |
@RestController | |
public class HelloController { | |
@Autowired | |
private NameService nameService; | |
@GetMapping("/hello") | |
public String hello() { | |
// 사전 권한 부여 규칙을 적용한 메서드 호출 | |
return "Hello, " + nameService.getName(); | |
} | |
} |

3. 사후 권한 부여 적용
- 메서드 호출 후에 권한 부여 규칙을 적용하고 싶다면 사후 권한 부여를 이용하면 됨
- 메서드가 수행하는 일은 명확하지만 메서드를 호출하는 상대에 관해서는 확신할 수 없을 때 메서드 실행은 허용하되 반환되는 내용을 검증하고 기준이 충족되지 않으면 호출자가 반환 값에 접근하지 못하게 할 수 있음
- 스프링 시큐리티로 사후 권한 부여를 적용하려면 @PostAuthorize 어노테이션을 이용함
- 해당 어노테이션은 @PreAuthorize와 비슷하며 권한 부여 규칙을 정의하는 SpEL을 값으로 받음

- 아래 예제 코드에서 Employee 객체가 정의되어 있고 Employee에는 이름, 책의 목록, 권한의 목록이 들어 있으면 각 애플리케이션 사용자에게 하나씩 Employee를 연결함
- 해당 예제에서 원하는 것은 메서드의 호출자가 읽기 권한이 있는 직원의 세부 정보만 얻을 수 있게 하는 것
- 레코드를 검색하기 전에는 직원 레코드와 연결된 권한을 알 수 없으므로 메서드 실행 후 권한 부여 규칙을 적용해야 하기 때문에 @PostAuthorize 어노테이션을 이용해야 함
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
@Configuration | |
@EnableGlobalMethodSecurity(prePostEnabled = true) | |
public class ProjectConfig { | |
@Bean | |
public UserDetailsService userDetailsService() { | |
var service = new InMemoryUserDetailsManager(); | |
var u1 = User.withUsername("natalie") | |
.password("12345") | |
.authorities("read") | |
.build(); | |
var u2 = User.withUsername("emma") | |
.password("12345") | |
.authorities("write") | |
.build(); | |
service.createUser(u1); | |
service.createUser(u2); | |
return service; | |
} | |
@Bean | |
public PasswordEncoder passwordEncoder() { | |
return NoOpPasswordEncoder.getInstance(); | |
} | |
} | |
public class Employee { | |
private String name; | |
private List<String> books; | |
private List<String> roles; | |
// 생성자, Getter, Setter | |
} | |
@Service | |
public class BookService { | |
private Map<String, Employee> records = | |
Map.of("emma", new Employee("Emma Thompson", | |
List.of("Karamazov Brothers"), | |
List.of("accountant", "reader")), | |
"natalie", new Employee("Natalie Parker", | |
List.of("Beautiful Paris"), | |
List.of("researcher")) | |
); | |
// 사후 권한 부여를 위한 식 정의 | |
@PostAuthorize("returnObject.roles.contains('reader')") | |
public Employee getBookDetails(String name) { | |
return records.get(name); | |
} | |
} | |
@RestController | |
public class BookController { | |
@Autowired | |
private BookService bookService; | |
@GetMapping("/book/details/{name}") | |
public Employee getDetails(@PathVariable String name) { | |
return bookService.getBookDetails(name); | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 엠마에 대해 반환된 역할 목록에는 "reader" 항목이 있으므로 모든 사용자가 엠마의 세부 정보에 접근할 수 있지만 | |
// 해당 문자열이 없는 나탈리의 세부 정보에는 아무도 접근할 수 없음 | |
// emma로 인증하고 emma 정보 request | |
curl -u emma:12345 http://localhost:8080/book/details/emma | |
// emma로 인증하고 response | |
{ | |
"name":"Emma Thompson", | |
"books":["Karamazov Brothers"], | |
"roles":["accountant","reader"] | |
} | |
// emma로 인증하고 natalie 정보 request | |
curl -u emma:12345 http://localhost:8080/book/details/natalie | |
// emma로 인증하고 natalie 정보 response | |
{ | |
"status":403, | |
"error":"Forbidden", | |
"message":"Forbidden", | |
"path":"/book/details/natalie" | |
} |
4. 메서드의 사용 권한 구현
- 복잡한 권한 부여 규칙을 구현해야 할 때는 긴 SpEL 식을 작성하지 말고 논리를 별도의 클래스로 만들어야 함
- 스프링 시큐리티의 사용 권한 개념으로 권한 부여 규칙을 별도의 클래스로 작성하면 애플리케이션을 읽고 이해하기 쉽게 만들 수 있음
- 이 절에서는 프로젝트에서 사용 권한으로 권한 부여 규칙을 적용함
- 이 예제는 문서를 관리하는 애플리케이션이며 모든 문서에는 해당 문서를 작성한 사용자인 소유자가 있음
- 기존 문서의 세부 정보를 얻으려면 사용자는 관리자이거나 해당 문서의 소유자여야 함
- 위 요구 사항을 해결하기 위해 사용 권한 평가기를 구현
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
public class Document { | |
private String owner; | |
// 생성자, Getter, Setter | |
} | |
@Repository | |
public class DocumentRepository { | |
// 고유 코드로 각 문서를 식별하고 소유자 이름을 지정 | |
private Map<String, Document> documents = | |
Map.of("abc123", new Document("natalie"), | |
"qwe123", new Document("natalie"), | |
"asd555", new Document("emma")); | |
public Document findDocument(String code) { | |
return documents.get(code); // 고유 식별 코드로 문서를 얻음 | |
} | |
} | |
@Service | |
public class DocumentService { | |
@Autowired | |
private DocumentRepository documentRepository; | |
// hasPermission() 식으로 권한 부여 식을 참조 | |
@PostAuthorize("hasPermission(returnObject, 'ROLE_admin')") | |
public Document getDocument(String code) { | |
return documentRepository.findDocument(code); | |
} | |
} |
- 사용 권한 논리를 구현할 책임은 개발자에게 있으며 이를 위해 PermissionEvaluator 계약을 구현하는 객체를 작성해야 함
- PermissionEvaluator 계약으로 사용 권한 논리를 구현하는 방법은 두 가지가 있음
- 객체 사용 권한: 사용 권한 평가기는 두 객체 (권한 부여 규칙의 주체가 되는 개체와 사용 권한 논리를 구현하기 위한 추가 세부 정보를 제공하는 객체)를 받음
- 객체 ID, 객체 형식, 사용 권한: 사용 권한 평가기는 필요한 객체를 얻는 데 이용할 수 있는 객체 ID를 받음, 또한 같은 권한 평가기가 여러 객체 형식에 적용될 때 이용할 수 있는 객체 형식을 받으며 사용 권한을 평가하기 위한 추가 세부 정보를 제공하는 객체가 필요함
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
public interface PermissionEvaluator { | |
boolean hasPermission( | |
Authentication a, | |
Object subject, | |
Object permission); | |
boolean hasPermission( | |
Authentication a, | |
Serializable id, | |
String type, | |
Object permission); | |
} |
- 예제에서는 첫 번째 메서드만 사용하면 됨
- 주체는 이미 있기 때문에 이 경우 메서드가 반환하는 값이 주체
- 또한 예제의 시나리오에 정의된 값이며 모든 문서에 접근할 수 있는 역할 이름은 'ROLE_admin'을 보냈음
- 물론 이 예제에서 사용 권한 평가기 클래스에 역할 이름을 직접 이용하면 hasPermission() 객체의 값으로 보낼 필요가 없었을 것
- 실제 시나리오는 여러 메서드가 있고 권한 부여 프로세스에 필요한 세부 정보도 각기 달라서 더 복잡할 수 있기 때문에 메서드 수준에서 권한 부여 논리에 이용하는 데 필요한 세부 정보를 보낼 수 있는 매개변수가 있음
- 스프링 시큐리티는 hasPermission() 메서드를 호출할 때 자동으로 Authentication 객체를 매개변수 값으로 제공하기 때문에 Authentication 객체를 직접 전달할 필요 없음
- 이미 SecurityContext에 있으므로 프레임워크는 인증 인스턴스의 값을 알 수 있음
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
@Component | |
public class DocumentsPermissionEvaluator implements PermissionEvaluator { | |
@Override | |
public boolean hasPermission( | |
Authentication authentication, | |
Object target, | |
Object permission) { | |
Document document = (Document) target; // target 객체를 Document 형식으로 변환 | |
String p = (String) permission; // 이 경우 permission 객체는 역할 이름이므로 String으로 형 변환 | |
// authentication 사용자에게 매개 변수로 받은 역할이 있는지 검사 | |
boolean admin = authentication.getAuthorities() | |
.stream() | |
.anyMatch(a -> a.getAuthority().equals(p)); | |
// 사용자가 운영자이거나 문서의 소유자면 사용 권한 부여 | |
return admin || | |
document.getOwner() | |
.equals(authentication.getName()); | |
} | |
@Override | |
public boolean hasPermission(Authentication authentication, | |
Serializable targetId, | |
String targetType, | |
Object permission) { | |
return false; // 두 번째 메서드는 쓰지 않으므로 구현할 필요 없음 | |
} | |
} |
- 스프링 시큐리티가 새 PermissionEvaluator 구현을 인식할 수 있도록 구성 클래스에 MethodSecurityExpressionHandler를 정의해야 함
- 아래 에제 코드에는 맞춤형 PermissionEvaluator를 알리기 위해 MethodSecurityExpressionHandler를 정의하는 방법이 나옴
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
@Configuration | |
@EnableGlobalMethodSecurity(prePostEnabled = true) | |
public class ProjectConfig extends GlobalMethodSecurityConfiguration { | |
@Autowired | |
private DocumentsPermissionEvaluator evaluator; | |
@Override | |
protected MethodSecurityExpressionHandler createExpressionHandler() { | |
// 맞춤형 사용 권한 평가기를 설정하기 위해 기본 보안 식 처리기 정의 | |
var expressionHandler = new DefaultMethodSecurityExpressionHandler(); | |
expressionHandler.setPermissionEvaluator(evaluator); // 맞춤형 사용 권한 평가기 설정 | |
return expressionHandler; | |
} | |
@Bean | |
public UserDetailsService userDetailsService() { | |
var service = new InMemoryUserDetailsManager(); | |
var u1 = User.withUsername("natalie") | |
.password("12345") | |
.roles("admin") | |
.build(); | |
var u2 = User.withUsername("emma") | |
.password("12345") | |
.roles("manager") | |
.build(); | |
service.createUser(u1); | |
service.createUser(u2); | |
return service; | |
} | |
@Bean | |
public PasswordEncoder passwordEncoder() { | |
return NoOpPasswordEncoder.getInstance(); | |
} | |
} | |
// 애플리케이션을 테스트하기 위한 엔드포인트 정의 | |
@RestController | |
public class DocumentController { | |
@Autowired | |
private DocumentService documentService; | |
@GetMapping("/documents/{code}") | |
public Document getDetails(@PathVariable String code) { | |
return documentService.getDocument(code); | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 사용자 나탈리는 문서의 소유자와 관계없이 모든 문서에 접근할 수 있지만 | |
// 사용자 엠마는 자기 문서에만 접근 가능 | |
// natalie로 인증하고 나탈리가 소유한 문서에 접근하는 request | |
curl -u natalie:12345 http://localhost:8080/documents/abc123 | |
// natalie로 인증하고 나탈리가 소유한 문서에 접근하는 response | |
{ | |
"owner":"natalie" | |
} | |
// emma로 인증하고 나탈리가 소유한 문서에 접근하는 request | |
curl -u emma:12345 http://localhost:8080/documents/abc123 | |
// emma로 인증하고 나탈리가 소유한 문서에 접근하는 response | |
{ | |
"status":403, | |
"error":"Forbidden", | |
"message":"Forbidden", | |
"path":"/documents/abc123" | |
} |
- 실행되기 전에 권한 부여 규칙을 적용하도록 현재 예제를 변경한다면 첫 번째 메서드 대신 두 번째 메서드를 사용해야 함
- 이때는 아직 반환된 객체는 없지만 객체 자체 대신 고유 식별자인 문서의 코드가 있음
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
@Component | |
public class DocumentsPermissionEvaluator implements PermissionEvaluator { | |
@Autowired | |
private DocumentRepository documentRepository; | |
@Override | |
public boolean hasPermission(Authentication authentication, | |
Object target, | |
Object permission) { | |
return false; // 이번에는 첫 번째 메서드로 권한 부여 규칙을 정의하지 않음 | |
} | |
@Override | |
public boolean hasPermission(Authentication authentication, | |
Serializable targetId, | |
String targetType, | |
Object permission) { | |
String code = targetId.toString(); // 객체는 없지만 객체 ID가 있으므로 ID로 객체를 얻음 | |
Document document = documentRepository.findDocument(code); | |
String p = (String) permission; | |
// 사용자가 운영자인지 확인 | |
boolean admin = | |
authentication.getAuthorities() | |
.stream() | |
.anyMatch(a -> a.getAuthority().equals(p)); | |
// 사용자가 운영자이거나 문서의 소유자면 문서에 접근할 수 있음 | |
return admin || | |
document.getOwner().equals( | |
authentication.getName()); | |
} | |
} | |
@Service | |
public class DocumentService { | |
@Autowired | |
private DocumentRepository documentRepository; | |
// 사용 권한 평가기의 두 번째 메서드로 사전 권한 부여 적용 | |
@PreAuthorize("hasPermission(#code, 'document', 'ROLE_admin')") | |
public Document getDocument(String code) { | |
return documentRepository.findDocument(code); | |
} | |
} |
참고
스프링 시큐리티 인 액션
반응형
'Spring > 스프링 시큐리티 인 액션' 카테고리의 다른 글
[17장] 전역 메서드 보안: 사전 및 사후 필터링 (0) | 2025.06.05 |
---|---|
[15장] OAuth 2: JWT와 암호화 서명 사용 (0) | 2025.06.01 |
[14장] OAuth 2: 리소스 서버 구현 (0) | 2025.06.01 |
[13장] OAuth 2: 권한 부여 서버 구현 (0) | 2025.06.01 |
[12장] OAuth 2가 작동하는 방법 (0) | 2025.05.31 |