백엔드 데브코스 TIL 41일차

Inchang Choi·2022년 5월 25일
0

백엔드 데브코스 TIL

목록 보기
25/30
post-thumbnail

학습목표

강의를 들으며 내가 알고 있는 내용을 점검하고,

새로 배운 내용을 정리하며,

궁금한 내용을 알아가며 학습해나가는 것을 목표로 합니다.

Spring Security Internals

AnonymousAuthenticationFilter

해당 필터에 요청이 도달할때까지 사용자가 인증되지 않았다면, 사용자를 null 대신 Anonymous 인증 타입으로 표현합니다.

그렇게 함으로써 사용자가 null 인지 확인하는것 보다 어떤 구체적인 타입으로 확인 할 수 있도록 합니다.

ExceptionTranslationFilter

필터 체인에서 FilterSecurityInterceptor 바로 위에 위치하며, FilterSecurityInterceptor 실행 중 발생할 수 있는 예외를 잡고 처리 합니다.

💡 필터 체인 상에서 ExceptionTranslationFilter의 위치를 주의해서 볼 필요가 있다. ExceptionTranslationFilter는 필터 체인 실행 스택에서 자기 아래에 오는 필터들에서 발생하는 예외들에 대해서만 처리할 수 있다. 커스텀 필터를 추가해야 하는 경우 이 내용을 잘 기억하고, 커스텀 필터를 적당한 위치에 두어야 한다.

FilterSecurityInterceptor 실행 중 발생 가능한 AuthenticationException, AccessDeniedException 예외에 대한 처리를 담당합니다.

AuthenticationException 예외는 인증 관련 예외이며, AccessDecisionManager에 의해 접근 거부가 발생했을 때 접근 거부 페이지를 보여주거나 사용자를 로그인 페이지로 보냅니다.

  • AuthenticationEntryPoint
    • 인증되지 않은 사용자 요청을 처리할때 핵심적인 역할을 수행합니다.
    • 보통 사용자를 로그인 요청 페이지로 포워딩하는 역할을 합니다.
    • 폼 기반 로그인 인증 외의 다른 인증 매커니즘을 처리해야 할때도 AuthenticationEntryPoint를 이용할 수 있습니다.
      • 예를 들어 CAS 인증 처리가 필요하다면 CAS 포탈로 사용자를 이동시킬 수 있습니다.
      • 서드 파티 시스템과 연동이 필요한 경우 AuthenticationEntryPoint를 직접 구현할 수도 있습니다.

  • AccessDeniedException 예외에 대한 핸들러 설정이 가능합니다.
    • 기본 구현은 org.springframework.security.web.access.AccessDeniedHandlerImpl 클래스입니다.
    • HttpSecurity 클래스의 exceptionHandling() 메소드를 통해 사용자 정의 핸들러를 설정합니다.
      • 접근 거부 요청에 대한 로깅 처리
      • HTTP 403 응답 생성

암호화 방식

대칭 키 암호화

암호화와 복호화에 같은 암호 키를 쓰는 알고리즘을 의미합니다.

대칭 키 암호는 공개 키 암호와 비교하여 계산 속도가 빠르다는 장점이 있습니다.

그러나 암호화를 하는 측과 복호화를 하는 측이 같은 암호 키를 공유해야 한다는 단점이 있습니다.

대표적인 알고리즘으로 AES, DES, SEED 등이 있습니다.

RSA 암호화

Rivet, Shamir, Adelman 세사람의 첫이름을 따 RSA 라고 하며, 공개키 암호 알고리즘 (사실상 표준 공개키 암호 알고리즘)

공개 키, 비밀 키가 한 쌍으로 존재하며 공개 키는 누구나 알 수 있지만 그에 대응하는 비밀 키는 키의 소유자만이 알 수 있어야 합니다.

