[SpringSecurity] Authorization 아키텍쳐

유알·2023년 1월 3일
1

[SpringSecurity]

목록 보기
8/15

시작

나의 이전 글에서는 주로 Authentication을 중점으로 다루었다.
Authentication은 쉽게 말하면 로그인이다.
Authorization은 권한 체크이다.

예를 들면, Authentication은 사원증 발급이고, Authorization은 경비 아저씨가 출입을 체크하는 것이다.

Authorities

처음에는 나도 인증과 인가의 과정이 굉장히 헷갈렸는데, 쉽게 말하면, 인증(Authentication)은 로그인 정보를 받아서 인증된 Authenticaiton을 만드는 과정이다. (인증되지 않은 Authentication도 존재한다.)

Authentication에는 Authorities 즉 역할이 존재한다.
이 역할을 이용해서 (또는 다른 정보도 같이 이용해서) 접근을 제어하는 것이 Authorization이라고 생각 하면 된다.

GrantedAuthority

//Authentication
	Collection<? extends GrantedAuthority> getAuthorities();

이 역할은 GrantedAuthority의 Collection으로 저장된다.

public interface GrantedAuthority extends Serializable {
	String getAuthority();
}

GrantedAuthority는 메서드가 하나뿐인 인터페이스이다. 이는 String을 반환한다.
만약 역할이 String으로 표현될 수 없으면 null을 반환하고 이 역할을 표현할 수 있는 방법을 구현해야한다.
그리고 이를 사용하는 AccessDecisionManager에 이를 읽어드릴 수 있는 방법을 구현해야한다.

처리과정

이 과정에 대해서 아래에서 상술할 것이다.
이 시작지점은 크게 두가지가 있다. FilterSecurityInterceptor, AuthorizationFilter
FilterSecurityInterceptor의 경우 AuthorizationFilter로 대체되었다.
https://docs.spring.io/spring-security/reference/servlet/authorization/authorize-requests.html

아래에서는 AuthorizationFilter만 서술하겠다.

AuthorizationFilter

여기서 시작지점은 SecurityFilterChain안에 있는 AuthorizationFilter이다.

등록 방법은

@Bean
SecurityFilterChain web(HttpSecurity http) throws AuthenticationException {
    http
//      .authorizeRequests(authorize -> authorize 이건 대체된 된 FilterSecurityInterceptor 를 사용할 때 설정
        .authorizeHttpRequests(authorize -> authorize // SecurityFilterChain 등록
			.requestMatchers("/resources/**", "/signup", "/about").permitAll()
			.requestMatchers("/admin/**").hasRole("ADMIN")
            .requestMatchers("/**").access(new TestAuthorize())
        )
        // ...
    return http.build();
}

이렇게 등록하면 된다.

AuthorizationManager

@FunctionalInterface
public interface AuthorizationManager<T> {
	AuthorizationDecision check(Supplier<Authentication> authentication, Object secureObject);

	default AuthorizationDecision verify(Supplier<Authentication> authentication, Object secureObject)
	        throws AccessDeniedException {
		AuthorizationDecision decision = check(authentication, object);
		if (decision != null && !decision.isGranted()) {
			throw new AccessDeniedException("Access Denied");
	}
}

AuthorizationManagerAuthorization처리를 담당한다. 마치 AuthenticationManager과 비슷하다.

두가지 메서드가 있는데, verify메서드가 내부적으로 check를 호출하여 사용한다.
check는 구현체에서 반드시 구현되어야 한다. 그리고 AuthorizationDecision을 반환한다.

AuthorizationDecision은 매우 간단하다.

public class AuthorizationDecision {

	private final boolean granted;

	public AuthorizationDecision(boolean granted) {
		this.granted = granted;
	}

	public boolean isGranted() {
		return this.granted;
	}

	@Override
	public String toString() {
		return getClass().getSimpleName() + " [granted=" + this.granted + "]";
	}

}

생성자로 boolean granted를 넣어서 생성하고, isGranted로 인증 여부를 확인할 수 있다.

check 메서드를 통해 반환된 AuthorizationDecisionverify메서드가 체크해서
실패하면 AccessEdniedException을 발생시킨다.

RequestMatcherDelegatingAuthorizationManager

Authorization에는 많은 종류가 있다. 예를 들어 주소별로 다른 권한 부여가 필요하다면, 각각 이를 컨트롤하고 매칭시키기는 힘들것이다.

이를 가운데에서 중재하고 알맞은 AuthorizationManager로 연결시켜주는 것이 바로 RequestMatcherDelegatingAuthorizationManager이 존재하는 이유이다.
(AuthorizationManager의 자손이다.)

이 클래스는 Request에 알맞은 AuthorizationManager를 매칭시켜준다.

//RequestMatcherDelegatingAuthorizationManager extend AuthorizationManager

