[Spring Security] Method 후처리(After Invocation)

WOOK JONG KIM·2022년 12월 5일
0

패캠_java&Spring

목록 보기
86/103
post-thumbnail

후처리란 voter 기반 체크 이후 메서드가 결과를 리턴한 다음, 리턴한 객체를 사용자가 접근할 수 있는지 판단하는 것
-> Access Decision Manager는 주로 사전 체크를 담당함
-> 리턴된 객체에 대해서 검사하는 매니저는 AfterInvocation Manager

MethodSecurityInterceptor

필터를 통과한 request는 보안이 마킹된 메소드를 실행할 때마다 MethodSecurityInterceptor 의 검사를 받음
-> 이를 설정하는 것이 GlobalMethodSecurityConfiguration
-> 여기에 securedEnabled=true 를 설정하면 @Secured 로 설정된 권한을 체크하고. prePostEnabled=true 로 설정하면 @PreAuthorize, @PreFilter, @PostAuthorize, @PostFilter 로 설정된 권한을 체크

MethodSecurityInterceptor 에서 중요한 멤버

  • AccessDecisionManager : @Secured 나 @PreAuthorize, @PreFilter 를 처리합니다.
  • AfterInvocationManager : @PostAuthorize, @PostFilter 를 처리 (return된 Object 활용 해서)
  • RunAsManager : 임시권한 부여

AfterInvocation은 Voter 기반이 아님

AfterInvocationProviderManager는 기본적으로 list 형태로 AfterInvocationprovider를 가지지만 보통은 1개만 가짐

개발자가 직접 구현할 수도 있지만 보통은 PostInvocationAdviceProvider가 동작함

Decision Voter에서는 여러 정책이 있었다
(다수결로 할지, 한명이라도 통과를 해주면 Granted할지) -> 정책에 따라 움직임

반면 AfterInvocationProviderManager는 정책이 딴 한개
-> AfterInvocationProvider를 차례로 다 거치고 나서 결과를 리턴해주는 방법
-> @PostFilter @PostAuthorize와 같은 애노테이션들은 PostInvocationAtrribute라는 configAttribute로 메타데이터 소스에 등록이 됨

Post,WebExpression,PreInvocationAttribute들은 Expression 기반의 ConfigAttribute

반면 SecurityConfig는 @Secured에 들어가는 String 값을 가지고 있다
-> RoleVoter나 AuthenticatedVoter들이 처리

AfterInvocationManager

Authentication 통행증만 가지고는 권한 체크가 충분하지 않음

Method를 거쳐 Return된 객체를 사용자가 접근할 수 있는지 없는지 체크할려면
-> 리턴되어 온 객체를 한번 체크를 해봐야 하고 이때 사용되는 애노테이션이 @PostAuthorized or @PostFilter

예를 들어 어떤 객체의 값을 변경해야 하는 경우에는 메소드에 들어오기 전에, 값을 조회하려고 하는 경우에는 값을 가져온 이후에 각각 접근 권한을 체크

체크해야 할 대상이 한개라면 Pre/PostAuthorized 로 체크를 하면 되지만, 대상이 복수개라면 보통은 리스트로 묶이기 때문에 대상을 filtering을 해서 들어가거나 넘겨야 함

AfterInvocationProviderManager

public Object decide(
  Authentication authentication,
  Object object,
  Collection<ConfigAttribute> config,  // config Collections
  Object returnedObject // 반환되는 객체
) throws AccessDeniedException {

	Object result = returnedObject;
    // Provider들에게 Decide를 맏김
	for (AfterInvocationProvider provider : this.providers) {
		result = provider.decide(authentication, object, config, result);
	}
	return result;
}

PostInvocationAdviceProvider

대표적인 Provider

public Object decide(
  Authentication authentication,
  Object object,
  Collection<ConfigAttribute> config,
  Object returnedObject
) throws AccessDeniedException {
	// Filter와 Authorize 애너테이션으로 부터 파싱한 값이 PostInvocationAttribute
	PostInvocationAttribute postInvocationAttribute = findPostInvocationAttribute(config);
	if (postInvocationAttribute == null) {
		return returnedObject;
	}
	return this.postAdvice.after(authentication, (MethodInvocation) object, postInvocationAttribute,
				returnedObject);
}

ExpressionBasedPostInvocationAdvice

(PostAdvice)

public Object after(
  Authentication authentication,
  MethodInvocation mi,
  PostInvocationAttribute postAttr,
  Object returnedObject
) throws AccessDeniedException {

	PostInvocationExpressionAttribute pia = (PostInvocationExpressionAttribute) postAttr;
	EvaluationContext ctx = this.expressionHandler.createEvaluationContext(authentication, mi);
	Expression postFilter = pia.getFilterExpression();
	Expression postAuthorize = pia.getAuthorizeExpression();
	if (postFilter != null) {
		if (returnedObject != null) {
			returnedObject = this.expressionHandler.filter(returnedObject, postFilter, ctx);
		}
	}
	this.expressionHandler.setReturnObject(returnedObject, ctx);
	if (postAuthorize != null && !ExpressionUtils.evaluateAsBoolean(postAuthorize, ctx)) {
		throw new AccessDeniedException("Access is denied");
	}
	return returnedObject;
}
Expression postFilter = pia.getFilterExpression();
Expression postAuthorize = pia.getAuthorizeExpression()

