WebSecurityExpressionRoot 클래스는 SpEL 표현식에서 사용할수 있는 다양한 메소드를 제공
커스텀 SpEL 생성 & 적용
SpEL 표현식 커스텀 핸들러 구현
WebSecurityExpressionRoot를 상속하는 클래스 생성
Admin 사용자의 로그인 아이디 끝 숫자가 홀수 인 경우 접근 요청을 승인하는 SpEL 표현식을 구현
admin01 — 접근 허용
admin02 — 접근 거부
WebSecurityExpressionRoot를 상속하고, Admin 사용자의 로그인 아이디 끝 숫자가 홀수 인 경우 접근 요청을 승인하는 isOddAdmin() 메소드를 추가함
AbstractSecurityExpressionHandler를 상속하는 클래스 생성. CustomWebSecurityExpressionRoot 객체를 생성하는 CustomWebSecurityExpressionHandler구현체 (DefaultWebSecurityExpressionHandler의 구현 형태를 보고 참고해서 만들면 됨)커스텀 SpEL 표현식 (oddAdmin)을 추가하고, CustomWebSecurityExpressionHandler 를 설정(expresionHandler( ))
"isFullyAuthenticated() and hasRole('ADMIN') and oddAdmin" — 명시적 로그인을 수행한 Admin 사용자의 로그인 아이디 끝 숫자가 홀수 인 경우 접근 허용
SpEL에서 isOddAdmin( ), oddAdmin 이 두 방식으로 호출이 가능함
expressionHandler를 제대로 설정하지 않으면 ExpressionUtils.evaluateAsBoolean() 메소드에서 예외가 발생함
DEBUG 로그 레벨로는 확인할 수 있으며, ExpressionUtils 클래스에 브레이크 포인트를 걸어두고 확인할 수 있음
⚠️
org.springframework.expression.spel.SpelEvaluationException: EL1008E: Property or field 'oddAdmin' cannot be found on object of type 'org.springframework.security.web.access.expression.WebSecurityExpressionRoot' - maybe not public or not valid?
// FilterSecurityInterceptor
public void invoke(FilterInvocation filterInvocation) throws IOException, ServletException {
if (isApplied(filterInvocation) && this.observeOncePerRequest) {
// filter already applied to this request and user wants us to observe
// once-per-request handling, so don't re-do security checking
filterInvocation.getChain().doFilter(filterInvocation.getRequest(), filterInvocation.getResponse());
return;
}
// first time this request being called, so perform security checking
if (filterInvocation.getRequest() != null && this.observeOncePerRequest) {
filterInvocation.getRequest().setAttribute(FILTER_APPLIED, Boolean.TRUE);
}
InterceptorStatusToken token = super.beforeInvocation(filterInvocation);
try {
filterInvocation.getChain().doFilter(filterInvocation.getRequest(), filterInvocation.getResponse());
}
finally {
super.finallyInvocation(token);
}
super.afterInvocation(token, null);
}
//beforeInvocation => AbstractSecurityInterceptor
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 {
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);
/* ... */
}
private void attemptAuthorization(Object object, Collection<ConfigAttribute> attributes, Authentication authenticated) {
try {
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;
}
}
@Override
@SuppressWarnings({ "rawtypes", "unchecked" })
public void decide(Authentication authentication, Object object, Collection<ConfigAttribute> attributes)
throws AccessDeniedException {
int grant = 0;
List<ConfigAttribute> singleAttributeList = new ArrayList<>(1);
singleAttributeList.add(null);
for (ConfigAttribute attribute : attributes) {
singleAttributeList.set(0, attribute);
for (AccessDecisionVoter voter : getDecisionVoters()) {
int result = voter.vote(authentication, object, singleAttributeList);
/* ... */
}
http.authorizeRequests()
.antMatchers("/me").hasAnyRole("USER", "ADMIN")
.antMatchers("/admin").access("hasRole('ADMIN') and isFullyAuthenticated()")
.anyRequest().permitAll()
.accessDecisionManager(new UnanimousBased(List.of(
new WebExpressionVoter(),
new OddAdminVoter())))
.and()
package com.prgms.devcourse.springsecuritymasterclass.config;
import org.springframework.security.access.AccessDecisionVoter;
import org.springframework.security.access.ConfigAttribute;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.web.FilterInvocation;
import org.springframework.security.web.util.matcher.RequestMatcher;
import javax.servlet.http.HttpServletRequest;
import java.util.Collection;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class OddAdminVoter implements AccessDecisionVoter<FilterInvocation> {
private static final Pattern PATTERN = Pattern.compile("[0-9]$");
private final RequestMatcher requestMatcher;
public OddAdminVoter(RequestMatcher requestMatcher) {
this.requestMatcher = requestMatcher;
}
@Override
public boolean supports(ConfigAttribute attribute) {
return true;
}
@Override
public boolean supports(Class<?> clazz) {
return FilterInvocation.class.isAssignableFrom(clazz);
}
@Override
public int vote(Authentication authentication, FilterInvocation object, Collection<ConfigAttribute> attributes) {
if(!requiresAuthorization(object.getRequest()))
return ACCESS_GRANTED;
User user = (User)authentication.getPrincipal();
String username = user.getUsername();
Matcher matcher = PATTERN.matcher(username);
if(matcher.find()){
int number = Integer.parseInt(matcher.group());
if(number % 2 != 0){
return ACCESS_GRANTED;
}
return ACCESS_DENIED;
}
return ACCESS_ABSTAIN;
}
private boolean requiresAuthorization(HttpServletRequest request){
return requestMatcher.matches(request);
}
}
http.authorizeRequests()
.antMatchers("/me").hasAnyRole("USER", "ADMIN")
.antMatchers("/admin").access("hasRole('ADMIN') and isFullyAuthenticated()")
.anyRequest().permitAll()
int ACCESS_GRANTED = 1;
int ACCESS_ABSTAIN = 0;
int ACCESS_DENIED = -1;
int vote(Authentication authentication, S object, Collection<ConfigAttribute> attributes);
AccessDecisionVoter 구현 일부 발췌
public class CustomWebSecurityExpressionRoot extends WebSecurityExpressionRoot {
static final Pattern PATTERN = Pattern.compile("[0-9]+$");
public CustomWebSecurityExpressionRoot(Authentication a, FilterInvocation fi) {
super(a, fi);
}
public boolean isOddAdmin() {
User user = (User) getAuthentication().getPrincipal();
String name = user.getUsername();
Matcher matcher = PATTERN.matcher(name);
if (matcher.find()) {
int number = toInt(matcher.group(), 0);
return number % 2 == 1;
}
return false;
}
}
public class CustomWebSecurityExpressionHandler extends AbstractSecurityExpressionHandler<FilterInvocation> {
private final AuthenticationTrustResolver trustResolver;
private final String defaultRolePrefix;
public CustomWebSecurityExpressionHandler(AuthenticationTrustResolver trustResolver, String defaultRolePrefix) {
this.trustResolver = trustResolver;
this.defaultRolePrefix = defaultRolePrefix;
}
@Override
protected SecurityExpressionOperations createSecurityExpressionRoot(Authentication authentication, FilterInvocation fi) {
CustomWebSecurityExpressionRoot root = new CustomWebSecurityExpressionRoot(authentication, fi);
root.setPermissionEvaluator(getPermissionEvaluator());
root.setTrustResolver(this.trustResolver);
root.setRoleHierarchy(getRoleHierarchy());
root.setDefaultRolePrefix(this.defaultRolePrefix);
return root;
}
}