본문 바로가기
Spring/Spring Security

메서드 후처리

코동이 2021. 10. 19.

메서드 후처리는 메서드가 결과를 리턴한 다음에 리턴된 객체를 사용자가 접근할 수 있는지 검사한다. 권한은 메서드쪽에서 MethodSecurityInterceptor를 통해서 검사한다.

 

MethodSecurityInterceptor 에서 중요한 멤버는 아래 세가지다.

 

AccessDecisionManager : @Secured 나 @PreAuthorize, @PreFilter 를 처리한다. (사전체크)

AfterInvocationManager : @PostAuthorize, @PostFilter 를 처리한다. (사후체크)

RunAsManager : 임시권한을 부여한다.

 

AccessDecisionManagerVoter가 필요하여 AccessDecisionVoter가 다양한 Voter를 가지고 있지만, AfterInvocationManagerVoter가 필요하지 않으므로 AfterInvocationProvider만 있다. 

 

 

AccessDecisionVoter는 다양한 정책이 있어 긍정정책, 만장일치, 다수결정책 등이 있다. 하지만, AfterInvocationProvider는 PostInvocationAdviceProvider를 차례로 거치고나서 return하는 하나의 방식만 있다.

 

* Expression 기반의 configAttribute

 

Filter도 AccessDecisionManager를 사용하기 때문에 Voter기반이지만, WebExpressionVoter만 사용한다. 보통 어노테이션이나 config 설정 값들이 configAttribute로 metadataSource에서 해쉬맵 형태로 기록이 되어있는데, Filter쪽은 WebExpressionAttribute를 가지고 있다.

 

메서드쪽에서는 PreInvocationAttribute를 가지고 있다. 

 

Post로 처리하는 @PostAuthorize나 @PostFiler는 PostInvocationAttribute라는 configAttribute로 metadataSource에 등록이 된다. 

 

 

 

SecurityConfig만 다른데, 우리가 알고 있는 @Secured에 있는 String값을 SecurityConfig 값으로 가지고 있다. RoleVoterAuthenticatedVoter가 관여한다.

 

* AfterInvocationManager

 

Authentication 인증토큰만으로는 부족하기 때문에 리턴된 값을 사용자가 접근할 수 있는지 체크하기 위해서는 return되어 온 객체를 체크해야 한다. 이 때, 사용되는 어노테이션이 @PostAuthorized 혹은 @PostFilter이다. 

 

public Object decide(
  Authentication authentication,
  Object object,
  Collection<ConfigAttribute> config,
  Object returnedObject
) throws AccessDeniedException {

	Object result = returnedObject;
	for (AfterInvocationProvider provider : this.providers) {
		result = provider.decide(authentication, object, config, result);
	}
	return result;
}

 

 decide() 함수에 들어오면 AfterInvocationProviderManagerAfterInvocationProvider를 여러개를 가질 수 있으며 계속 decide()를 실행시켜 이 결과를 차곡차곡 쌓았다가 리턴을 해준다. (보통 @PostAuthorized 혹은 @PostFilter가 하나의 Provider로 제공이되어 한번에 검사하고 끝난다.)

 

provider.decide()에서 적용되는 provider는 PostInvocationAdviceProvider는 다음과 같다.

 

public Object decide(
  Authentication authentication,
  Object object,
  Collection<ConfigAttribute> config,
  Object returnedObject
) throws AccessDeniedException {

	PostInvocationAttribute postInvocationAttribute = findPostInvocationAttribute(config);
	if (postInvocationAttribute == null) {
		return returnedObject;
	}
	return this.postAdvice.after(authentication, (MethodInvocation) object, postInvocationAttribute,
				returnedObject);
}

 postInvocationAttribute 값이 있다면 postAdvice.after()를 요청한다.

 

잠시, 후처리에서 ConfigAttribute의 생성, 동작 방식을 살펴본다.

 

PostInvocationAttribute를 구현하고 있는 객체는 PostInvocationExpressionAttribute이다. PostInvocationExpressionAttribute 상위에 AbstractExpressionBasedMethodConfigAttribute가 있는데 Pre에서도 사용하고 Post에서도 동시에 handling하는 클래스이다.

 

abstract class AbstractExpressionBasedMethodConfigAttribute implements ConfigAttribute {

	private final Expression filterExpression;

	private final Expression authorizeExpression;

	/**
	 * Parses the supplied expressions as Spring-EL.
	 */
	AbstractExpressionBasedMethodConfigAttribute(String filterExpression, String authorizeExpression)
			throws ParseException {
		Assert.isTrue(filterExpression != null || authorizeExpression != null,
				"Filter and authorization Expressions cannot both be null");
		SpelExpressionParser parser = new SpelExpressionParser();
		this.filterExpression = (filterExpression != null) ? parser.parseExpression(filterExpression) : null;
		this.authorizeExpression = (authorizeExpression != null) ? parser.parseExpression(authorizeExpression) : null;
	}

	AbstractExpressionBasedMethodConfigAttribute(Expression filterExpression, Expression authorizeExpression)
			throws ParseException {
		Assert.isTrue(filterExpression != null || authorizeExpression != null,
				"Filter and authorization Expressions cannot both be null");
		this.filterExpression = (filterExpression != null) ? filterExpression : null;
		this.authorizeExpression = (authorizeExpression != null) ? authorizeExpression : null;
	}

	Expression getFilterExpression() {
		return this.filterExpression;
	}

	Expression getAuthorizeExpression() {
		return this.authorizeExpression;
	}

	@Override
	public String getAttribute() {
		return null;
	}

}

 

