전역 메서드 보안이라고 책에 보면 나오는데
지금은 메소드 보안이라고 쓰면 된다.
이유는 원래는 글로벌메서드시큐리티 어노테이션을 썼으나
바뀌었음.
일단 첫번재로 이해가 되지 않았던 부분은 보안처리를 웹어플리케이션 레벨이 아닌 메소드 레벨로 하는 이유이다.
큰 아키텍처 레벨이 아닌 메소드 레벨로 축소하면 그만큼 마이크로레벨 단위로 보안설정을 할 수 있는 장점이 있다.
이부분은 크게 이해하기 어렵지 않았음.
여기서 핵심은 유연성이 커진다는 측면.
근데 메소드레벨에서 사전과 사후를 나누는 이유를 알 수가 없었다.
그냥 보안처리를 하면 했지 왜 사전과 사후를 나누었을까?
우리가 일반적으로 생각하는 보안은 실행되기 전 적절한 권한이 있는지를 체크만하는 것을 생각하는데
여기서 나오는 사후권한 체크는 실행되고 나서 어떤 데이터가 나오느냐에 따라서 사용자가 접근할 수 있는지를 제어해준다.
그래서 데이터베이스에서 어떤 데이터를 액세스 한다고 했을 때, 어떤 데이터가 나오는지 모르기 때문에
실행되기전 레벨이 아닌 실행되고 나서의 레벨에서 보안을 체크하는 것이다.
다시 정리하자면
전역메서드 보안 - 기본적으로 비활성화
할 수 있는 것은 2가지
1.호출 권한 부여 - 사전 권한 부여와 사후 권한부여 / 사전 권한부여는 누가 메소드를 호출할 수 있는지와 사후 권한부여는 메서드가 반환하는것에 대한 액세스 여부
2.필터링 - 메서드가 실행되기전 매개변수를 통해서 받을 수 있는 것(사전필터링)과 메서드가 실행된 후 호출자가 메서드에서 다시 받을 수 있는 것(사후 필터링)을 결정함.
기존에는 엔드포인트 레벨에서 보안을 설정했으나 메소드 레벨에서도 설정가능하다.
메소드보안은 아래에 자세히 나와있음.
https://docs.spring.io/spring-security/reference/servlet/authorization/method-security.html
Method Security :: Spring Security
As already noted, there is a Spring AOP method interceptor for each annotation, and each of these has a location in the Spring AOP advisor chain. Namely, the @PreFilter method interceptor’s order is 100, @PreAuthorize's is 200, and so on. The reason this
docs.spring.io
How Method Security Works
Spring Security’s method authorization support is handy for:
- Extracting fine-grained authorization logic; for example, when the method parameters and return values contribute to the authorization decision.
- Enforcing security at the service layer
- Stylistically favoring annotation-based over HttpSecurity-based configuration
And since Method Security is built using Spring AOP, you have access to all its expressive power to override Spring Security’s defaults as needed.
As already mentioned, you begin by adding @EnableMethodSecurity to a @Configuration class or <sec:method-security/> in a Spring XML configuration file.
This annotation and XML element supercede @EnableGlobalMethodSecurity and <sec:global-method-security/>, respectively. They offer the following improvements:
If you are using @EnableGlobalMethodSecurity or <global-method-security/>, these are now deprecated, and you are encouraged to migrate.
|
Method authorization is a combination of before- and after-method authorization. Consider a service bean that is annotated in the following way:
- Java
- Kotlin
@Service
public class MyCustomerService {
@PreAuthorize("hasAuthority('permission:read')")
@PostAuthorize("returnObject.owner == authentication.name")
public Customer readCustomer(String id) { ... }
}
A given invocation to MyCustomerService#readCustomer may look something like this when Method Security is activated:

- Spring AOP invokes its proxy method for readCustomer. Among the proxy’s other advisors, it invokes an AuthorizationManagerBeforeMethodInterceptor that matches the @PreAuthorize pointcut
- The interceptor invokes PreAuthorizeAuthorizationManager#check
- The authorization manager uses a MethodSecurityExpressionHandler to parse the annotation’s SpEL expression and constructs a corresponding EvaluationContext from a MethodSecurityExpressionRoot containing a Supplier<Authentication> and MethodInvocation.
- The interceptor uses this context to evaluate the expression; specifically, it reads the Authentication from the Supplier and checks whether it has permission:read in its collection of authorities
- If the evaluation passes, then Spring AOP proceeds to invoke the method.
- If not, the interceptor publishes an AuthorizationDeniedEvent and throws an AccessDeniedException which the ExceptionTranslationFilter catches and returns a 403 status code to the response
- After the method returns, Spring AOP invokes an AuthorizationManagerAfterMethodInterceptor that matches the @PostAuthorize pointcut, operating the same as above, but with PostAuthorizeAuthorizationManager
- If the evaluation passes (in this case, the return value belongs to the logged-in user), processing continues normally
- If not, the interceptor publishes an AuthorizationDeniedEvent and throws an AccessDeniedException, which the ExceptionTranslationFilter catches and returns a 403 status code to the response
크게 사전 권한부여와 사후 권한부여 2가지가 있다.
사전권한부여 테스트를 위해서
아래와 같은 어노테이션을 선언해준다.
package com.laurentiuspilca.ssia.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.password.NoOpPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class ProjectConfig {
config파일에 테스트 유저 2명을 등록하는 부분
package com.laurentiuspilca.ssia.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.password.NoOpPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
@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();
}
}
그리고 실제로 컨트롤러를 타기전에
preauthorize어노테이션을 통해서 권한체크하는 부분
package com.laurentiuspilca.ssia.services;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.stereotype.Service;
@Service
public class NameService {
@PreAuthorize("hasAuthority('write')")
public String getName() {
return "Fantastico";
}
}
실제 컨트롤러 부분.
import com.laurentiuspilca.ssia.services.NameService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class HelloController {
@Autowired
private NameService nameService;
@GetMapping("/hello")
public String hello() {
return "Hello, " + nameService.getName();
}
}
사전 및 사후 권한부여 방식 차이
- 메서드 보안 표현식
- 표현식을 종합적으로 지원하기 위한 새로운 애너테이션 도입
- 사전/사후 권한 체크를 지원하고 제출한 컬렉션 인자나 리턴한 값을 필터링 할 수 있음2). @Post
- 메소드가 실행되기 전에 검사
- 실제로 해당 메서드를 호출할 권한이 있는지를 확인한다.
- 3). @PreAuthorize
- 1). @Pre
[예제Code]
@PreAuthorize("#contact.name == authentication.name") public void doSomething(Contact contact);
4). @PostAuthorize
- 메소드가 실행 된 이후에 실행됨
- 메소드가 실행 된 이후의 return 값을 활용할 수 있음
- returnObject 예약어로 리턴 객체에 접근할 수 있음
[예제Code]
@PostAuthorize("isAuthenticated() and (( returnObject.name == principal.name ) or hasRole('ROLE_ADMIN'))") @RequestMapping( value = "/{seq}", method = RequestMethod.GET ) public User getuser( @PathVariable("seq") long seq ){ return userService.findOne(seq); }
사후 권한은 조금 특이한데
config부분은 동일하다.
package com.laurentiuspilca.ssia.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.password.NoOpPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
@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();
}
}
컨트롤러 부분
package com.laurentiuspilca.ssia.controllers;
import com.laurentiuspilca.ssia.model.Employee;
import com.laurentiuspilca.ssia.services.BookService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class BookController {
@Autowired
private BookService bookService;
@GetMapping("/book/details/{name}")
public Employee getDetails(@PathVariable String name) {
return bookService.getBookDetails(name);
}
}
모델부분
package com.laurentiuspilca.ssia.model;
import java.util.List;
import java.util.Objects;
public class Employee {
private String name;
private List<String> books;
private List<String> roles;
public Employee(String name, List<String> books, List<String> roles) {
this.name = name;
this.books = books;
this.roles = roles;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public List<String> getBooks() {
return books;
}
public void setBooks(List<String> books) {
this.books = books;
}
public List<String> getRoles() {
return roles;
}
public void setRoles(List<String> roles) {
this.roles = roles;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Employee employee = (Employee) o;
return Objects.equals(name, employee.name) &&
Objects.equals(books, employee.books) &&
Objects.equals(roles, employee.roles);
}
@Override
public int hashCode() {
return Objects.hash(name, books, roles);
}
}
마지막으로 가장 중요한 서비스 부분
package com.laurentiuspilca.ssia.services;
import com.laurentiuspilca.ssia.model.Employee;
import org.springframework.security.access.prepost.PostAuthorize;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.Map;
@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);
}
}
C:\Users\user>curl -u emma:12345 http://localhost:8080/book/details/emma
{"name":"Emma Thompson","books":["Karamazov Brothers"],"roles":["accountant","reader"]}
C:\Users\user>curl -u natalie:12345 http://localhost:8080/book/details/emma
{"name":"Emma Thompson","books":["Karamazov Brothers"],"roles":["accountant","reader"]}
C:\Users\user>curl -u natalie:12345 http://localhost:8080/book/details/natalie
{"timestamp":"2023-10-04T07:04:56.628+00:00","status":403,"error":"Forbidden","message":"","path":"/book/details/natalie"}
중요한 건 사후 권한체크는 records부분을 모두 호출한다는 것이다.
호출하고 나오는 정보중에 reader가 있다면 데이터를 가져오고 없다면 forbidden이 떨어지는 것을 확인할 수 있다.
'IT' 카테고리의 다른 글
spring boot custom login failed 핸들러+thymeleaf 에러메시지 처리방법 - 세션에 에러메시지 담으면 안되는 이유 (0) | 2023.10.30 |
---|---|
스프링시큐리티 전역메서드 보안 17장 / 사전필터링과 사후필터링 / 스프링시큐리티 oauth2 어플리케이션 18장 (0) | 2023.10.16 |
파이썬 series,dataframe (0) | 2023.09.20 |
spring security chap.15 oauth2 jwt와 암호화 서명 사용 (0) | 2023.09.19 |
파이썬 1158 (0) | 2023.09.13 |