공개 키 암호 방식은 크게 두 가지 종류로 나눌 수 있습니다.

  • 공개 키 암호 — 특정한 비밀 키를 가지고 있는 사용자만 내용을 열어볼 수 있습니다.
  • 공개 키 서명 — 특정한 비밀 키로 만들었다는 것을 누구나 확인할 수 있습니다.

일반적으로 공개 키 암호 방식은 대칭 키 암호화보다 느립니다.

그렇기 때문에 실제 데이터를 암호화하기 위한 대칭 키를 공개 키로 암호화하고 통신 상대에게 전달하는 방식으로 많이 쓰입니다.

Spring Security Essentials

Thread Per Request 모델

Thread Per Request 모델은 톰캣과 같은 서블릿 컨테이너에서 사용해오고 있는 병렬처리 기법 중 하나 입니다.

동작하는 순서는 아래와 같습니다.

  1. WAS는 ThreadPool을 생성합니다. (Tomcat 기본값 200)
  2. HTTP 요청이 들어오면 Queue에 적재되고, ThreadPool 내의 특정 Thread가 Queue에서 요청을 가져와 처리하게 됩니다.
  3. HTTP 요청은 처음부터 끝까지 동일한 Thread에서 처리됩니다.
  4. HTTP 요청 처리가 끝나면 Thread는 다시 ThreadPool에 반납됩니다.

즉, WAS의 최대 동시 처리 HTTP 요청의 개수는 ThreadPool의 개수와 같습니다.

Thead 개수를 늘리면 동시 처리 개수는 늘어나지만, Thread Context 스위칭에 의한 오버헤드도 커지기 때문에 성능이 선형적으로 증가하지는 않습니다.


https://happyer16.tistory.com/entry/대용량-트래픽을-감당하기-위한-Spring-WebFlux-도입

최근 소개된 WebFlux 같은 기술은 Thread 개수를 작게 유지하며 HTTP 요청을 동시 처리 할 수 있도록 합니다.

최대한 적은 수의 스레드로 최대한 많은 요청을 처리하도록 합니다.
HTTP 요청은 하나 이상의 Thread에 바인딩되어 처리될 수 있습니다.

ThreadLocal 변수

Thread 범위 변수, 동일 Thread 내에서는 언제든 ThreadLocal 변수에 접근할 수 있음을 의미합니다.

// TheadLocal 변수를 생성
ThreadLocal<Integer> threadLocalValue = new ThreadLocal<>();
// ThreadLocal 변수에 값 쓰기
threadLocalValue.set(1);
// ThradLocal 변수에서 값 읽기
Integer result = threadLocalValue.get();
// ThreadLocal 변수 값 제거
threadLocal.remove();

즉, 동일 Thread내에서 실행되는 Controller, Service, Repository, 도메인 모델 어디에서든 명시적인 파라미터 전달 필요없이 ThreadLocal 변수에 접근할 수 있습니다.

ThreadPool과 함께 사용하는 경우 Thread가 ThreadPool에 반환되기 직전 ThreadLocal 변수 값을 반드시 제거 해야합니다.

그렇지 않을 경우 아래와 같은 상황이 발생하고, 미묘한 버그가 생겨날 수 있음

  1. 요청을 처리하기 위해 ThreadPool에서 Thread를 하나 가져옵니다.
  2. 요청 처리에 필요한 변수를 ThreadLocal에 set 합니다.
  3. 요청 처리가 완료되고 Thread는 ThreadPool에 반환됩니다.
  4. 다른 요청을 처리하기 위해 ThreadPool에서 Thread를 하나 가져왔는데 이전 요청 처리에 사용된 ThreadLocal 변수가 남아있고, 이를 참조하여 잘못된 동작을 수행할 수 있습니다.

SecurityContextHolder

SecurityContextHolder는 SecurityContext 데이터를 쓰거나 읽을수 있는 API를 제공합니다.

(기본 구현은 ThreadLocal를 이용하고 있습니다.)

💡 At the heart of Spring Security’s authentication model is the SecurityContextHolder. It contains the SecurityContext.

