개발/Java|Spring

Spring Security Authentication(인증)

달리초이 2023. 1. 18. 13:56

 

Authentication은 인증된 결과만 저장하는 게 아니라 인증을 하기 위한 정보와 인증을 받기 위한 정보가 하나의 객체에 동시에 들어있다. AuthenticationProvider가 어떤 인증에 대해서 허가를 할 것인지 판단하기 위해 직접 입력된 인증을 보고 허가된 인증을 내주는 방식이기 때문이다.

 

Authentication Mechanism

Username and Password 사용자 이름/비밀번호로 인증하는 방법
OAuth 2.0 Login 소셜 로그인. OpenID Connect 및 비표준 OAuth 2.0 로그인
SAML 2.0 Login SAML 2.0 로그인
Central Authentication Server(CAS) 중앙인증서버(CAS) 지원
Remember Me 세션이 만료된 사용자를 기억하는 방법
JAAS Authentication JAAS 인증
OpenID OpenID 인증(OpenID Connect와 혼동하지 말 것)
사전 인증 시나리오 SiteMinder 또는 Java EE security와 같은 외부 메커니즘으로 처리하면서, 스프링 시큐리티로 권한 인가와 주요 취약점 공격을 방어할 수 있다.
X509 Authentication X509 인증

 

 

Architecture Components

스프링 시큐리티의 주요 아키텍처 컴포넌트이다. 

SecurityContextHolder 스프링 시큐리티에서 인증한 대상에 대한 상세 정보가 저장된다. SecurityContext를 제공하는 static 메소드(getContext)를 지원한다.
SecurityContext SecurityContextHolder로 접근할 수 있으며, 현재 인증한 사용자의 Authentication 을 가지고 있다.
Authentication 사용자가 제공한 인증용 credential이나 SecurityContext 에 있는 현재 사용자의 credential을 제공하며,  AuthenticationManager의 입력으로 사용한다. Principal과 GrantAuthority를 제공한다. 인증이 이루어지면 Authentication이 저장된다.
Principal user에 해당하는 정보로, 사용자이름/비밀번호로 인증할 땐 보통 UserDetails를 반환한다.
credentials 주로 비밀번호로 대부분은 유출되지 않도록 사용자를 인증한 후 비운다.
GrantedAuthority ROLE_ADMIN 등 Authentication에서 접근 주체(Principal)에 부여한 권한을 말한다. prefix로 'ROLE_'이 붙는다. 인증 이후에 인가를 할 때 사용한다. 권한은 복수일 수 있으므로 Collection<GrantedAuthority> 형태로 제공한다.
AuthenticationManager 스프링 시큐리티의 필터가 인증을 어떻게 수행하는지를 정의하는 API
ProviderManager 가장 많이 사용하는 AuthenticationManager 구현체
AuthenticationProvider ProviderManager가 특정 인증 유형을 수행할 때 사용한다.
AuthenticationEntryPoint 클아이언트에 credential을 요청할 때 사용한다.(i.e. 로그인 페이지로 리다이렉트하거나 WWW-Authenticate 헤더를 전송하는 등)
AbstractAuthenticationProcessingFilter 인증에 사용할 Filter의 베이스. 필터를 잘 이해하면 여러 컴포넌트를 조합해서 심도 있는 인증 플로우를 구성할 수 있다.

 

 

SecurityContextHolder

스프링 시큐리티의 인증 모델 중심에는 SecurityContextHolder 가 있다. 여기에는 스프링 시큐리티로 인증한 사용자의 상세 정보를 저장한다. 여기에 어떻게 값을 넣는지는 상관하지 않는데, 값이 있다면 현재 인증한 사용자 정보로 사용한다.

 

@docs.spring.io

 

 

AuthenticationProvider는 여러개가 동시에 존재할 수 있고, 인증 방식에 따라 ProviderManger도 여러개 존재할 수 있다. 

 

 

- 현재 인증된 사용자 액세스

SecurityContext context = SecurityContextHolder.getContext();
Authentication authentication = context.getAuthentication();
String username = authentication.getName();
Object principal = authentication.getPrincipal();
Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();

 

스프링 시큐리티의 기본적인 SecurityContext 관리 전략은 ThreadLocal을 사용하는 ThreadLocalSecurityContextHolderStrategy 이다. 기본적으로 ThreadLocal을 사용해서 정보를 저장하기 때문에, 메소드에 직접 SecurityContext를 넘기지 않아도 동일한 쓰레드라면 항상 SecurityContext에 접근할 수 있다. 기존 principla 요청을 처리한 다음 비워주는 것만 잊지 않으면 ThreadLocal을 사용해도 안전하다. 스프링 시큐리티의 FilterChainProxy는 항상 SecurityContext를 비워준다.

 

