Develog
코드스테이츠 63일차 본문
Spring Security의 인증 처리 흐름
1. 사용자가 Username과 Password를 통해 인증을 요청한다.
2. AuthenticationFilter의 구현체인 UsernamePasswordAuthenticationFilter는 전달받은 Username과 Password를 가지고 UsernamePasswordAuthenticationToken을 생성한다.
3. 생성된 Authentication을 AuthenticationManager에 전달한다.
4. AuthenticationManager는 전달받은 Authentication을 다수의 AuthenticationProvider에 전달하여 유효성 검증 및 처리를 위임한다.
5. 입력받은 사용자의 인증 정보의 유효성 검증을 위해 UserDetailsService로 전달한다.
6. UserDetailsService는 loadUserByUsername() 메소드를 통해 사용자 정보를 조회하여 실제 존재하는 사용자인지, Username과 Password가 유효한지 검증한다.
7. 만약 6에서 사용자의 인증 정보 검증이 성공적으로 이루어졌다면 해당 사용자 정보를 활용해 UserDetails를 생성한다.
8. 생성된 UserDetails를 AuthenticationProvider에 전달한다.
9. authenticate() 메소드가 호출되며 UserDetails와 Authorities로 생성한 Authentication을 전달한다.
10. 생성된 Authentication을 AuthenticationFilter에 전달한다.
11. AuthenticationFilter는 인증 처리가 모두 완료되어 해당 사용자의 인증 정보를 담고 있는 Authentication을 SecurityContext에 저장한다.
Authentication
Spring Security에서 Authentication은 인터페이스로 존재하며 사용자가 인증을 성공적으로 수행하게 되면 사용자의 인증 정보에 관련된 정보를 가지고 있다.
대표적으로 다음과 같은 정보를 가지고 있다.
- principal - 사용자를 식별한다. 사용자의 고유 식별자와 암호로 인증이 이루어지면 일반적으로 UserDetails 인터페이스의 구현체이다. 구현체로는 org.springframework.security.core.userdetails.User 클래스가 있고 또한 직접 UserDetails 를 상속받아 구현할 수 있다.
- credentials - 암호이다. 사용자의 인증이 이루어진 후 지워진다.
- Authorities - AuthenticationManger에 의해 부여된 인가에 대한 정보이다. 부여된 권한에 대한 정보는 GrantedAuthority로 추상화한다. 일반적으로 사용하는 구현체는 SimpleGrantedAuthority이다.
일반적으로 많이 사용되는 구현체는 UsernamePasswordAuthenticationToken이다.
UsernamePasswordAuthenticationToken 은 사용자의 고유 식별자(Username)와 암호(Password)로 간단하게 Authentication객체를 생성할 수 있다.
Authentication(인증)
스프링 시큐리티는 사용자 인증 처리를 기본으로 제공한다.
- 스프링 시큐리티는 종합적인 인증(authentication) 처리를 지원한다.
- 인증은 특정 리소스에 접근하려고 하는 사용자가 누구인지를 확인할 때 사용한다.
- 보통 사용자가 이름과 비밀번호를 입력하는 것으로 사용자를 인증한다.
- 한번 인증하면 사용자를 식별하고 권한을 부여할 수 있다. (인가. authorization)
Password Storage
스프링 시큐리티의 PasswordEncoder 인터페이스는 비밀번호를 안전하게 저장할 수 있도록 단방향 변환을 수행해준다.
- PasswordEncoder는 비밀번호를 단방향으로 변환한다. (양방향 변환을 목적으로 만들지 않았다.)
- 인증에 사용할 credential 정보를 저장한다.
PasswordEncoder를 사용해서 저장하는 비밀번호는 인증 시점에 사용자가 입력하는 비밀번호와 비교하는 용도로 쓴다.
Password Storage History
몇 년 동안 암호를 저장하는 표준 메커니즘도 발전해 왔다.
1. 처음에는 일반 텍스트로 암호를 저장했습니다.
- 비밀번호를 담고 있는 데이터(DB)에 접근하려면 credential이 필요했기 때문에 비밀번호가 안전하다고 생각했다.
- 악의적인 SQL 인젝션 등의 공격으로 사용자 이름, 비밀번호 등의 “데이터 덤프”를 읽어갈 수 있는 방법이 존재한다.
- 사용자 암호를 보호하기 위해 더 많은 조치가 필요해졌다.
2. 그 다음엔 암호를 저장하기 전에 SHA-256과 같은 단방향 해시를 실행한 후 암호를 저장했다.
- 사용자가 인증을 시도할 때 입력한 암호를 해시 처리하여 비밀번호와 입력한 비밀번호의 해시값과 비교한다.
- 시스템에선 비밀번호의 단방향 해시만 저장하면 되었다.
- 기존에는 일반 텍스트와 같이 악의적으로 다른 사람의 비밀번호에 대한 정보를 읽더라도 실제 비밀번호가 아닌 비밀번호의 단방향 해시값만 알 수 있었다.
- 단방향 해시값 만으로 비밀번호를 추측하기는 불가능에 가까웠기에 시스템의 암호를 알아내기 어려웠다.
- 하지만 이런 시스템에 대응하기 위해 악의적인 사용자들은 레인보우 테이블로 알려진 룩업 테이블을 만들었다.
- ㄴ매번 비밀번호를 추측해내는 것이 아닌 비밀번호를 미리 계산해서 룩업 테이블에 저장하는 것이다.
3. 레인보우 테이블을 무력화하기 위해 솔티드 패스워드(salted password)를 사용했다.
- 단순히 비밀번호를 해시 함수 입력으로 사용하는 대신 모든 사용자의 비밀번호로 랜덤 바이트(솔트)를 만들었다.
- 솔트와 사용자의 비밀번호로 해시 함수를 실행하면 유니크한 해시값을 생성한다.
- 사용자가 인증을 시도하면 해시처리한 비밀번호를 저장된 솔트와 사용자가 입력한 비밀번호의 해시값과 비교한다.
- ㄴ솔트는 유니크하기 때문에 솔트와 비밀번호 조합은 절대 동일할 수 없으며 레인보우 테이블 무력화할 수 있었다.
- 하지만 기술이 발전함에 따라 최신 하드웨어를 사용하면 초당 수십억 건을 계산할 수 있게 되며 비밀번호를 쉽게 해독할 수 있어 SHA-256과 같은 암호화 해시가 더 이상 안전하지 않게 되었다.
4. 현재는 적응형 단방향 함수(adaptive one-way function)로 비밀번호를 저장하는 것이 추천된다.
- CPU, 메모리 등의 많은 리소스를 소모해서 비밀번호를 검증합니다.
- 적응형 단방향 함수는 하드웨어 사양에 따라 워크 팩터(work factor)를 구성할 수 있습니다.
- ㄴ워크 팩터란 작업 요소란 뜻으로, 시스템에서 비밀번호를 검증할 때 소요되는 시간과 관련되어 있습니다.
- 위와 같은 방식은 공격자가 쉽게 비밀번호를 해독하지 못하게 만들지만 시스템 자체에 부담이 됩니다.
- 스프링 시큐리티는 워크 팩터의 시작점을 제공하지만 성능에 따라 각자의 시스템에 맞게 설정하는 게 좋습니다.
- ㄴ종류 : bcrypt , PBKDF2 , scrypt, argon2 등이 있습니다.
- 내부적으로 리소스를 많이 소모하기 때문에 매 요청마다 사용자 이름과 비밀번호를 검증하면 애플리케이션 성능이 크게 떨어질 수 있습니다.
- 보안을 적용해서 검증하는 것 자체가 리소스를 소모하는 일이기 때문에 스프링 시큐리티가 비밀번호 검증 속도를 끌어올릴 방법은 없습니다.
- 사용하는 부분에서 장기 자격 증명(사용자 이름 및 암호), 단기 자격 증명(세션, OAuth 토큰 등)으로 바꾸는게 좋습니다.
- ㄴ단기 자격 증명은 동일한 보안 수준을 유지하면서도 빨리 검증할 수 있습니다.
Servlet Authentication Architecture
Architecture
서블릿 인증에서 사용하는 스프링 시큐리티의 주요 아키텍처 컴포넌트
SecurityContextHolder
- 스프링 시큐리티에서 인증한 대상에 대한 상세 정보는 SecurityContextHolder에 저장한다.
SecurityContext
- SecurityContextHolder 로 접근할 수 있으며 인증한 사용자의 Authentication을 가진다.
Authentication
- 사용자가 인증을 위해 제공한 자격 증명(credential)이나 SecurityContext에 있는 현재 사용자의 자격 증명(credential)을 제공하며 AuthenticationManager의 입력으로 사용한다.
GrantedAuthority
- Authentication에서 접근 주체(principal)에 부여한 권한
AuthenticationManager
- 스프링 시큐리티의 필터가 인증을 어떻게 수행하는지에 대한 방법을 정의하는 API
ProviderManager
- 가장 많이 사용하는 AuthenticationManager 구현체
AuthenticationProvider
- ProviderManager가 특정 인증 유형을 수행할 때 사용한다.
Request Credentials with AuthenticationEntryPoint
- 클라이언트에 credential을 요청할 때 사용한다.
AbstractAuthenticationProcessingFilter
- 인증에 사용할 Filter의 베이스
- 여러 컴포넌트를 조합해서 심도 있는 인증 플로우를 구성할 수 있다.
SecurityContextHolder
스프링 시큐리티의 인증 모델의 중심이며 SecurityContext를 가지고 있다.
스프링 시큐리티로 인증한 사용자의 상세 정보를 저장한다.
스프링 시큐리티는 SecurityContextHolder에 값을 어떻게 넣을지 신경 쓰지 않고, 값이 있을 때 현재 인증한 사용자 정보로 사용한다.
사용자 인증 처리의 가장 쉬운 방법은 직접 SecurityContextHolder를 설정하는 것이다.
SecurityContext context = SecurityContextHolder.createEmptyContext(); // (1)
Authentication authentication =
new TestingAuthenticationToken("username", "password", "ROLE_USER"); // (2)
context.setAuthentication(authentication);
SecurityContextHolder.setContext(context); // (3)
- 빈 SecurityContext를 만들어 시작한다.
- 새 Authentication 객체를 생성하고 Authentication 구현체는 모두 사용할 수 있다.
프로덕션 환경에선 TestingAuthenticationToken 대신UsernamePasswordAuthenticationToken(userDetails, password, authorities)를 주로 사용한다. - SecurityContextHolder에 SecurityContext를 설정해주고 스프링 시큐리티는 이 정보를 사용해 권한을 부여(인가)한다.
SecurityContext
SecurityContextHolder로 접근하는데 사용된다.
Authentication 객체를 가지고 있다.
Authentication
AuthenticationManager의 입력으로 사용되어 인증에 사용할 사용자의 credential을 제공한다.
Authentication이 가지고 있는 정보들
1. principal - 사용자를 식별하는 정보
- 사용자 이름/비밀번호로 인증할 땐 보통 UserDetails 인스턴스를 이용한다.
2. credentials - 일반적으로 비밀번호를 사용한다. (유출되지 않도록 인증 후 비웁니다.)
3. authorities - 사용자에게 부여한 권한은 grantedAuthority로 추상화한다.
GrantedAuthority
사용자에게 부여한 권한을 추상화한다.
role과 scope가 있다.
- Authentication.getAuthorities() 메서드로 접근할 수 있다.
- ㄴGrantedAuthority 객체의 Collection을 리턴한다.
- 인증한 주체에게 부여된 권한을 뜻한다.
- 권한은 보통 역할(role)을 의미하고 웹 인가, 메서드 인가, 도메인 객체 인가 등에서 사용한다.
- 이름/비밀번호 기반 인증을 사용한다면 UserDetailsService가 GrantedAuthority를 로드한다.
- GrantedAuthority 객체는 애플리케이션 전체에 걸친 권한을 의미한다.
AuthenticationManager
스프링 시큐리티 필터의 인증 수행 방식을 정의하는 API이다.
AuthenticationManager를 호출한 객체(스프링 시큐리티의 필터)가 리턴한 Authentication을 SecurityContextHolder에 설정한다.
- 스프링 시큐리티의 Filters를 사용하지 않을 경우엔 AuthenticationManager를 사용하지 않고 SecurityContextHolder에 직접 설정하면 된다.
AuthenticationManager 가장 많이 사용하는 구현체는 ProviderManager이다.
ProviderManager
AuthenticationManager 구현체이다.
ProviderManager는 동작을 AuthenticationProvider List에 위임한다.
- 모든 AuthenticationProvider는 인증을 성공 or 실패 or 결정을 내릴 수 없는 것으로 판단하여 다운스트림에 있는 AuthenticationProvider가 결정할 수 있도록 만들 수 있다.
- 설정해둔 AuthenticationProvider가 전부 인증에 실패하면 ProviderNotFoundException 예외 발생과 함께 실패한다.
- AuthenticationProvider 마다 각자 맡은 인증을 수행한다.
- ProviderManager에 인증을 수행 할 수 있는 AuthenticationProvider가 없을 때 사용할 부모 AuthenticationManager를 설정할 수 있다.
- 여러 ProviderManager 인스턴스에 동일한 부모를 공유하는 것도 가능하다.
- 인증 매커니즘이 다른 SecurityFilterChain 여러 개가 공통 인증을 사용하는 경우에 흔히 쓰는 패턴이다.
- ProviederManager는 인증에 성공하면 반환받은 Authencation 객체에 있는 민감한 credential 정보를 지우고 비밀번호 같은 정보를 HttpSession에 길게 유지하지 않는다.
- ㄴcredential 정보를 지우게 되면 사용자 객체를 캐싱할 때 문제가 발생하게 된다.
AuthenticationProvider
AuthenticationProvier는 각 구현체의 authenticate() 메서드에서 정의된 특정 유형에 따른 인증을 수행한다.
ProviderManager에 AuthenticationProvider를 여러 개 주입할 수 있다.
AuthenticationProvider 마다 담당하는 인증 유형이 다르다.
ex)
- DaoAuthenticationProvider는 이름/비밀번호 기반 인증 지원합니다. (기본으로 적용되는 구현체이다)
- ㄴDaoAuthenticationProvider는 추상메소드인 retriveUser() 메소드를 구현한 클래스이고, retriveUser() 메소드에서는 클라이언트로부터 전달받은 username을 메소드 파라미터로 받아서 UserDetailsService 인터페이스의 loadUserByUsername() 메소드를 호출한다.
- ㄴ성공적으로 사용자의 정보가 조회되면 createSuccessAuthentication() 메소드에서 Authentication 객체인 UsernamePasswordAuthenticationToken 를 생성해서 반환한다.
- ㄴUserDetailsService 는 Spring Security에서 일반적으로 loadUserByUsername() 메소드에서 애플리케이션의 데이터베이스로부터 사용자의 정보에 해당하는 레코드를 조회하여 UserDetails 인터페이스로 반환하는 역할을 수행하는 인터페이스이다.
- JwtAuthenticationProvider는 JWT 토큰 인증을 지원한다.
SecurityContextPersistenceFilter
SecurityContext는 객체의 생성, 저장, 조회 등의 LifeCycle을 담당하는 Filter이고, SecurityContext를 Persist 하기 위한 구현체인 SecurityContextRepository를 가지고 있다.
- SecurityContextPersistenceFilter 클래스는 SecurityContext 객체를 영속화하는 역할을 수행하는 Filter 입니다.
- 인증 처리 매커니즘 이전에 실행되어야 하기 때문에 Security Filter들 중에서도 2번째로 실행되는 Filter입니다.
- SecurityContextPersistenceFilter는 SecurityContextRepository 라는 SecurityContext 저장소 객체를 보유하고 있습니다.
- ㄴ해당 저장소 객체를 통해 SecurityContext를 영속화하고 기존에 저장된 SecurityContext 객체를 꺼내어 SecurityContextHolder에 저장하는 등의 역할을 수행한다.
처리 순서
1. 클라이언트의 요청이 발생한다.
2. SecurityContextRepository 인터페이스의 loadContext() 메소드로 저장된 SecurityContext 객체를 가져온다.
- 기본으로 사용되는 저장소는 HttpSessionSecurityRepository 이며 HttpSession에 SecurityContext를 저장한다.
3. 해당 클라이언트의 요청이 모두 수행되고 난 후 기존에 저장했던 SecurityContextHolder 의 clearContext() 로 SecurityContext를 제거한다.
4. SecurityContextRepository 에 해당 SecurityContext 를 다시 저장한다.
SecurityContext 객체의 생성, 저장, 조회
익명 사용자
- 새로운 SecurityContext 객체를 생성하여 SecurityContextHolder에 저장한다.
- AnonymouseAuthenticationFilter에서 AnonymousAuthenticationToken 객체를 SecurityContext에 저장한다.
인증 시
- 새로운 SecurityContext객체를 생성하여 SecurityContextHolder에 저장한다.
- UsernamePasswordAuthenticationFilter에서 인증 성공 후 SecurityContext 에 UsernamePasswordAuthentication 객체를 SecurityContext에 저장한다.
- 인증이 최종 완료되면 Session 에 SecurityContext를 저장한다.
인증 후
- Session에서 SecurityContext 꺼내어 SecurityContextHolder에서 저장한다.
- SecurityContext안에 Authentication 객체가 존재하면 계속 인증을 유지한다.
최종 응답 시 공통
- SecurityContextHolder안의 SecurityContext객체에서 보관하던 인증정보를 반드시 초기화 해줘야한다.
- SecurityContextHolder.clearContext() 메서드를 호출해 인증 정보를 초기화한다.
SecurityContextRepository란?
SecurityContext를 Persist하기 위한 구현체이다.
- 일반적으로 사용되는 구현체는 HttpSessionSecurityContextRepository가 있다.
'코드스테이츠' 카테고리의 다른 글
코드스테이츠 65-66일차 (0) | 2022.07.27 |
---|---|
코드스테이츠 64일차 (0) | 2022.07.26 |
코드스테이츠 60-61일차 (0) | 2022.07.21 |
코드스테이츠 59일차 (0) | 2022.07.19 |
코드스테이츠 58일차 (0) | 2022.07.18 |