public class SecurityContextHolder {
	// ... 생략 ...
	private static SecurityContextHolderStrategy strategy;

	public static void clearContext() {
		strategy.clearContext();
	}

	public static SecurityContext getContext() {
		return strategy.getContext();
	}

	public static void setContext(SecurityContext context) {
		strategy.setContext(context);
	}
  // ... 생략 ...
}

/**
 * SecurityContextHolderStrategy 전략패턴 인터페이스
 */
public interface SecurityContextHolderStrategy {

	void clearContext();

	SecurityContext getContext();

	void setContext(SecurityContext context);

	SecurityContext createEmptyContext();

}

/**
 * SecurityContextHolderStrategy 인터페이스 ThreadLocal 구현체
 */
final class ThreadLocalSecurityContextHolderStrategy implements SecurityContextHolderStrategy {

	private static final ThreadLocal<SecurityContext> contextHolder = new ThreadLocal<>();

	@Override
	public void clearContext() {
		contextHolder.remove();
	}

	@Override
	public SecurityContext getContext() {
		SecurityContext ctx = contextHolder.get();
		if (ctx == null) {
			ctx = createEmptyContext();
			contextHolder.set(ctx);
		}
		return ctx;
	}

	@Override
	public void setContext(SecurityContext context) {
		Assert.notNull(context, "Only non-null SecurityContext instances are permitted");
		contextHolder.set(context);
	}

	@Override
	public SecurityContext createEmptyContext() {
		return new SecurityContextImpl();
	}

}

ThreadLocal을 기본 구현으로 사용한다는 것은 Thread Per Request 모델을 기본적으로 고려했음을 의미합니다. (물론 Spring Security는 Webflux를 지원하기도 합니다.)

FilterChainProxy 구현을 보면 finally 블록에서 SecurityContextHolder.clearContext() 메소드를 호출하는 확인할 수 있습니다.

이것은 HTTP 요청 처리가 완료되고 Thread가 ThreadPool에 반환되기전 ThreadLocal 변수 값을 제거하기 위합입니다.

@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
	boolean clearContext = request.getAttribute(FILTER_APPLIED) == null;
	if (!clearContext) {
		doFilterInternal(request, response, chain);
		return;
	}
	try {
		request.setAttribute(FILTER_APPLIED, Boolean.TRUE);
		doFilterInternal(request, response, chain);
	}
	catch (RequestRejectedException ex) {
		this.requestRejectedHandler.handle((HttpServletRequest) request, (HttpServletResponse) response, ex);
	}
	finally {
		SecurityContextHolder.clearContext();
		request.removeAttribute(FILTER_APPLIED);
	}
}

SecurityContext

SecurityContextHolder 클래스를 통해 코드 어느 부분(Controller, Serivce, Repository)에서든 SecurityContext에 접근할 수 있습니다.

SecurityContext context = SecurityContextHolder.getContext();
// ... 생략 ...
SecurityContextHolder.setContext(context);

SecurityContext 자체는 어떤 특별한 기능을 제공하지 않으며, 당순히 org.springframework.security.core.Authentication 객체를 Wrapping 하고 있습니다.

public interface SecurityContext extends Serializable {

	Authentication getAuthentication();

	void setAuthentication(Authentication authentication);

}

Authentication

사용자를 표현하는 인증 토큰 인터페이스이며, 인증주체를 표현하는 Principal 그리고 사용자의 권한을 의미하는 GrantedAuthority 목록을 포함합니다.

  • AnonymousAuthenticationToken 클래스는 익명 사용자를 표현하기 위한 Authentication 인터페이스 구현체입니다.
  • UsernamePasswordAuthenticationToken 클래스는 로그인 아이디/비밀번호 기반 Authentication 인터페이스 구현체입니다.
  • RememberMeAuthenticationToken 클래스는 remember-me 기반 Authentication 인터페이스 구현체입니다.

