Backend/Spring Security

Spring Security의 인가(Authorization)

olsohee 2024. 1. 22. 14:33

Spring Security의 인가

인가는 인증된 사용자가 특정 리소스에 접근이 가능한지를 결정하는 과정이다. Spring Security에서의 인가는 인증 정보를 담은 Authentication의 authorities(역할)을 통해 접근이 가능한지를 결정한다.

 

Spring Security는 인증을 위한 필터로 AuthorizationFilter를 제공한다. AuthorizationFilter를 사용하려면, 다음과 같이 authorizeHttpRequests() 메소드를 통해 등록하면 된다.

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {

    http.authorizeHttpRequests(authHttp ->
                    authHttp.requestMatchers(POST, "/join", "/login").permitAll())
                    ...
                    
    return http.build();
}

실행 흐름과 주요 모듈

  1. 먼저 AuthorizationFilter는 SecurityContextHolder에서 Authentication 객체를 얻어서 Authentication과 HttpServletRequest를 AuthorizationManager의 check() 메소드로 넘긴다.
  2. AuthorizationManager의 check() 메서드는 AuthorizationDecision을 반환한다.
  3. AuthorizationFilter는 반환받은 AuthorizationDecision을 검증하며 적절한 권한인지 확인한다.
  4. 만약 검증에 실패하면, AccessDeniedException 예외를 발생시킨다.
  5. 만약 검증에 성공하면, 적절한 권한인 것이므로 다음 FilterChain을 이어간다.

AuthorizationFilter

public class AuthorizationFilter extends GenericFilterBean {

	...

	@Override
	public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain chain)
			throws ServletException, IOException {

		HttpServletRequest request = (HttpServletRequest) servletRequest;
		HttpServletResponse response = (HttpServletResponse) servletResponse;

		if (this.observeOncePerRequest && isApplied(request)) {
			chain.doFilter(request, response);
			return;
		}

		if (skipDispatch(request)) {
			chain.doFilter(request, response);
			return;
		}

		String alreadyFilteredAttributeName = getAlreadyFilteredAttributeName();
		request.setAttribute(alreadyFilteredAttributeName, Boolean.TRUE);
		try {
			// AuthorizationManager의 check() 메소드에 인증 정보인 Authentication과 HttpServletRequest 객체를 전달
			// 이를 통해 AuthorizationDecision 객체를 얻어옴
			AuthorizationDecision decision = this.authorizationManager.check(this::getAuthentication, request);
			this.eventPublisher.publishAuthorizationEvent(this::getAuthentication, request, decision);
			
			// !decision.isGranted()이면 AccessDeniedException 예외 발생
			if (decision != null && !decision.isGranted()) {
				throw new AccessDeniedException("Access Denied");
			}
			chain.doFilter(request, response);
		}
		finally {
			request.removeAttribute(alreadyFilteredAttributeName);
		}
	}
	...
}

AuthorizationManager

AuthorizationManager 인터페이스에는 두 가지 메서드(verify(), check())가 있다. verify 메서드는 내부적으로 check 메서드를 호출하고, check 메서드는 AuthorizationDecision을 반환한다.

@FunctionalInterface
public interface AuthorizationManager<T> {

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

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

}

AuthorizationDecision

AuthorizationDecision은 생성자로 boolean granted를 넣어서 생성하고, isGranted() 메서드를 통해 인증 여부를 확인할 수 있다. 즉, AuthorizationManager의 check() 메서드를 통해 반환된 AuthorizationDecision을 verify() 메서드에서 검증하여 검증에 실패하면 AccessDeniedException 예외를 발생시킨다.

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 + "]";
	}

}

 


Reference