애너테이션으로 부터 파싱한 값에서 Filter 표현식과 Authorize 표현식을 각각 가져옴 -> Filter를 먼저 적용

여기서 컨텍스트(ctx) SPEL을 적용한 루트 컨텍스트
-> 이 컨텍스트 객체는 Pre Expression이나 Post Expression에서 같이 사용

returnedObject = this.expressionHandler.filter(returnedObject, postFilter, ctx);

expressionHandler가 ctx를 가지고 returned Object을 주면
이 객체에 대해 postFilter(String 표현, Expression 이겠지?)를 적용해서 리턴된 결과를 returned Object에 쌓음

this.expressionHandler.setReturnObject(returnedObject, ctx);
	if (postAuthorize != null && !ExpressionUtils.evaluateAsBoolean(postAuthorize, ctx)) {
		throw new AccessDeniedException("Access is denied");
	}

이렇게 필터링된 Returned Object 객체에 postAuthorize로 표현된 SpEL으로 Evaluate 해서 결과를 Return

PreInvocationVoterPostInvocation는 MethodSecurityExpressionRoot를 같이 사용
-> 이를 위해 returnObject이 들어가 있었다


코드 예시

CustomMethodSecurityExpressionRoot

...
public boolean noPrepareSet(Paper paper){
        return !paper.getState().equals(Paper.State.PREPARE);
    }
...

CustomPermissionEvaluator

@Component
public class CustomPermissionEvaluator implements PermissionEvaluator {

    // @Autowired
    @Lazy // 이 경우 생성시점을 뒤로 늦추기 위해 @LAZY 사용
    private PaperService paperService;

    @Override
    public boolean hasPermission(Authentication authentication, Object targetDomainObject, Object permission) {
        return false;
    }

    @Override
    public boolean hasPermission(Authentication authentication,
                                 Serializable targetId,
                                 String targetType,
                                 Object permission) {
        Paper paper = paperService.getPaper((long)targetId);
        if(paper == null) throw new AccessDeniedException("시험지가 존재하지 않음.");


        if(paper.getState() == Paper.State.PREPARE) return false;

        boolean canUse = paper.getStudentIds().stream().filter(userId -> userId.equals(authentication.getName()))
                .findAny().isPresent();

        return canUse;
    }
}

PaperController

@RequestMapping("/paper")
@RestController
public class PaperController {

    @Autowired
    private PaperService paperService;

//    @PreAuthorize("isStudent()")
//    @PostFilter("filterObject.state != T(com.sp.fc.web.service.Paper.State).PREPARE") // List<Paper>안에있는 Paper 한개 = filterObject
    // 위 코드의 경우 PREPARE가 아닌 것만 내려감
    // 위와 같이 옵션 값을 자주 사용할 경우 CustomMethodSecurityExpressionRoot에 등록해도 됨

//    @PostFilter("noPrepareSet(filterObject) && filterObject.studentIds.contains(#user.username)") // 모든 페이지 내려준다 가정시

//    @PostFilter("noPrepareSet(filterObject)") -> 여기서 어노테이션을 삽입하지않고 Service 단에서 넣으면
    // 동작하지 않음(빈의 라이프타임과 관련) -> service에 이유 설명
    @GetMapping("/mypapers")
    public List<Paper> myPapers(@AuthenticationPrincipal User user){
        return paperService.getMyPapers(user.getUsername());
    }

//    @PreAuthorize("hasPermission(#paperId, 'paper', 'read')")
    @PostAuthorize("returnObject.studentIds.contains(#user.username)") // 위와 로직 같음
    // 이는 서비스에 가져가도 마찬가지로 동작
    // 서비스 단에서는 ("returnObject.studentIds.contains(principal.username)")
    @GetMapping("/get/{paperId}")
    public Paper getPaper(@AuthenticationPrincipal User user, @PathVariable Long paperId){
        return paperService.getPaper(paperId);
    }

}

PageService

@Service
public class PaperService implements InitializingBean {

    private HashMap<Long, Paper> paperDB = new HashMap<>();

    @Override
    public void afterPropertiesSet() throws Exception {

    }

    public void setPaper(Paper paper){
        paperDB.put(paper.getPaperId(), paper);
    }


//    @PostFilter("noPrepareSet(filterObject)")
    // 개발자가 PaperService를 만들고 CustomPermissionEvaluator에 Autowired 해주었는데
    // 이를 autowired하는 시점이 MethodSecurityConfiguration(@Autowired CustomPermissionEvaluator)
    // 에서 CustomPermissionEvaluator 빈이 만들어 지는데 이때 PaperService도 동시에 생성에 들어감(생성 시점이 너무 이름)
    // 이후 Service를 감싸는 Proxy가 생성되지 않는다는 이슈가 있음
    // 이 경우 해결법 -> PermissionEvaluator 코드 참고
    
    
    // PostAuthroize는 리스트가 아닐 때 주로 사용

    @PostFilter("noPrepareSet(filterObject)")
    public List<Paper> getMyPapers(String username) {

        return paperDB.values().stream().filter(
                paper -> paper.getStudentIds().contains(username)
        ).collect(Collectors.toList());
    }

    public Paper getPaper(Long paperId) {
        return paperDB.get(paperId);
    }
}
profile
Journey for Backend Developer

0개의 댓글