후처리란 voter 기반 체크 이후 메서드가 결과를 리턴한 다음, 리턴한 객체를 사용자가 접근할 수 있는지 판단하는 것
-> Access Decision Manager
는 주로 사전 체크를 담당함
-> 리턴된 객체에 대해서 검사하는 매니저는 AfterInvocation Manager
필터를 통과한 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들이 처리
Authentication 통행증만 가지고는 권한 체크가 충분하지 않음
Method를 거쳐 Return된 객체를 사용자가 접근할 수 있는지 없는지 체크할려면
-> 리턴되어 온 객체를 한번 체크를 해봐야 하고 이때 사용되는 애노테이션이 @PostAuthorized or @PostFilter
예를 들어 어떤 객체의 값을 변경해야 하는 경우에는 메소드에 들어오기 전에
, 값을 조회하려고 하는 경우에는 값을 가져온 이후
에 각각 접근 권한을 체크
체크해야 할 대상이 한개라면 Pre/PostAuthorized
로 체크를 하면 되지만, 대상이 복수개라면 보통은 리스트로 묶이기 때문에 대상을 filtering
을 해서 들어가거나 넘겨야 함
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;
}
대표적인 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);
}
(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
PreInvocationVoter
나 PostInvocation
는 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);
}
}