스프링 시큐리티가 지원하는 인증에 대해 알아보자

Belluga·2021년 8월 12일
0
post-thumbnail

인증과 인가의 차이점?

인증(Authentication) : 사용자의 신원을 확인하는 행위

인증 은 특정 리소스에 접근하려는 사용자가 누구인지 확인할 때 사용합니다.
일상 생활에 비유해보면 회사에서는 출입증 또는 생체정보를 통해 출입하려는 사람의 신원을 확인합니다.
웹에서는 보통 아이디와 패스워드를 이용하여 로그인을 통해 본인임을 증명합니다.

인가(Authorization) : 사용자의 권한을 확인하는 행위

일반적으로 사원의 권한에 따라 건물 내 접근할 수 있는 회의실, 사무실이 제한됩니다.
웹에서도 사용자에 관리자/사업자/랭킹 등 권한을 부여할 수 있습니다.

Spring Security

스프링 시큐리티는 서버에 필요한 인증 및 인가 그리고 주요 취약점 공격으로부터 방어를 지원하는 프레임워크입니다.

스프링 시큐리티는 서블릿 필터를 기반으로 동작합니다.
서블릿 필터는 서블릿이 호출되기 전 전처리를 하거나 서블릿 응답 이후 후처리 작업을 할 수 있습니다.

그러나 서블릿 컨테이너는 빈을 인식하지 못하기 때문에 스프링 빈으로 등록된 Filter 빈들을 실행할 수 없습니다.

따라서 스프링 프레임워크는 서블릿 필터의 구현체 DelegatingFilterProxy를 제공합니다. 업무를 위임하다라는 뜻을 가진 Delegate 의미처럼 직접 처리를 하는 것이 아닌 해당 필터를 등록하면 ApplicationContext에서 Filter 빈들을 찾아 실행합니다.

FilterChanProxy는 스프링 시큐리티가 제공하는 Filter로 스프링 시큐리티의 중심점이라고 할 수 있습니다. FilterChanProxy 또한 빈이기 때문에 DelegatingFilterProxy로 감싸져있습니다.

FilterChanProxy는 SecurityFilterChain을 통해 여러 Filter 인스턴스로 작업을 위임할 수 있습니다.

고유한 설정을 가진 여러개의 SecurityFilterChain을 두고 FilterCHainProxy 설정을 통해 URL마다 SecurityFilterChain을 맵핑할 수 있습니다. 특정 요청에 대해 스프링 시큐리티가 무시하길 바란다면, SecurityFilterChain에 보안 Filter를 0개 설정할 수 있습니다.

스프링 시큐리티가 지원하는 인증

스프링 시큐리티가 워낙 방대하기 때문에 스프링 시큐리티가 지원하는 인증에 대해 단편적인 부분만 정리하도록 하겠습니다.

스프링 시큐리티에서 인증을 이용하기 위해 알아야할 컴포넌트를 간략하게 살펴보도록 하겠습니다.

이름설명
SecurityContextHolder인증된 사용자가 저장되는 저장소
SecurityContextSecurityContextHolder로 접근할 수 있으며, 현재 인증한 사용자의 Authentication 을 포함한다.
Authentication사용자가 인증을 위해 제공한 자격증명 (AuthenticationManager의 입력으로 사용) 또는 현재 사용자의 증명을 제공한다.
principalAuthentication이 가지는 정보. 사용자를 식별한다. 사용자 이름/비밀번호로 인증할 땐 보통 UserDetails 인스턴스다.
credentialsAuthentication이 가지는 정보. 주로 비밀번호이며 대부분은 유출되지 않도록 사용자를 인증한 다음 비운다.
authoritiesAuthentication이 가지는 정보. 사용자에게 부여한 권한은 GrantedAuthority로 추상화한다.
GrantedAuthority부여된 권한
AuthenticationManager스프링 시큐리티의 필터가 어떻게 인증을 할지 정의한 인터페이스이다. Authentication을 SecurityContextHolder에 설정하는 것은 AuthenticationManager를 호출한 객체 (i.e. 스프링 시큐리티의 필터)가 담당한다.
ProviderManager가장 많이 사용하는 AuthenticationManager 구현체이다. ProviderManager엔 AuthenticationProvider를 여러 개 주입할 수 있으며 AuthenticationProvider 중 인증이 가능한 인터페이스를 찾는다
AuthenticationProvider인터페이스이며 AuthenticationProvider마다 담당하는 인증 유형이 다르다. 예를 들어 DaoAuthenticationProvider는 이름/비밀번호 기반 인증을, JwtAuthenticationProvider는 JWT 토큰 인증을 지원한다.
Request Credentials with AuthenticationEntryPoint클라이언트에게 인증을 요청하는 것(redirecting to a log in page, sending a WWW-Authenticate response 등)
AbstractAuthenticationProcessingFilterAuthenticationProvider 를 구현한 인증을 위한 기본 제공 필터
UserDetailsUserDetailsService가 리턴하는 값이다.