인증이 완료되거나 혹은 인증되지 사용자를 모두를 포괄적으로 표현하며, 인증 여부를 확인할 수 있습니다.

사용자의 인증 완료 여부에 따라 Principal 값이 달라집니다.

  • 로그인 전 Principal — 로그인 아이디 (String)
  • 로그인 후 Principal — org.springframework.security.core.userdetails.User 객체

https://docs.spring.io/spring-security/site/docs/current/reference/html5/#servlet-authentication-securitycontextholder

인증(Authentication)처리

인증(Authentication) — 사용자가 주장하는 본인이 맞는지 확인하는 절차를 의미합니다.

일반적으로 온라인상에서 수행되는 인증은 아이디/비밀번호를 입력하여 수행됩니다.

어플리케이션은 아이디에 해당하는 사용자를 확인하고, 입력한 비밀번호가 저장된 비밀번호와 일치하는지 확인합니다.


Spring Security 3.0 — Dmitry Noskov

DefaultLoginPageGeneratingFilter

HTTP GET 요청에 대해 디폴트 로그인 페이지를 생성해주는 필터입니다.

로그인 아이디/비밀번호/Remember-Me 파라미터명을 변경 가능합니다.

로그인 페이지 자체를 커스텀 구현 가능하며, 이 경우 해당 필터는 비활성화 됩니다.

http
	// ... 생략 ...
	.formLogin()
		.loginPage("/mylogin")
		.permitAll()
	  .and()
  // ... 생략 ...

AbstractAuthenticationProcessingFilter (a.k.a UsernamePasswordAuthenticationFilter)

사용자 인증을 처리하기 위한 필터로 가장 대표적인 것으로 UsernamePasswordAuthenticationFilter 구현체가 있습니다.

사용자 인증을 위한 정보(credentials)를 취합하고, Authentication 객체를 생성합니다.

  • UsernamePasswordAuthenticationFilter 구현에서는 로그인 아이디/비밀번호를 취합하고, Authentication 인터페이스 구현체중 하나인UsernamePasswordAuthenticationToken 객체를 생성함

인증이 완료되지 않은 Authentication 객체는 AuthenticationManager 객체로 전달됩니다.

인증이 정상적으로 완료된다면 새롭게 만들어진 Authentication 객체를 반환합니다.

여기서 새롭게 만들어진 Authentication 객체는 인증이 완료된 상태이고, GrantedAuthority 목록을 포함하고 있습니다.

@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
	if (this.postOnly && !request.getMethod().equals("POST")) {
		throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
	}
	String username = obtainUsername(request);
	username = (username != null) ? username : "";
	username = username.trim();
	String password = obtainPassword(request);
	password = (password != null) ? password : "";
	UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);
	// Allow subclasses to set the "details" property
	setDetails(request, authRequest);
	return this.getAuthenticationManager().authenticate(authRequest);
}

UsernamePasswordAuthenticationFilter 인증 흐름

AuthenticationManager, ProviderManager

AuthenticationManager 인터페이스는 사용자 인증을 위한 API를 제공합니다.

public interface AuthenticationManager {

	Authentication authenticate(Authentication authentication) throws AuthenticationException;

}

기본 구현체로 org.springframework.security.authentication.ProviderManager 클래스가 있습니다.

이 구현체는 1개 이상의 AuthenticationProvider 인터페이스 구현체로 구성됩니다.

AuthenticationProvider 인터페이스 구현체가 실제 사용자 인증을 처리하게 됩니다.

1개 이상의 AuthenticationProvider 인터페이스 구현체 중 어떤 AuthenticationProvider가 실제 인증을 처리할지 결정할 수 있습니다.

주어진 Authentication 객체에 대해 supports(Class<?> authentication) 메소드가 true 를 반환하는 AuthenticationProvider 객체가 인증을 처리합니다.

예를 들어 UsernamePasswordAuthenticationToken 타입의 인증 요청은 DaoAuthenticationProvider가 처리합니다.

profile
always positive

0개의 댓글