WebSecurityExpressionRoot 클래스는 SpEL 표현식에서 사용할수 있는 다양한 메소드를 제공
SpEL 표현식 커스텀 핸들러 구현
Admin 사용자의 로그인 아이디 끝 숫자가 홀수 인 경우 접근 요청을 승인하는 SpEL 표현식을 구현
admin01 — 접근 허용
admin02 — 접근 거부
WebSecurityExpressionRoot를 상속하고, Admin 사용자의 로그인 아이디 끝 숫자가 홀수 인 경우 접근 요청을 승인하는 isOddAdmin() 메소드를 추가함
AbstractSecurityExpressionHandler를 상속하고, CustomWebSecurityExpressionRoot 객체를 생성하는 SecurityExpressionHandler 구현체 추가
커스텀 SpEL 표현식 (oddAdmin)을 추가하고, CustomWebSecurityExpressionHandler 를 설정
"isFullyAuthenticated() and hasRole('ADMIN') and oddAdmin" — 명시적 로그인을 수행한 Admin 사용자의 로그인 아이디 끝 숫자가 홀수 인 경우 접근 허용
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?
미션
"/admin" URL 접근에 대한 접근 권한 검사를 SpEL 표현식 방식에서 voter 방식으로 변경해보기 (OddAdminVoter 클래스)
AccessDecisionVoter<FilterInvocation> 인터페이스를 구현하는 OddAdminVoter 클래스 추가
Admin 사용자의 로그인 아이디 끝 숫자가 홀수 인 경우 접근 허용
URL이 "/admin" 이 아닌 경우 접근을 승인함
expressionHandler(expressionHandler()) 부분 삭제 — 기본 expressionHandler를 사용함
표현식에서 oddAdmin 부분 삭제 (삭제 후 표현식은 아래와 같음)
💡
antMatchers("/admin").access("isFullyAuthenticated() and hasRole('ADMIN')")
AccessDecisionManager 인터페이스 구현체 중 UnanimousBased를 사용하도록 설정하고, 아래 voter를 추가
private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws IOException, ServletException {
if (SecurityContextHolder.getContext().getAuthentication() != null) {
this.logger.debug(LogMessage
.of(() -> "SecurityContextHolder not populated with remember-me token, as it already contained: '"
+ SecurityContextHolder.getContext().getAuthentication() + "'"));
chain.doFilter(request, response);
return;
}
Authentication rememberMeAuth = this.rememberMeServices.autoLogin(request, response);
if (rememberMeAuth != null) {
// Attempt authenticaton via AuthenticationManager
try {
rememberMeAuth = this.authenticationManager.authenticate(rememberMeAuth);
// Store to SecurityContextHolder
SecurityContextHolder.getContext().setAuthentication(rememberMeAuth);
onSuccessfulAuthentication(request, response, rememberMeAuth);
this.logger.debug(LogMessage.of(() -> "SecurityContextHolder populated with remember-me token: '"
+ SecurityContextHolder.getContext().getAuthentication() + "'"));
if (this.eventPublisher != null) {
this.eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent(
SecurityContextHolder.getContext().getAuthentication(), this.getClass()));
}
if (this.successHandler != null) {
this.successHandler.onAuthenticationSuccess(request, response, rememberMeAuth);
return;
}
}
catch (AuthenticationException ex) {
this.logger.debug(LogMessage
.format("SecurityContextHolder not populated with remember-me token, as AuthenticationManager "
+ "rejected Authentication returned by RememberMeServices: '%s'; "
+ "invalidating remember-me token", rememberMeAuth),
ex);
this.rememberMeServices.loginFail(request, response);
onUnsuccessfulAuthentication(request, response, ex);
}
}
chain.doFilter(request, response);
}
RememberMeAuthenticationFilter 구현 일부 발췌
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
if (!supports(authentication.getClass())) {
return null;
}
if (this.key.hashCode() != ((RememberMeAuthenticationToken) authentication).getKeyHash()) {
throw new BadCredentialsException(this.messages.getMessage("RememberMeAuthenticationProvider.incorrectKey",
"The presented RememberMeAuthenticationToken does not contain the expected key"));
}
return authentication;
}
RememberMeAuthenticationProvider 구현 일부 발췌
@Override
public final boolean isFullyAuthenticated() {
return !this.trustResolver.isAnonymous(this.authentication)
&& !this.trustResolver.isRememberMe(this.authentication);
}
SecurityExpressionRoot 구현 일부 발췌
private SecurityContextRepository repo;
private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws IOException, ServletException {
// ensure that filter is only applied once per request
if (request.getAttribute(FILTER_APPLIED) != null) {
chain.doFilter(request, response);
return;
}
request.setAttribute(FILTER_APPLIED, Boolean.TRUE);
if (this.forceEagerSessionCreation) {
HttpSession session = request.getSession();
if (this.logger.isDebugEnabled() && session.isNew()) {
this.logger.debug(LogMessage.format("Created session %s eagerly", session.getId()));
}
}
HttpRequestResponseHolder holder = new HttpRequestResponseHolder(request, response);
SecurityContext contextBeforeChainExecution = this.repo.loadContext(holder);
try {
SecurityContextHolder.setContext(contextBeforeChainExecution);
if (contextBeforeChainExecution.getAuthentication() == null) {
logger.debug("Set SecurityContextHolder to empty SecurityContext");
}
else {
if (this.logger.isDebugEnabled()) {
this.logger
.debug(LogMessage.format("Set SecurityContextHolder to %s", contextBeforeChainExecution));
}
}
chain.doFilter(holder.getRequest(), holder.getResponse());
}
finally {
SecurityContext contextAfterChainExecution = SecurityContextHolder.getContext();
// Crucial removal of SecurityContextHolder contents before anything else.
SecurityContextHolder.clearContext();
this.repo.saveContext(contextAfterChainExecution, holder.getRequest(), holder.getResponse());
request.removeAttribute(FILTER_APPLIED);
this.logger.debug("Cleared SecurityContextHolder to complete request");
}
}
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;
}
}