메서드 보안은 엔드포인트 보안과는 다르게 메서드 실행 전, 후 AOP를 이용해 규칙을 검증하고 검증에 통과하지 못하면 블럭하는 방법이다.
엔드포인트를 통과했더라도 특정 서비스 호출에 인가과정이 필요하거나 추가적인 보안을 위해 메서드 보안을 사용한다.
메서드 보안을 사용하기 위해서 구성 클레스에서 다음과 같은 어노테이션을 추가한다
@Configuration
@EnableMethodSecurity
public class ProjectConfig {
유저 2명을 등록했다
@Bean
public UserDetailsService userDetailsService() {
UserDetails u1 = User.withUsername("name")
.password("1234")
.authorities("read")
.build();
UserDetails u2 = User.withUsername("name2")
.password("1234")
.authorities("write")
.build();
return new InMemoryUserDetailsManager(u1, u2);
}
@Bean
public PasswordEncoder passwordEncoder() {
return NoOpPasswordEncoder.getInstance();
}
사용자 정의 UserDetailsService를 등록하면 password해석을 위해 PasswordEncoder도 등록이 필요하다.
@Service
public class MainService {
@PreAuthorize("hasAuthority('write')")
public String hello() {
return "Hello World!";
}
}
PreAuthorize어노테이션은 이런식으로 메서드 위에 붙는데 springEL을 이용한다.
따로 SecurityFIlterChain설정을 안했기 떄문에 엔드포인트 인증으로 http basic이 사용되고 있는데
username과 password를 제대로 입력했더라도 컨트롤러에서 hello메서드를 호출할때
write Authority를 가진 name2만 접근이 가능하다.
알맞는 권한이 없다면 403 forbidden을 받게 된다.
인증된 사용자의 이름의 일치를 검증
컨트롤러에서 사용자의 비밀이름을 접근할 수 있게 엔드포인트를 설정한다.
@GetMapping("/secret/names/{name}")
public List<String> getSecretNames(@PathVariable String name) {
return mainService.getSecretNames(name);
}
MainService에서 getSecretNames메서드를 정의한다.
private Map<String, List<String>> secretNames = Map.of("name", List.of("sname1", "sname2"));
@PreAuthorize("#name == authentication.principal.username")
public List<String> getSecretNames(String name) {
return secretNames.get(name);
}
springEL을 통해 #name으로 파라미터의 name과 인증과정에서의 등록된 username의 일치를 확인한다.
일치한다면 아래의 메서드가 실행된다.
반대로 PostAuthorize는 메서드는 실행되지만 메서드의 리턴값이 Security Aspect에 의해 막히게 된다.
대신 Aspect는 예외를 던진다.
검증 로직이 복잡해진다면 Document 객체를 만들어 사용자 정의 메서드를 통해 검증 로직을 만드는 것이 좋다.
만일 메서드 실행을 아예 막는 것이 아니라 규칙에 따라 파라미터를 전달하고 싶다면 필터링을 이용한다.
필터링도 security aspect에 의해서 구현이 되고 마찬가지로 springEL을 이용한다.
필터링을 이용해 서비스 로직이 인증된 사용자명과 동일한 owner를 가지는 product만 매개변수로
받는 예제 구현
@Getter @Setter
@AllArgsConstructor
public class Product {
private String name;
private String owner;
}
@PreFilter("filterObject.owner == authentication.name")
public List<Product> sellProducts(List<Product> products) {
return products;
}
PreFilter는 메서드 실행전 필터링을 의미.
filterObject는 collection타입의 매개변수가 올때 단일 객체를 의미한다.
따라서 sellProducts는 Product객체의 owner와 인증된 사용자명이 같은 product만 매개변수로 받게 된다.

spring data jpa에서 securityContext에 대한 접근이 필요할때
db에서 데이터를 끌어와 was 메모리에 올린 뒤 PostFilter같은 어노테이션을 통해 리턴에 대한 필터링하게 되면 자원의 낭비가 매우 크다.
처음부터 알맞는 쿼리를 보내는게 좋다.
@Configuration
@EnableMethodSecurity
public class ProjectConfig {
@Bean
public SecurityEvaluationContextExtension
securityEvaluationContextExtension() {
return new SecurityEvaluationContextExtension();
}
}
스프링 시큐리티 자체에서는 @PreAuthorize, @PostAuthorize, @PreFilter, @PostFilter 등의 어노테이션에서 SpEL을 통한 시큐리티 컨텍스트 접근을 기본적으로 지원하지만 spring data jpa의 query같은 어노테이션에서는 지원을 하지 않기 때문에 추가해줘야한다.
public interface ProductRepository
extends JpaRepository<Product, Integer> {
Query("""SELECT p FROM Product p WHERE
p.name LIKE %:text% AND
p.owner=?#{authentication.name}
"""
List<Product> findProductByNameContains(String text);
}
위와 같이 Query에서 springEL을 통해 securityContext에 접근해 맞춤 쿼리를 사용할 수 잇다.