해당 필터가 호출되는 시점에서 사용자는 이미 인증이 완료된 상태입니다.
(익명 사용자도 인증이 완료된 것으로 ROLE_ANONYMOUS 권한)
보호되는 리소스에서 요구하는 권한 정보는 SecurityMetadataSource
인터페이스를 통해 ConfigAttribute
타입으로 가져오게 됩니다.
FilterSecurityInterceptor
AbstractSecurityInterceptor
AccessDecisionManager
AccessDecisionVoter
public void invoke(FilterInvocation filterInvocation) throws IOException, ServletException {
if (this.isApplied(filterInvocation) && this.observeOncePerRequest) {
filterInvocation.getChain().doFilter(filterInvocation.getRequest(), filterInvocation.getResponse());
} else {
if (filterInvocation.getRequest() != null && this.observeOncePerRequest) {
filterInvocation.getRequest().setAttribute("__spring_security_filterSecurityInterceptor_filterApplied", Boolean.TRUE);
}
// ✨ AbstractSecurityInterceptor → beforeInvocation()를 실행하게 됩니다.
InterceptorStatusToken token = super.beforeInvocation(filterInvocation);
try {
filterInvocation.getChain().doFilter(filterInvocation.getRequest(), filterInvocation.getResponse());
} finally {
super.finallyInvocation(token);
}
super.afterInvocation(token, (Object)null);
}
}
protected InterceptorStatusToken beforeInvocation(Object object) {
Assert.notNull(object, "Object was null");
if (!this.getSecureObjectClass().isAssignableFrom(object.getClass())) {
throw new IllegalArgumentException("Security invocation attempted for object " + object.getClass().getName() + " but AbstractSecurityInterceptor only configured to support secure objects of type: " + this.getSecureObjectClass());
} else {
// ✨ 이 부분이 리소스에서 요구하는 권한 목록들을 가져오는 구간입니다.
// SecurityMetadataSource 인터페이스 구현체가 ConfigAttribute 타입으로 가져오게 됩니다.
Collection<ConfigAttribute> attributes = this.obtainSecurityMetadataSource().getAttributes(object);
if (CollectionUtils.isEmpty(attributes)) {
Assert.isTrue(!this.rejectPublicInvocations, () -> {
return "Secure object invocation " + object + " was denied as public invocations are not allowed via this interceptor. This indicates a configuration error because the rejectPublicInvocations property is set to 'true'";
});
if (this.logger.isDebugEnabled()) {
this.logger.debug(LogMessage.format("Authorized public object %s", object));
}
this.publishEvent(new PublicInvocationEvent(object));
return null;
} else {
// ✨ 리소스에서 요구하는 권한이 있는 경우에 이제 해당 권한을 가지고 있는지 확인해야 겠죠?
if (SecurityContextHolder.getContext().getAuthentication() == null) {
this.credentialsNotFound(this.messages.getMessage("AbstractSecurityInterceptor.authenticationNotFound", "An Authentication object was not found in the SecurityContext"), object, attributes);
}
Authentication authenticated = this.authenticateIfRequired();
if (this.logger.isTraceEnabled()) {
this.logger.trace(LogMessage.format("Authorizing %s with attributes %s", object, attributes));
}
// ✨ 여기서 결국 처리를 하게 됩니다!
this.attemptAuthorization(object, attributes, authenticated);
if (this.logger.isDebugEnabled()) {
this.logger.debug(LogMessage.format("Authorized %s with attributes %s", object, attributes));
}
if (this.publishAuthorizationSuccess) {
this.publishEvent(new AuthorizedEvent(object, attributes, authenticated));
}
Authentication runAs = this.runAsManager.buildRunAs(authenticated, object, attributes);
if (runAs != null) {
SecurityContext origCtx = SecurityContextHolder.getContext();
SecurityContext newCtx = SecurityContextHolder.createEmptyContext();
newCtx.setAuthentication(runAs);
SecurityContextHolder.setContext(newCtx);
if (this.logger.isDebugEnabled()) {
this.logger.debug(LogMessage.format("Switched to RunAs authentication %s", runAs));
}
return new InterceptorStatusToken(origCtx, true, attributes, object);
} else {
this.logger.trace("Did not switch RunAs authentication since RunAsManager returned null");
return new InterceptorStatusToken(SecurityContextHolder.getContext(), false, attributes, object);
}
}
}
}
// ✨ 권한을 가졌는지 체크하기!
private void attemptAuthorization(Object object, Collection<ConfigAttribute> attributes, Authentication authenticated) {
try {
// ✨ AccessDecisionManager의 구현체인 AffirmativeBased가 실행되게 됩니다.
this.accessDecisionManager.decide(authenticated, object, attributes);
} catch (AccessDeniedException var5) {
if (this.logger.isTraceEnabled()) {
this.logger.trace(LogMessage.format("Failed to authorize %s with attributes %s using %s", object, attributes, this.accessDecisionManager));
} else if (this.logger.isDebugEnabled()) {
this.logger.debug(LogMessage.format("Failed to authorize %s with attributes %s", object, attributes));
}
this.publishEvent(new AuthorizationFailureEvent(object, attributes, authenticated, var5));
throw var5;
}
}
AccessDecisionVoter
목록을 가지고, 투표를 하게 되어 결과를 취합해서 접근 승인 여부를 결정하게 됩니다.
여기에는 총 3가지 구현체를 제공하게 됩니다.
먼저 기본값인 AffirmativeBased
부터 살펴보겠습니다.
이 친구는 승인 되는 녀석이 하나라도 있다면 접근이 승인됩니다!
public void decide(Authentication authentication, Object object, Collection<ConfigAttribute> configAttributes) throws AccessDeniedException {
int deny = 0;
Iterator var5 = this.getDecisionVoters().iterator();
while(var5.hasNext()) {
AccessDecisionVoter voter = (AccessDecisionVoter)var5.next();
int result = voter.vote(authentication, object, configAttributes);
switch (result) {
case -1:
++deny;
break;
case 1: // ✨ 한 녀석이라도 승인 되면 엌케이!
return;
}
}
if (deny > 0) {
throw new AccessDeniedException(this.messages.getMessage("AbstractAccessDecisionManager.accessDenied", "Access is denied"));
} else {
this.checkAllowIfAllAbstainDecisions();
}
}
이 친구는 다수의 Voter가 승인을 해야 접근이 승인됩니다!
public void decide(Authentication authentication, Object object, Collection<ConfigAttribute> configAttributes) throws AccessDeniedException {
int grant = 0;
int deny = 0;
Iterator var6 = this.getDecisionVoters().iterator();
while(var6.hasNext()) {
AccessDecisionVoter voter = (AccessDecisionVoter)var6.next();
int result = voter.vote(authentication, object, configAttributes);
switch (result) {
case -1:
++deny;
break;
case 1:
++grant;
}
}
// ✨ 보시면 이제 허가가 과반수 이상인지를 확인할 수 있죠?
if (grant <= deny) {
if (deny > grant) {
throw new AccessDeniedException(this.messages.getMessage("AbstractAccessDecisionManager.accessDenied", "Access is denied"));
} else if (grant == deny && grant != 0) {
if (!this.allowIfEqualGrantedDeniedDecisions) {
throw new AccessDeniedException(this.messages.getMessage("AbstractAccessDecisionManager.accessDenied", "Access is denied"));
}
} else {
this.checkAllowIfAllAbstainDecisions();
}
}
}
마지막으로 이 친구는 만장일치가 되어야만 승인됩니다!
public void decide(Authentication authentication, Object object, Collection<ConfigAttribute> attributes) throws AccessDeniedException {
int grant = 0;
List<ConfigAttribute> singleAttributeList = new ArrayList(1);
singleAttributeList.add((Object)null);
Iterator var6 = attributes.iterator();
while(var6.hasNext()) {
ConfigAttribute attribute = (ConfigAttribute)var6.next();
singleAttributeList.set(0, attribute);
Iterator var8 = this.getDecisionVoters().iterator();
while(var8.hasNext()) {
AccessDecisionVoter voter = (AccessDecisionVoter)var8.next();
int result = voter.vote(authentication, object, singleAttributeList);
switch (result) {
case -1: // ✨ 한명이라도 반대를 하게 되면 Exception을 던지는것을 볼 수 있죠?
throw new AccessDeniedException(this.messages.getMessage("AbstractAccessDecisionManager.accessDenied", "Access is denied"));
case 1:
++grant;
}
}
}
if (grant <= 0) {
this.checkAllowIfAllAbstainDecisions();
}
}
각각의 Voter는 접근을 승인, 거절, 보류를 판단하게 됩니다.
int ACCESS_GRANTED = 1; // 승인
int ACCESS_ABSTAIN = 0; // 보류
int ACCESS_DENIED = -1; // 거절
int vote(Authentication authentication, S object, Collection<ConfigAttribute> attributes);
SpEL(Spring Expression Language)
....