Spring Security Authentication Architecture

먼저 전체적인 스프링 시큐리티 인증 구조를 살펴보도록 하겠습니다.

[1] Http Request
사용자가 아이디 비밀번호로 로그인을 요청합니다.

[2], [3] AuthenticationFilter
AbstractAuthenticationProcessingFilter 추상 클래스는 사용자 인증을 위한 베이스 Filter입니다.

AbstractAuthenticationProcessingFilter는 인증 요청(HttpServletRequest)으로부터 Authentication을 만듭니다. 생성하는 Authentication 타입AbstractAuthenticationProcessingFilter 하위 클래스에 따라 달라집니다.

예를 들어 사용자명과 비밀번호로 이뤄진 폼기반 인증에 사용하는 UsernamePasswordAuthenticationFilter는 HttpServletRequest에 있는 username과 password로 UsernamePasswordAuthenticationToken을 생성합니다.

기본 HTTP 인증에 사용하는 BasicAuthenticationFilter는 요청 Header에서 username, password로 UsernamePasswordAuthenticationToken을 생성합니다.

생성한 AuthenticationAuthenticationManager로 넘겨서 인증합니다.

[4] AuthenticationManager
Authentication 객체를 받아 인증하고 인증되었다면 인증된 Authentication 객체를 돌려주는 메서드를 구현하도록 하는 인터페이스입니다.

실제 인증을 처리할 AuthenticationProviders에게 authentication을 전달합니다.

[5] AuthenticationProviders
실질적인 인증을 처리합니다.

AuthenticationProvider마다 담당하는 인증 유형이 다릅니다.
DaoAuthenticationProvider는 이름/비밀번호 기반 인증을, JwtAuthenticationProvider는 JWT 토큰 인증을 담당합니다.

AuthenticationProvider 목록을 순회하면서 supports() 메서드(지원할 수 있는지에 대한 여부) 호출시 TRUE를 리턴해주는 AuthenticationProvider에게 authenticate() 메서드를 실행합니다.

로그인 인증시 사용하는 주요 인터페이스는 아래와 같습니다.
(1) UserDetailsService
(2) PasswordEncoder

UserDetailsService로부터 입력받은 아이디에 대한 사용자 정보를 DB에서 조회한 결과를 받아옵니다. 받아온 사용자의 Encoded된 password와 입력받은 password를 PasswordEncoder로 Encoding한 값과 매칭하는지 확인합니다.
결과가 같으면 인증이 완료되고 인증이 완료된 Authentication 객체를 리턴해줍니다.

[6], [7], [8] UserDetailsServie, UserDetails
UserDetailsService를 통해 입력받은 username에 해당하는 정보를 DB에서 가져옵니다. 해당 아이디 유저를 찾았다면 User를 담고있는 UserDetails 객체를 만들어 반환합니다.

[9], [10]
인증이 완료되면 Authentication을 SecurityContextHolder에 담아놓습니다.

PasswordEncoder가 바뀐다면?

이전에는 비밀번호를 저장함에 있어 SHA-256과 같은 단방향 해시를 적용하는 것이 관행이었다고 합니다. 그러나 하드웨어의 발전으로 SHA-256과 같은 암호화 방식은 쉽게 해독될 수 있었고 현재 스프링 시큐리티는 BCrypt 해시함수를 제공하며 권고하고 있습니다.