	@Override
	public AuthorizationDecision check(Supplier<Authentication> authentication, HttpServletRequest request) {
    	// for루프를 통해 하나씩 확인
		for (RequestMatcherEntry<AuthorizationManager<RequestAuthorizationContext>> mapping : this.mappings) {
			RequestMatcher matcher = mapping.getRequestMatcher();
			MatchResult matchResult = matcher.matcher(request);
            // request에 match 되는지 확인
			if (matchResult.isMatch()) {
            	//적절한 AuthorizationManager를 받음
				AuthorizationManager<RequestAuthorizationContext> manager = mapping.getEntry();
                //그 Manager의 check메서드를 실행시키고 그 반환값을 반환함.
				return manager.check(authentication,
						new RequestAuthorizationContext(request, matchResult.getVariables()));
			}
		}
		return DENY;
	}

즉 아래와 같이 설정할때 내가 커스텀으로 만든TestAuthorize()은 저 Matcher와 함께 RequestMatcherDelegatingAuthorizationManager에 저장되게 된다.

@Bean
SecurityFilterChain web(HttpSecurity http) throws AuthenticationException {
    http
//      .authorizeRequests(authorize -> authorize 이건 대체된 된 FilterSecurityInterceptor 를 사용할 때 설정
        .authorizeHttpRequests(authorize -> authorize // SecurityFilterChain 등록
			.requestMatchers("/resources/**", "/signup", "/about").permitAll()
			.requestMatchers("/admin/**").hasRole("ADMIN")
            .requestMatchers("/**").access(new TestAuthorize())
        )
        // ...
    return http.build();
}

최종모습

위의 SecurityFilterChain 설정에 hasRole과 같은 설정도 이제 각각의 Authorization을 진행하는 AuthorizationManager이다.
즉 각각의 Matchers에 AuthorizationManager를 배정해주면 RequestMatcherDelegatingAuthorizationManager가 요청에 따라 적절히 매칭해주어서 Authorization이 일어나는 것이다.

익명 사용자

아래는 내가 만든 커스텀 AuthorizationManager이다. 이걸 등록한 것이다.
무조건 false를 줌으로써 무조건 거부한다.

public class TestAuthorize implements AuthorizationManager<RequestAuthorizationContext> {

	@Override
    public AuthorizationDecision check(Supplier<Authentication> authentication, RequestAuthorizationContext object) {
    	Authentication a = authentication.get(); // 이걸 살펴보았다.
        AuthorizationDecision authorizationDecision = new AuthorizationDecision(false);
        return authorizationDecision;
    }
}

그렇다면 인증을 거치지 않은 사용자가 이 필터를 타게된다면, authentication 은 null이 될까?
정답은 그렇지 않다.
바로 annonymous 유저가 반환된다. 이는 ROLE_ANNONYMOUS 를 가진 유저로써 이에 대한 설명은 내용을 벗어나므로 자세히 설명하지는 않겠다.

끝내며

나는 처음에 JWT Token을 인증 방식에서 Authorization 에 AuthorizationManager을 쓰려고 했다.
하지만 이번 공부를 하면서 그건 좋은 방식이 아니라는 것을 느꼈다.

그 이유 첫번째는 AuthorizationManager에 올 때는 이미 Authentication을 주입 받은 상태이다. 특히 JWT 토큰은 익명 사용자 Authentiaction을 주입 받겠지만, 이는 SpringSecurity의 메커니즘에 위배된다.
즉 비정상적이다.

두번째는 이렇게 해서 그냥 isGranted만 통과하게 한다면, 추후에 Principal을 뽑는다는가 하는 전체적인 메커니즘이 깨지게 된다.

그래서 JWT같은 비세션방식 을 이용할때는 이미 AuthorizeManager에 등록하기 전에 이미 SecurityContextHolderAuthentication이 이미 존재하는 상태여야 한다.

그래야 자연스러운 흐름이 전개된다.

profile
더 좋은 구조를 고민하는 개발자 입니다

2개의 댓글

comment-user-thumbnail
2024년 2월 8일

안녕하세요.
인가 관련해서 커스텀하려고 정보를 찾다가 작성하신 글을 봤는데
머리속에 정리가 너무 잘되었습니다. 감사합니다. :)

1개의 답글