WebMVC 패턴으로 프로젝트를 만들었다면, 대부분 요청 1개에 Thread 1개가 생성된다. 이때 ThreadLocal을 사용하면 Thread마다 고유한 공간을 만들 수 있고 거기 SecurityContext 하나를 저장할 수 있다. SecurityContext 공유 전략을 바꿀 수도 있고, 기본 설정 모드는 'SecurityContextHolder.MODE_THREADLOCAL' 이다. 이는 ThreadLocal을 사용하여 같은 쓰레드 안에서 SecurityContext를 공유한다. 이외에도 'MODE_INHERITABLETHREADLOCAL', 'MODE_GLOBAL' 모드도 있다.

 

 

AuthenticationManager

스프링 시큐리티 필터의 인증 수행 방식을 정의하는 API다. 매니저가 리턴한 Authentication을 SecurityContextHolder에 설정하는 건, AuthenticationManager를 호출한 객체가 담당한다. 스프링 시큐리티의 Filters를 사용하지 않는다면 AuthenticationManager를 사용할 필요없이 직접 SecurityContextHolder를 설정하면 된다.

 

@docs.spring.io

 

구현체는 어떤 것을 사용해도 좋지만 가장 많이 쓰는 게 ProviderManager 다.

 

직접 AuthenticationManager를 정의해서 제공하지 않으면, AuthenticationManager를 만드는 AuthenticationManagerFactoryBean에서 DaoAuthenticationProvider를 기본 인증제공자로 등록한 AuthenticationManager를 만든다.

DaoAuthenticationProvider는 반드시 1개의 UserDetailsService를 발견할 수 있어야 한다. 만약 없으면 InmemoryUserDetailsManager에 [username=user, password=(서버가 생성한 패스워드)]인 사용자가 등록되어 제공된다.

 

 

ProviderManager

동작을 AuthenticationProvider List에 위임한다. 모든 AuthenticationProvider는 인증을 성공시키거나, 실패하거나, 아니면 결정을 내릴 수 없는 것츠로 판단하고 다운스트림에 있는 AuthenticationProvider가 결정하도록 만들 수 있다. 설정해둔 AuthenticationProvider가 전부 인증하지 못하면 ProviderNotFoundException과 함께 실패한다. 이 예외는 AuthenticationException의 하위클래스로, 넘겨진 Authentication 유형을 지원하는 ProviderManager를 설정하지 않았음을 의미한다.

 

@docs.spring.io

 

기본적으로 ProviderManager는 인증에 성공하면 반환받은 Authentication 객체에 있는 모든 민감한 credential 정보를 지운다. 

 

 

AuthenticationProvider

ProviderManager엔 AuthenticationProvider를 여러개 주입할 수 있다. 각각 담당하는 인증 유형이 다르다. 예를 들면 DaoAuthenticationProvider는 이름/비밀번호 기반 인증을, JwtAuthenticationProvider는 JWT 토큰 인증을 지원한다. 이처럼 인증 유형마다 담당 AuthenticationProvider가 있기 때문에, AuthenticationManager 빈 하나만 외부로 노출하면서도 여러 인증 유형을 지원할 수 있다.

 

 

 

 

 


 

Username/Password Authentication

사용자 이름과 비밀번호 검증은 가장 많이 사용하는 방법 중 하나이다. 스프링 시큐리티는 HttpServletRequest에서 이름과 비밀번호를 읽을 수 있는 다음 메커니즘을 기본 제공한다. 이름/비밀번호 조회 메커니즘은 지원하는 저장 메커니즘 중 어떤 것과도 조합할 수 있다.

Reading the Username & Password - Form
- Basic
- Digest
Storage Mechanisms - In Memory
- JDBC
- UserDetails
- UserDetailsService
- PasswordEncoder
- DaoAuthenticationProvider
- LDAP

Form Login

스프링 시큐리티는 html 폼 기반 사용자 이름/비밀번호 인증을 지원한다.

GET /login 을 처리하고 별도의 로그엔 페이지 설정을 하지 않으면 제공되는 필터이다. 기본 로그인 폼을 제공한다.

 

@docs.spring.io

 

사용자가 권한이 없는 리소스에 인증되지 않은 요청을 보낸면 AccessDeniedException을 던져 요청이 거절되었음을 알린다. 설정한 로그인 페이지로의 리다이렉트 응답을 전송한다. 그러면 브라우저는 리다이렉트된 로그인 페이지를 요청하고 어플리케이션에서 로그인 페이지를 렌더링해야 한다.

