image

어제 공부했던 Spring Security 인증처리 과정에서대해
코드를 찾아가보면서 어느정도 깨달음이 있어서
진전이 있는 느낌이 들어 오랜만에 편안한 마음으로 공부를 했다.

오늘은 Spring Security 권한관련 Filter를 공부하는 날이고
섹션 중 Spring Security는 마지막인 시간이다.


어제 (Spring Security 인증처리)에 대한 내용을
코드와 아케텍쳐를 통해 쭉 살펴보았다.

오늘은 권한과 관련된 Filter인
AuthorizationFilter 클래스에 대한 권한 부여의
간단한 처리 흐름과 코드를 살펴보려고한다.


Spring Security 권한부여

어제는 인증처리에 관련된 내용을 배웠다.
인증이란 이 사이트를 이용할 수 있는 유저인지 확인하는 과정이다.
하지만 권한부여는 뭘까?

인증된 사용자가 모든 리소스에 접근을 제한하는 것을 말한다.
헤딩 사용자가 이용할 수 있는 리소스를 제한하거나 풀어주는 것을
권한 부여라고 한다.


권한부여 처리흐름

AuthorizationFilter

image

Spring Security에서는 권한 부여를 위해
AuthorizationFilter 클래스를 이용해 처리를 한다.

SecurityFilterChain을 구성하는 Filter 중 하나로
인증 Filter를 거친 후 진행하게 되어진다.
본격적으로 AuthorizationFilter의 권한처리 흐름을 알아보자


(1). SecurityContextHolder로 부터 Authentication 객채를 가져온다.
(Spring Security 인증처리) 과정에서 인증이 완료된 객체 Authentication
SecurityContextHolder에 넣어줬던 흐름이 인증처리의 마지막이어었다.

public class AuthorizationFilter extends OncePerRequestFilter {
    
    ... 이상 생략
    
    private Authentication getAuthentication() {
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        if (authentication == null) {
            throw new AuthenticationCredentialsNotFoundException(
                    "An Authentication object was not found in the SecurityContext");
        }
        return authentication;
    }
    
    ... 이하 생략
            
}

실제로 AuthorizationFilter 클래스 코드 중 getAuthentication(); 메서드를 이용해
Authentication 객체를 꺼내는 모습을 코드로 볼 수 있다.


(2). Authentication 객체와 HttpServletRequest 객체를
AuthorizationManager에게 전달한다.

public class AuthorizationFilter extends OncePerRequestFilter {

    private final AuthorizationManager<HttpServletRequest> authorizationManager;
     
    ... 생략
    
    public AuthorizationFilter(AuthorizationManager<HttpServletRequest> authorizationManager) {
        Assert.notNull(authorizationManager, "authorizationManager cannot be null");
        this.authorizationManager = authorizationManager;
    }

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
            throws ServletException, IOException {

        AuthorizationDecision decision = this.authorizationManager.check(this::getAuthentication, request);
        this.eventPublisher.publishAuthorizationEvent(this::getAuthentication, request, decision);
        if (decision != null && !decision.isGranted()) {
            throw new AccessDeniedException("Access Denied");
        }
        filterChain.doFilter(request, response);
    }
    
    ... 이하 생략

}

실제로 코드를 확인해보면
생성자로 AuthorizationManager의 객체를 DI받고 있다.
그리고 .check(this::getAuthentication, request); 메서드를 호출해
AuthenticationHttpServletRequest 객체를 매개변수로 넘겨주고있다.


(3). AuthorizationManager
권한 부여 처리를 총괄하는 매니저 역할의 인터페이스이다.
구현체는 RequestMatcherDelegatingAuthorizationManager 클래스이다.

@FunctionalInterface
public interface AuthorizationManager<T> {
    
    ... 이상 생략

	@Nullable
	AuthorizationDecision check(Supplier<Authentication> authentication, T object);
}

코드로도 볼 수 있듯이 인터페이스에 메서드가 정의되어있고

public final class RequestMatcherDelegatingAuthorizationManager implements AuthorizationManager<HttpServletRequest> {

    ...
    이상 생략

    @Override
    public AuthorizationDecision check(Supplier<Authentication> authentication, HttpServletRequest request) {
        if (this.logger.isTraceEnabled()) {
            this.logger.trace(LogMessage.format("Authorizing %s", request));
        }
        for (RequestMatcherEntry<AuthorizationManager<RequestAuthorizationContext>> mapping : this.mappings) {

            RequestMatcher matcher = mapping.getRequestMatcher();
            MatchResult matchResult = matcher.matcher(request);
            if (matchResult.isMatch()) {
                AuthorizationManager<RequestAuthorizationContext> manager = mapping.getEntry();
                if (this.logger.isTraceEnabled()) {
                    this.logger.trace(LogMessage.format("Checking authorization on %s using %s", request, manager));
                }
                return manager.check(authentication,
                        new RequestAuthorizationContext(request, matchResult.getVariables()));
            }
        }
        this.logger.trace("Abstaining since did not find matching RequestMatcher");
        return null;
    }