스프링 시큐리티를 학습하면서 한가지 궁금증이 생겼습니다. 비밀번호를 저장하기 위한 최선의 방법은 또다시 바뀔 것인데, 이렇게 중간에 암호화 알고리즘이 바뀌게되면 기존 유저의 인증이 불가능하게되고 이는 인증 절차가 어떤 해시함수를 사용하느냐에 종속되는 것이 아닌가 하는 의문이었습니다.

스프링 시큐리티 공식 문서에서 그에 대한 답을 구할 수 있었습니다.
스프링 시큐리티는 Spring Security 5 부터 DelegatingPasswordEncoder를 도입해서 암호화 알고리즘을 변경에 유연하게 대처할 수 있도록 하였습니다.

DelegatingPasswordEncoder

PasswordEncoder passwordEncoder =
    PasswordEncoderFactories.createDelegatingPasswordEncoder();

DelegatingPasswordEncoder 인스턴스는 간단히 PasswordEncoderFactories로 만들 수 있습니다.

public static PasswordEncoder createDelegatingPasswordEncoder() {
      String encodingId = "bcrypt";
      Map<String, PasswordEncoder> encoders = new HashMap<>();
      encoders.put(encodingId, new BCryptPasswordEncoder());
      encoders.put("ldap", new LdapShaPasswordEncoder());
      encoders.put("MD4", new Md4PasswordEncoder());
      encoders.put("MD5", new MessageDigestPasswordEncoder("MD5"));
      encoders.put("noop", NoOpPasswordEncoder.getInstance());
      encoders.put("pbkdf2", new Pbkdf2PasswordEncoder());
      encoders.put("scrypt", new SCryptPasswordEncoder());
      encoders.put("SHA-1", new MessageDigestPasswordEncoder("SHA-1"));
      encoders.put("SHA-256", new MessageDigestPasswordEncoder("SHA-256"));
      encoders.put("sha256", new StandardPasswordEncoder());
 
      return new DelegatingPasswordEncoder(encodingId, encoders);
   }

딜리게이트에 여러 알고리즘을 넣어놓고 현재 쓰는 대표 알고리즘을 선정합니다. 현재는 BCrypt가 권고되고 있어서 그런지 대표가 bcrypt로 되어있네요.

DelegatingPasswordEncoder Storage Format은 아래와 같습니다.

{id}encodedPassword

여기서 id는 사용할 PasswordEncoder를 식별하는 데 사용하는 값이고, encodedPassword는 선택한 PasswordEncoder에서 사용할 인코딩된 비밀번호입니다.

예를 들어 BCrypt PasswordEncoder를 사용해 암호화한 패스워드 결과 아래와 같습니다.

{bcrypt}$2a$10$dXJ3SW6G7P50lGmMkkmwe.20cQQubK3.HZWzG3YB1tlRy.fqvM/BG

scrypt PasswordEncoder를 사용해 암호화 패스워드 결과는 아래와 같을것입니다.

{scrypt}$e0801$8bWJaSu2IKSn9Z9kM+TPXfOc/9bdYSrN1oD9qfVThWEwdRTnO7re7Ei+fUZRJ68k9lTyuTeUp4of4g24hHnazw==$OAOec05+bXxvuu/1qZ6NUR+xQYvYv7BeL1QxwRpY5P

이 방식으로 어떻게 암호화 알고리즘을 변경에 유연하게 대처할 수 있을까요?
현재 대표 알고리즘이 BCrypt로 되어있는 상황에서 DB에서 사용자 패스워드를 조회해왔더니 {sha256}1231dsafcafcsf 로 되어있다고 가정해봅시다.

딜리게이트는 무슨 알고리즘인지 확인 후 원래 요청을 bcrypt가 아닌 sha256으로 인코딩해 결과를 확인합니다.

결과가 일치하는 경우 현재 대표 알고리즘인 bcrypt로 인코딩한 결과를 DB에 덮어쓰게됩니다. 해당 방식은 사용자의 요청이 오기전까지는 DB에 이전 알고리즘으로 남아있을 것입니다.

Reference

https://docs.spring.io/spring-security/site/docs/current/reference/html5/

https://jeong-pro.tistory.com/205

0개의 댓글