Expression
기반의 권한 체크(@PrePost, @PostFilter ... )가 편리하고 권장되기는 하지만, 기존에 구축된 Security 들 중에 일부는@Secured
기반으로 구축이 되어 있을 수 있고, 그 기반에서 소스를 유지보수 혹은 확장해 나가야 할 수도 있음
@Secured 기반의 권한 체크는 메소드의 사전에만 체크를 하기 때문에
사후 체크를 하려면 @PostAuthorize(혹은 @PostFilter) 를 사용하거나 별도의 AOP 를 설계 해야 함
@Secured 는 또한 Voter 를 추가할 수 있도록 설계되어 Voter 기반의 AccessDecisionManager와 어울림
SecurityMetadataSource는 각 인터셉터에서 관리
Method 시큐리티 메타 데이터 소스는 globalMethodSecurityConfiguration
에서 관리
Metadatasource안에서 어노테이션으로 마킹해놓은 스트링(ConfigAttribute)를 주면은 이를 기반으로 권한 체크
-> Ex)PreAuthorize(“hasRole(‘ADMIN’)”)
AbstractFallback, PrePost, Delegating MethodSecurityMetadataSoruce는 Abstract MethodSecurityMetadataSource
를 상속해서 구현
mapBased
는 xml 기반
Delegating(중재) Metadatasoruce
메타 데이터 소스를 리스트로 가지고 있다가 앞에서 부터 차례대로 적절한 메타 데이터 소스를 골라냄
-> 만약 이를 처리할 수 있다면 만약 이를 처리할수 있다면 그 MetaDataSoruce에서 ConfigAttribute 리스트
를 뽑아주는 역할
특별히 권한과 관련된 새로운 어노테이션을 추가하고 싶다면 PrePost나 Secured 처럼 MetaDataSource
를 만들어서 추가를 해주어야 함
메소드 권한을 설정하는 GlobalMethodSecurityConfiguration 에서 methodSecurityMetadataSource() 를 만들어 내는 곳에서 어노테이션을 파싱해 MethodSecurityMetadataSource 를 만들어 냄
protected MethodSecurityMetadataSource customMethodSecurityMetadataSource() {
return null;}
...
public MethodSecurityMetadataSource methodSecurityMetadataSource() {
List<MethodSecurityMetadataSource> sources = new ArrayList<>();
ExpressionBasedAnnotationAttributeFactory attributeFactory = new ExpressionBasedAnnotationAttributeFactory(
getExpressionHandler());
MethodSecurityMetadataSource customMethodSecurityMetadataSource = customMethodSecurityMetadataSource();
// 사용자가 구현한 customMethodSecurityMetadataSoruce가 있다면
// 이를 Delegating metadatasource에 가장 먼저 넣어 줌
if (customMethodSecurityMetadataSource != null) {
sources.add(customMethodSecurityMetadataSource);
}
boolean hasCustom = customMethodSecurityMetadataSource != null;
boolean isPrePostEnabled = prePostEnabled();
boolean isSecuredEnabled = securedEnabled();
boolean isJsr250Enabled = jsr250Enabled();
// PrePost 에노테이션이 있다면 이를 메타데이터 소스에 등록
// @EnableGlobalMethodSecurity(prePostEnabled = true)
if (isPrePostEnabled) {
sources.add(new PrePostAnnotationSecurityMetadataSource(attributeFactory));
}
if (isSecuredEnabled) {
sources.add(new SecuredAnnotationSecurityMetadataSource());
}
if (isJsr250Enabled) {
...
}
return new DelegatingMethodSecurityMetadataSource(sources);
}
실무에선 Pre,Post 기반 검증을 자주 사용 하지만 @Secured 동작 방식을 알고 커스터마이징도 해보자!
CustomSecurity.java
@Target({ ElementType.METHOD, ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface CustomSecurityTag {
String value();
}
CustomSecurityMetadataSource.java
public class CustomSecurityMetadataSource implements MethodSecurityMetadataSource {
PrePostAnnotationSecurityMetadataSource prePostAnnotationSecurityMetadataSource;
SecuredAnnotationSecurityMetadataSource securedAnnotationSecurityMetadataSource;
@Override
public Collection<ConfigAttribute> getAttributes(Method method, Class<?> targetClass) {
CustomSecurityTag annotation = findAnnotation(method, targetClass, CustomSecurityTag.class);
if(annotation != null){
return List.of(new SecurityConfig(annotation.value()));
}
return null;
// 밑의 경우는 @Secured 사용한 경우
// if(method.getName().equals("getPapersByPrimary") && targetClass == PaperController.class){
// return List.of(new SecurityConfig("SCHOOL_PRIMARY")); // 기존에 만든 클래스 아님
// }
// return null;
}
@Override
public Collection<ConfigAttribute> getAttributes(Object object) throws IllegalArgumentException {
return null;
}
@Override
public Collection<ConfigAttribute> getAllConfigAttributes() {
return null;
}
@Override
public boolean supports(Class<?> clazz) {
return MethodInvocation.class.isAssignableFrom(clazz);
}
// PrePost 메타데이터 소스 참고
private <A extends Annotation> A findAnnotation(Method method, Class<?> targetClass, Class<A> annotationClass) {
Method specificMethod = ClassUtils.getMostSpecificMethod(method, targetClass);
A annotation = AnnotationUtils.findAnnotation(specificMethod, annotationClass);
if (annotation != null) {
return annotation;
}
return annotation;
}
}
Custom Voter
-> 구현이후 MethodSecurityConfiguration에 추가
public class CustomVoter implements AccessDecisionVoter<MethodInvocation> {
private final String PREFIX = "SCHOOL_";
@Override
public boolean supports(ConfigAttribute attribute) {
// ex) Attribute에 SCHOOL_PRIMARY가 담겨 오는 경우
return attribute.getAttribute().startsWith(PREFIX);
}
@Override
public boolean supports(Class<?> clazz) {
return MethodInvocation.class.isAssignableFrom(clazz);
}
@Override
public int vote(Authentication authentication, MethodInvocation object, Collection<ConfigAttribute> attributes) {
String role = attributes.stream().filter(attr -> attr.getAttribute().startsWith(PREFIX))
.map(attr -> attr.getAttribute().substring(PREFIX.length()))
.findFirst().get(); // ROLE을 가져옴
if(authentication.getAuthorities().stream().filter(auth -> auth.getAuthority().equals("ROLE_" + role.toUpperCase()))
.findAny().isPresent()){
// SCHOOL_를 ROLE_로 치환해서 그 Authority가 있으면 접근을 허용해주겠다
return ACCESS_GRANTED;
}
return ACCESS_DENIED;
}
}
controller
...
// @Secured({"SCHOOL_PRIMARY"}) // 이를 처리해줄 Voter가 필요 (Custom Voter)
@CustomSecurityTag("SCHOOL_PRIMARY")
@GetMapping("/getPapersByPrimary")
public List<Paper> getPapersByPrimary(@AuthenticationPrincipal User user){
return paperService.getAllPapers();
}
테스트코드
@DisplayName("5. 교장 선생님은 모든 시험지를 볼 수 있다")
@Test
void test_5() {
paperService.setPaper(paper1);
paperService.setPaper(paper2);
paperService.setPaper(paper3);
client = new TestRestTemplate("primary", "1111");
ResponseEntity<List<Paper>> response = client.exchange(uri("/paper/getPapersByPrimary"),
HttpMethod.GET, null, new ParameterizedTypeReference<List<Paper>>() {
});
assertEquals(200, response.getStatusCodeValue());
assertEquals(3, response.getBody().size());
System.out.println(response.getBody());
}