prePostEnable = true로 설정하면, 스프링 어노테이션에 있는 EL관련들을 확인하고 객체로 만들어 두었다가 decide()할 때 적용시키는 역할을 한다.

 

* ExpressionBasedPostInvocationAdvice 

 

PostInvocationAdviceProviderafter()가 무슨 역할을 하는지 구체적으로 살펴본다.

 

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;
}

 

 애노테이션으로부터 PostInvocationExpressAttribute로 파싱해온 pia 값을 가져오는데, Filter 값인 @postFilter와 Authorize 값인 @PostAuthorize를 각각 가져온다. (참고로 ctx는 pre와 post쪽에서 같이 사용한다.) filter()에서 @PostFilter를 적용해서 리턴된 returnObject를 다시 postAuthorize로 표현하여 evaluateAsBoolean()을 하여 리턴시킨다. 

 

 

* 실습

 

    @PostFilter("filterObject.state != T(com.sp.fc.service.Paper.State).PREPARE")
    @GetMapping("/mypapers")
    public List<Paper> myPapers(@AuthenticationPrincipal User user) {
        return paperService.getMyPapers(user.getUsername());
    }

 

Paper 엔티티에서 State가 PREPARE가 아닌 시험지들을 조회한다. Root Context에 해당 어노테이션을 정의한다.

 

@Getter
@Setter
public class CustomMethodSecurityExpressionRoot extends SecurityExpressionRoot
    implements MethodSecurityExpressionOperations {
...
    public boolean notPrepareState(Paper paper) {
        return paper.getState() != com.sp.fc.service.Paper.State.PREPARE;
    }
...
}

 

@PostFilter("notPrepareState(filterObject)")

 

나의 시험지만 조회하도록 하는 방법을 시큐리티를 사용할 수도 있다.

 

    @PostFilter("notPrepareState(filterObject) && filterObject.studentIds.contains(#user.username)")
    @GetMapping("/mypapers")
    public List<Paper> myPapers(@AuthenticationPrincipal User user) {
        return paperService.getMyPapers(user.getUsername());
    }
    
        public List<Paper> getMyPapers(String username) {
        return new ArrayList<>(paperDB.values());
//        return paperDB.values().stream().filter(
//                paper -> paper.getStudentIds().contains(username)
//        ).collect(Collectors.toList());
    }

 

@PostFilter를 Service에 정의한다면 어떨까?

 

    @GetMapping("/mypapers")
    public List<Paper> myPapers(@AuthenticationPrincipal User user) {
        return paperService.getMyPapers(user.getUsername());
    }
    
    @PostFilter("notPrepareState(filterObject)")
    public List<Paper> getMyPapers(String username) {
//        return new ArrayList<>(paperDB.values());
        return paperDB.values().stream().filter(
                paper -> paper.getStudentIds().contains(username)
        ).collect(Collectors.toList());
    }

 

Service에 정의하면 @PostFilter는 제대로 적용되지 않는다. 이 경우에는 Bean의 생성주기와 관련이 있다. MethodSecurityConfigurationCustomPermissionEvaluator을 DI로 주입하고, CustomPermissionEvaluatorPaperService를 주입하여 생성이 된다. 하지만, PaperService가 너무 빨리 생성되어 Service를 감싸는 Proxy가 생성이 안되는 이슈가 있다. 이 경우 PaperService@Lazy해야한다.

 

@Component
public class CustomPermissionEvaluator implements PermissionEvaluator {
  @Lazy
  private PaperService paperService;
 ...

 

일반적으로 Authorize는 리스트가 아닌 단일 객체를 리턴하는 경우 PostAuthorization을 검사하고, PostFilter는 List로 리턴될 때 검사를 한다.

 

 

//    @PreAuthorize("hasPermission(#paperId, 'paper', 'read')")
@PostAuthorize("returnObject.studentIds.contains(#user.username)")
@GetMapping("/get/{paperId}")
public Paper getPaper(@AuthenticationPrincipal User user, @PathVariable Long paperId){
    return paperService.getPaper(paperId);
}

 

@PreAuthorize로 체크하는 것이 hasPermission으로 PaperService로 시험지를 열어보는 것보다는, @PostAuthorize으로 검사하는 것이 낫다.

 

@PostAuthorize("returnObject.studentIds.contains(principal.username)")
public Paper getPaper(Long paperId) {
    return paperDB.get(paperId);
}

 

principal@PostAuthorize를 사용했을 때 현재 로그인 된 사용자의 정보가 담겨 있으며 loadUserByUsername()으로 조회한 사용자 정보가 담겨 있다.

 

Service에 @PosAuthorize를 사용해도 되는데, 매개변수에 User가 없기 때문에 principal을 가져온다. 그런데, 보통 Service에서 권한을 주기보다는 Controller에 주는것이 자연스럽다. Service에서 권한에 상관없이 시험지를 조회할 수 있는 경우가 있기 때문이다.

 

 

 

PreInvocationAuthorizationAdviceVoterPreInvocation, PostInvocation 모두 MethodSecurityExpressionRoot를 같이 사용한다. pre에서는 filterObject를 사용한다(보통 컬렉션을 recursive하게 검사할 때 사용한다.) post에서는 returnObject가 필요하다. pre에서는 리턴이 없어서 returnObject가 없지만 MethodSecurityExpressionRoot를 공유하므로 filterObjectreturnObject가 있는 것이다.

반응형

'Spring > Spring Security' 카테고리의 다른 글

임시권한 부여하기  (0) 2021.10.23
@Secured 기반 권한체크  (0) 2021.10.20
권한위원회 voter  (0) 2021.10.17
스프링 시큐리티의 권한(Authorization)  (0) 2021.10.15
권한체크 오류처리  (0) 2021.10.15