public SecurityFilterChain filterChain(HttpSecurity http) {
	http
		.formLogin(withDefaults());
	// ...
}
// Custom Log In Form Configuration
public SecurityFilterChain filterChain(HttpSecurity http) {
	http
		.formLogin(form -> form
			.loginPage("/login")
			.permitAll()
		);
	// ...
}

 

UsernamePasswordAuthenticationFilter

Form 데이터로 username, password 기반의 인증을 담당하고 있는 필터이다.

POST /login 을 처리하고 processingUrl을 변경하면 주소를 바꿀 수 있다.

@docs.spring.io

 

LogoutFilter

HttpSecurity 빈을 주입할 때 자동으로 로그아웃 기능을 추가한다. 기본적으로 /logout URL에 접근했을 때 다음과 같이 사용자를 로그아웃 시킨다.

 

- HTTP 세션 무효화

- 관련한 모든 RememverMe 인증 제거

- SecurityContextHolder 비우기

- /login?logout 으로 리다이렉트

 

public SecurityFilterChain filterChain(HttpSecurity http) {
    http
        .logout(logout -> logout
            .logoutUrl("/my/logout")
            .logoutSuccessUrl("/my/index")
            .logoutSuccessHandler(logoutSuccessHandler)
            .invalidateHttpSession(true)
            .addLogoutHandler(logoutHandler)
            .deleteCookies(cookieNamesToClear)
        )
        ...
}

 

 

 

 

Basic Authentication

REST API 서버를 별도로 두고 뷰페이지를 분리하는 등 기본적으로 로그인 페이지를 사용할 수 없는 상황에서 사용한다.(React, Angular, Vue 등 SPA / 브라우저 기반의 모바일앱)

 

@docs.spring.io

 

사용자가 권한이 없는 리소스에 인증되지 않은 요청을 보내면 AccessDeniedException을 던져 요청이 거절되었음을 알린다. 이때 클라이언트가 요청을 재전송할 수 있으므로 RequestCache는 보통 요청을 저장하지 않는 NullRequestCache를 사용한다.

 

BasicAuthenticationFilter

username과 password를 Base64로 인코딩해서 모든 요청에 포함해서 보내면 BasicAuthenticationFilter는 이걸 인증한다. 그렇기 때문에 세션이 필요없고 요청이 올때마다 인증이 이루어 진다.(stateless) 이런 방식은 요청할 때마다 아이디와 비밀번호가 반복해서 노출되기 때문에 보안에 취약하다. 따라서 이 필터를 사용한다면 반드시 https를 사용하도록 권장된다.

 

@docs.spring.io

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) {
	http
		// ...
		.httpBasic(withDefaults());
	return http.build();
}

최초 로그인 시에만 인증을 처리하고 이후에는 session에 의존한다. 또 RememberMe를 설정한 경우 remember-me 쿠키가 브라우저에 저장되기 때문에 세션 만료 이후라도 브라우저 기반의 앱에서는 로그인 페이지를 거치지 않고 장시간 서비스를 이용할 수 있다.

 

 

 

Persisting Authentication

사용자가 보호된 리소스를 처음 요청할 때 인증 정보를 입력하라는 메시지가 표시됩니다. 자격 증명을 요구하는 가장 일반적인 방법 중 하나는 사용자를 로그인 페이지로 리디렉션하는 것입니다.

SecurityContextPersistenceFilter

@docs.spring.io

보통 두번째로 실행되는 필터이다. (첫번째 필터는 Async 요청에 대해서도 SecurityContext를 처리할 수 있도록 해주는 WebAsyncManagerIntegrationFilter이다.)

SecurityContext를 찾아와서 SecurityContextHolder에 넣어주는 역할을 한다. 만약 없다면 HttpSession에서 SecurityContext를 가져와서 새로 만들어준다. (HttpSessionSecurityContextRepository : 서버 세션에 SecurityContext를 저장하는 기본 저장소)

// Security Context 명시적 저장
public SecurityFilterChain filterChain(HttpSecurity http) {
	http
		// ...
		.securityContext((securityContext) -> securityContext
			.requireExplicitSave(true)
		);
	return http.build();
}

 

 

 

 

 

 

[참고]

Spring Security docs : https://docs.spring.io/spring-security/reference/5.7/index.html

옥탑방개발자 : https://github.com/jongwon/sp-fastcampus-spring-sec

 

 

728x90
반응형