    ... 이하 생략
            
}

구현체인 RequestMatcherDelegatingAuthorizationManager 클래스에
check();메서드가 Override되어 있는 모습을 확인할 수 있다.


(4),(5). 이제 전달받은 객체들로 check(); 메서드 내부에서
권한 처리에 대한 로직이 수행된다.

  1. RequestMatcherEntry 정보를 얻은 후에 RequestMatcher 객체를 얻는다.
  2. RequestMatcher 객체로 HttpServletRequest 객체를 넣어 MatchResult 객체를 만든다.
  3. MatchResult 객체의 메서드 .isMatch();로 권한을 체크한다.

만약에 권한이 존재해 if (matchResult.isMatch()) 메서드를 통과한다면
manager.check();를 통해서 matchResult.getVariables();를 통해서
Map을 담고있는 객체가 반환되며 권한 부여과정이 진행되고

만약에 권한이 존재하지 않아 if (matchResult.isMatch()) 메서드를 통과하지 못한다면
return null;로 인해 null값이 반환되게 되어진다.
즉, 맨처음에 우리가 check();메서드를 호출했던 AuthorizationFilter에서

@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
        throws ServletException, IOException {

    AuthorizationDecision decision = this.authorizationManager.check(this::getAuthentication, request);
    this.eventPublisher.publishAuthorizationEvent(this::getAuthentication, request, decision);
    if (decision != null && !decision.isGranted()) {
        throw new AccessDeniedException("Access Denied");
    }
    filterChain.doFilter(request, response);
}

throw new AccessDeniedException("Access Denied"); 예외를 던지게되어
권한을 부여하지 않도록 처리를 해주는 것이다.


접근 제어 표현식

Spring Security 웹 및 메서드 보안을 위한 표현식

Spring EL

표현식 : 설명 :
hasRole 현재 보안 주체(principal)가 지정된 역할을 갖고 있는지 여부를 확인하고 가지고 있다면 true를 리턴한다.
hasRole(’admin’)처럼 파라미터로 넘긴 role이 ROLE_ 로 시작하지 않으면 기본적으로 추가한다.
(DefaultWebSecurityExpressionHandler의 defaultRolePrefix를 수정하면 커스텀할 수 있다.)
hasAnyRole 현재 보안 주체가 지정한 역할 중 1개라도 가지고 있으면 true를 리턴한다. (문자열 리스트를 콤마로 구분해서 전달한다.)
ex) hasAnyRole(’admin’, ‘user’)
hasAuthority 현재 보안 주체가 지정한 권한을 갖고 있는지 여부를 확인하고 가지고 있다면 true를 리턴한다.
ex) hasAuthority(’read’)
hasAnyAuthority 현재 보안 주체가 지정한 권한 중 하나라도 있으면 true를 리턴한다.
ex) hasAnyAuthority(’read’, ‘write’)
principal 현재 사용자를 나타내는 principal 객체에 직접 접근할 수 있다.
authentication SecurityContext로 조회할 수 있는 현재 Authentication 객체에 직접 접근할 수 있다.
permitAll 항상 true로 평가한다.
denyAll 항상 false로 평가한다.
isAnonymous() 현재 보안 주체가 익명 사용자면 true를 리턴한다.
isRememberMe() 현재 보안 주체가 remember-me 사용자면 true를 리턴한다.
isAuthenticated() 사용자가 익명이 아닌 경우 true를 리턴한다.
isFullyAuthenticated() 사용자가 익명 사용자나 remember-me 사용자가 아니면 true를 리턴한다.
hasPermission
(Object target, Object permission)
사용자가 target에 해당 permission 권한이 있으면 true를 리턴한다.
ex) hasPermission(domainObject, ‘read’)
hasPermission
(Object targetId, String targetType, Object permission)
사용자가 target에 해당 permission 권한이 있으면 true를 리턴한다.
ex) hasPermission(1, ‘com.example.domain.Message’, ‘read’)



이렇게 오늘을 끝으로 Spring Security의
인증과 권한에 대해 알아보았다.
좀 더 깊은내용을 이해하기 위해서는 여러가지 Filter에 대해 공부가
필요해보이고, 내일부터 공부하는 JWT를 이용할때 어떠한 방식으로
Spring Security 로직이 변경되는지 내일부터 다시 공부해보자

오늘 공부는 여기서 끝 !!


오늘의 커피량: ☕️ ☕️
오늘의 점심: 삽겹살, 된장찌개, 밥