2023.07.11 코드스테이츠 64회차. ( Filter와 FilterChain, Spring Security 인증 구성요소 이해 )
Filter
- 서블릿 필터는 서블릿 기반 애플리케이션의 엔드포인트에 요청이 도발하기 전에 중간에서 요청을 가로챈 후 어떤 처리를 할 수 있도록 해주는 Java의 컴포넌트 이다
- 클라이언트가 서버 측 애플리케이션으로 요청을 전송하면 제일 먼저 servet filer를 거치게 된다
- filter에서 처리가 모두 완료 되면 DispatcherServlet에서 클라이언트의 요청을 핸들러에 매핑하기 위한 다음 작업을 진행한다
Filter Chain
- 여러개의 Filter가 체인을 형성하고 있는 Filter의 묶음을 의미한다
Filter와 Filter Chain의 특성
- Servlet FilterChain은 요청 URI path를 기반으로 HttpServletRequest를 처리합니다. 따라서 클라이언트가 서버 측 애플리케이션에 요청을 전송하면 서블릿 컨테이너는 요청 URI의 경로를 기반으로 어떤 Filter와 어떤 Servlet을 매핑할지 결정합니다.
- Filter는 Filter Chain 안에서 순서를 지정할 수 있으며 지정한 순서에 따라서 동작하게 할 수 있습니다.
- Filter Chain에서 Filter의 순서는 매우 중요하며 Spring Boot에서 여러 개의 Filter를 등록하고 순서를 지정하기 위해서는 다음과 같은 두 가지 방법을 적용할 수 있습니다.
1. Spring Bean으로 등록되는 Filter에 @Order 애너테이션을 추가하거나 orderd 인터페이서를 구현해서 Filter의 순서를 지정할 수 있다
2. FilterRegisrtationBean을 이용해 Filter의 순서를 명시적으로 지정할 수 있다
Filter 인터페이스
- (1)의 init() 메서드에서는 생성한 Filter에 대한 초기화 작업을 진행할 수 있다
- (2)의 doFilter() 메서드에서는 해당 Filter가 처리하는 실질적인 로직을 구현한다
= (2-1)에는 request를 이용해 (2-2)의 chain.doFilter(request, response)가 호출되기 전에 할 수 있는 전처리 작업에 대한 코드를 구현할 수 있다
= (2-3)에는 response를 이용해 (2-2)의 chain.doFilter(request, response)가 호출된 이후에 할 수 있는 후처리 작업에 대한 코드를 구현할 수 있다
- (3)의 destroy() 메서드는 Filter가 컨테이너에서 종료될 때 호출되는데 주로 Filter가 사용한 자원을 반납하는 처리 등의 로직을 작성하고자 할 때 사용된다
Filter 실습
- registrationBean.setOrder() 메서드로 순서를 지정할 수 있다
- registrationBean.setOrder()의 파라미터로 지정한 숫자가 적은 숫자일수록 먼저 실행 된다
########################
- Spring Boot에서는 FilterRegistrationBean을 이용해 Filter를 등록할 수 있다.
- Spring Boot에서 등록하는 Filter는 다음과 같은 방법으로 실행 순서를 지정할 수 있다.
- Spring Bean으로 등록되는 Filter에 @Order 애너테이션을 추가하거나 Orderd 인터페이스를 구현해서 Filter의 순서를 지정할 수 있다.
- FilterRegistrationBean의 setOrder() 메서드를 이용해 Filter의 순서를 지정할 수 있다.
#########################
DelegatingPasswordEncoder
- DelegatingPasswordEncoder는 Spring Security에서 지원하는 PasswordEncoder 구현 객체를 생성해 주는 컴포넌트이다
- DelegatingPasswordEncoder를 통해 애플리케이션에서 사용할 PasswordEncoder를 결정하고, 결정된 PasswordEncoder로 사용자가 입력한 패스워드를 단방향으로 암호화해 준다
DelegatingPasswordEncoder 도입 전 문제점
- 스프링 시큐리티 5.0 이전 버전에서는 평문 텍스트(Plain text) 패스워드를 그대로 사용하는 NoOpPasswordEncoder 가 디폴트 PasswordEncoder로 고정이 되어 있었지만 아래와 같은 문제를 해결하기 위해 DelegatingPasswordEncoder를 도입해서 조금 더 유연한 구조로 PasswordEncoder를 사용할 수 있게 되었다
- 패스워드 인코딩 방식을 마이그레이션 하기 쉽지 않은 오래된 방식을 사용하고 있는 경우
- 패스워드 단방향 암호화에 사용되는 hash 알고리즘은 시간이 지나면서 보다 더 안전한 hash 알고리즘이 지속적으로 고안되고 있기 때문에 항상 고정된 암호화 방식을 사용하는 것은 바람직한 사용 방식이 아닙니다.
- 보안에 취약한 오래된 방식의 암호화 방식을 고수하는 애플리케이션은 해커의 아주 좋은 타깃이 될 수 있으니까요.
- 스프링 시큐리티는 프레임워크이기 때문에 하위 호환성을 보장하지 않는 업데이트를 자주 할 수 없습니다.
- 오래된 하위 버전의 기술이 언젠가 Deprecated 되는 것처럼 보안에 취약한 오래된 방식의 암호화 알고리즘 역시 언제까지 관리 대상이 되지는 않습니다.
DelegatingPasswordEncoder의 장점
- DelegatingPasswordEncoder를 사용해 다양한 방식의 암호화 알고리즘을 적용할 수 있는데, 우리가 사용하고자 하는 암호화 알고리즘을 특별히 지정하지 않는다면 Spring Security에서 권장하는 최신 암호화 알고리즘을 사용하여 패스워드를 암호화할 수 있도록 해줍니다.
- 패스워드 검증에 있어서도 레거시 방식의 암호화 알고리즘으로 암호화된 패스워드의 검증을 지원합니다.
- Delegating이라는 표현에서도 DelegatingPasswordEncoder의 특징이 잘 드러나듯이 나중에 암호화 방식을 변경하고 싶다면 언제든지 암호화 방식을 변경할 수 있습니다.
- 단 이 경우, 기존에 암호화되어 저장된 패스워드에 대한 마이그레이션 작업이 진행되어야 합니다.
DelegatingPasswordEncoder를 이용한 PasswordEncoder 생성
PasswordEncoder passwordEncoder = PasswordEncoderFactories.createDelegatingPasswordEncoder();
- PasswordEncoderFactories.createDelegatingPasswordEncoder();를 통해 DelegatingPasswordEncoder의 객체를 생성하고, 내부적으로 DelegatingPasswordEncoder가 다시 적절한 PasswordEncoder 객체를 생성한다
Custom DelegatingPasswordEncoder 생성
- Spring Security에서 지원하는 PasswordEncoderFactories 클래스를 이용하면 기본적으로 Spring Security에서 권장하는 PasswordEncoder를 사용할 수 있지만 필요한 경우, DelegatingPasswordEncoder로 직접 PasswordEncoder를 지정해서 Custom DelegatingPasswordEncoder를 사용할 수 있다
String idForEncode = "bcrypt";
Map encoders = new HashMap<>();
encoders.put(idForEncode, new BCryptPasswordEncoder());
encoders.put("noop", NoOpPasswordEncoder.getInstance());
encoders.put("pbkdf2", new Pbkdf2PasswordEncoder());
encoders.put("scrypt", new SCryptPasswordEncoder());
encoders.put("sha256", new StandardPasswordEncoder());
PasswordEncoder passwordEncoder = new DelegatingPasswordEncoder(idForEncode, encoders);
- Map encoders에 원하는 유형의 PasswordEncoder를 추가해서 DelegatingPasswordEncoder의 생성자로 넘겨주면 디폴트로 지정(idForEncode)한 PasswordEncoder를 사용할 수 있다
암호화된 Password Format
- Spring Security 5에서는 패스워드를 암호화할 때, 암호화 알고리즘 유형을 prefix로 추가한다
- {id}encodedPassword
( 암호화된 패스워드의 포맷 )
- 아래의 코드는 위의 Custom DelegatingPasswordEncoder 에서 생성한 Custom DelegatingPasswordEncoder에서 지원하는 단방향 암호화 알고리즘 유형에 따른 암호화된 패스워드의 예 이다
- BCryptPasswordEncoder로 암호화할 경우,
- {bcrypt}$2a$10$dXJ3SW6G7P50lGmMkkmwe.20cQQubK3.HZWzG3YB1tlRy.fqvM/BG
- PasswordEncoder id는 “bcrypt”
- encodedPassword는$2a$10$dXJ3SW6G7P50lGmMkkmwe.20cQQubK3.HZWzG3YB1tlRy.fqvM/BG” 이다.
- {bcrypt}$2a$10$dXJ3SW6G7P50lGmMkkmwe.20cQQubK3.HZWzG3YB1tlRy.fqvM/BG
- Pbkdf2PasswordEncoder로 암호화할 경우,
- {pbkdf2}5d923b44a6d129f3ddf3e3c8d29412723dcbde72445e8ef6bf3b508fbf17fa4ed4d6b99ca763d8dc
- PasswordEncoder id는 “pbkdf2”
- encodedPassword는 “5d923b44a6d129f3ddf3e3c8d29412723dcbde72445e8ef6bf3b508fbf17fa4ed4d6b99ca763d8dc”이다.
- {pbkdf2}5d923b44a6d129f3ddf3e3c8d29412723dcbde72445e8ef6bf3b508fbf17fa4ed4d6b99ca763d8dc
- SCryptPasswordEncoder로 암호화할 경우,
- {scrypt}$e0801$8bWJaSu2IKSn9Z9kM+TPXfOc/9bdYSrN1oD9qfVThWEwdRTnO7re7Ei+fUZRJ68k9lTyuTeUp4of4g24hHnazw==$OAOec05+bXxvuu/1qZ6NUR+xQYvYv7BeL1QxwRpY5Pc=
- PasswordEncoder id는 “scrypt”
- encodedPassword는 “$e0801$8bWJaSu2IKSn9Z9kM+TPXfOc/9bdYSrN1oD9qfVThWEwdRTnO7re7Ei+fUZRJ68k9lTyuTeUp4of4g24hHnazw==$OAOec05+bXxvuu/1qZ6NUR+xQYvYv7BeL1QxwRpY5Pc=”이다.
- {scrypt}$e0801$8bWJaSu2IKSn9Z9kM+TPXfOc/9bdYSrN1oD9qfVThWEwdRTnO7re7Ei+fUZRJ68k9lTyuTeUp4of4g24hHnazw==$OAOec05+bXxvuu/1qZ6NUR+xQYvYv7BeL1QxwRpY5Pc=
- StandardPasswordEncoder로 암호화할 경우,
- {sha256}97cde38028ad898ebc02e690819fa220e88c62e0699403e94fff291cfffaf8410849f27605abcbc0
- PasswordEncoder id는 “sha256”
- encodedPassword는 “97cde38028ad898ebc02e690819fa220e88c62e0699403e94fff291cfffaf8410849f27605abcbc0”이다.
- {sha256}97cde38028ad898ebc02e690819fa220e88c62e0699403e94fff291cfffaf8410849f27605abcbc0
패스워드 해킹 공격에 따라 성장하는 패스워드 암호화(Password Encryption) 기술
1. Plain Text 저장
- Plain Text는 말 그대로 암호화되지 않은 텍스트 그 자체를 의미한다
2. 해시(Hash) 알고리즘
- 해시 알고리즘은 단방향 암호화를 위한 핵심 알고리즘이다
- 단방향 암호화라는 용어에서도 그 특성이 잘 드러나듯이 한번 암호화되면 복호화되기 어려운 특성을 가지고 있다
- 데이터베이스에 암호화되어 저장되는 패스워드 자체는 사용자가 입력한 패스워드와 비교해 올바른 패스워드를 입력했는지 검증하는 용도이기 때문에 다시 복호화될 필요가 없다
3. MD5(Message Digest 5)
- MD5 역시 단방향 알고리즘인데도 불구하고 복호화가 된 사례가 종종 발견되어 지금은 거의 사용하지 않는 알고리즘이다
- 다이제스트(Digest)는 원본 메시지를 암호화한 메시지를 의미한다
( 암호화 기술에 굉장히 자주 사용되는 용어 )
4. SHA(Secure Hash Algorithm)
- MD5의 결함을 보완하기 위해서 나온 대표적인 해시 알고리즘이 바로 SHA 알고리즘 이다
- SHA 알고리즘은 해시된 문자열을 만들어내기 위해 비트 회전 연산이 추가된 방식이다
( 해시된 문자열의 비트 값을 회전하면서 반복적으로 해시 처리를 하는 것 )
- 해커 입장에서는 사용자가 패스워드로 사용할만한 문자열들을 미리 목록(Rainbow Table)으로 만들어 놓고, 이 목록에 있는 문자열을 동일한 알고리즘으로 암호화한 후, 탈취한 암호화된 문자열과 서로 비교하는 작업을 통해 패스워드의 원본 문자열을 알 수 있게 되는데, 이러한 공격을 Rainbow Attack이라고 한다
5. Rainbow Attack에 대한 대응책
- Rainbow Attack을 백 퍼센트 무력화할 순 없겠지만 컴퓨터가 다이제스트(Digest)를 비교하는 작업의 횟수를 줄일 방법은 존재 한다
- 가장 단순한 방법은 앞에서 살펴본 SHA 알고리즘처럼 해시된 다이제스트를 또 해시하고, 또 해시된 다이제스트를 반복적으로 해시하는 것이다
( 이를 키 스트레칭 이라고 한다 )
- 또 한 가지 방법은 솔트(Salt)를 이용하는 방법이다
솔트(Salt)란 패스워드로 입력하는 원본 메시지에 임의의 어떤 문자열을 추가해서 해시 처리하는 것을 의미한다
솔트(Salt)를 추가하면 Rainbow Table을 이용해 비교해야 하는 경우의 수가 늘어나기 때문에 완벽하지는 않지만 Rainbow Attack에 대응할 수 있다
6. Work Factor를 추가한 Hash 알고리즘
- 해시 알고리즘을 연구하는 사람들의 고민 중 하나는 공격자가 Rainbow Attack과 같은 공격을 통해 해시된 메시지를 알아내려고 시도하더라도 어떻게 하면 최대한 느리게 최대한 비용이 많이 들게 할 수 있을까 인데 여기서 탄생한 Hash 알고리즘이 PBKDF2, bcrypt, scrypt 이다
- Work Factor는 공격자가 해시된 메시지를 알아내는 데 더 느리게 더 비용이 많이 들게 해주는 특정 요소를 의미한다
- PBKDF2나 bcrypt의 경우 Work Factor로 솔트와 키 스트레칭을 기본적으로 사용하지만 내부적으로 훨씬 복잡한 알고리즘을 이용해서 공격자의 공격을 느리게 만든다
- scrypt는 기본적으로 다이제스트 생성 시, 메모리 오버헤드를 갖도록 설계되어 있기 때문에 무차별 대입 공격(Brute Force Attack)을 시도하기 위해 병렬화 처리가 매우 어려운 특징이 있다
###############################
- 스프링 시큐리티 5.0 이전 버전부터 DelegatingPasswordEncoder를 도입해 조금 더 유연한 구조로 PasswordEncoder를 사용할 수 있게 되었다.
- DelegatingPasswordEncoder의 장점
- 사용하고자 하는 암호화 알고리즘을 특별히 지정하지 않는다면 Spring Security에서 권장하는 최신 암호화 알고리즘을 사용하여 패스워드를 암호화할 수 있도록 해준다.
- 패스워드 검증에 있어서 레거시 방식의 암호화 알고리즘으로 암호화된 패스워드의 검증을 지원한다.
- 암호화 방식을 변경하고 싶다면 언제든지 암호화 방식을 변경할 수 있다.
- 단 이 경우, 기존에 암호화되어 저장된 패스워드에 대한 마이그레이션 작업이 진행되어야 한다.
###############################################
Spring Security의 인증 처리 흐름
- Spring Security Filter Chain에 사용자의 인증 요청이 전달되었다면 그다음의 처리는?
Spring Security의 컴포넌트로 보는 인증(Authentication) 처리 흐름
- (1)에서 사용자가 로그인 폼 등을 이용해 Username(로그인 ID)과 Password를 포함한 request를 Spring Security가 적용된 애플리케이션에 전송한다
( 사용자의 로그인 요청이 Spring Security의 Filter Chain까지 들어오면 여러 Filter들 중에서 UsernamePasswordAuthenticationFilter가 해당 요청을 전달받는다 )
- 사용자의 로그인 요청을 전달받은 UsernamePasswordAuthenticationFilter는 Username과 Password를 이용해 (2)와 같이 UsernamePasswordAuthenticationToken을 생성한다
( UsernamePasswordAuthenticationToken은 Authentication 인터페이스를 구현한 구현 클래스이며, 여기에서 Authentication은 아직 인증이 되지 않은 Authentication이다 )
- 아직 인증되지 않은 Authentication을 가지고 있는 UsernamePasswordAuthenticationFilter는 (3)과 같이 해당 Authentication을 AuthenticationManager에게 전달한다
( AuthenticationManager는 인증 처리를 총괄하는 매니저 역할을 하는 인터페이스이고, AuthenticationManager를 구현한 구현 클래스가 바로 ProviderManager 이다 )
## ProviderManager가 인증이라는 작업을 총괄하는 실질적인 매니저인 것이다.
( 현실 세계에서의 매니저처럼 Spring Security의 ProviderManager 역시 직접 인증을 처리하는 것이 아니라 인증을 처리할 누군가를 찾은 뒤, 인증 처리를 대신 맡긴다 < 그 누군가가 바로 AuthenticationProvider > )
- (4)와 같이 ProviderManager로부터 Authentication을 전달받은 AuthenticationProvider는 (5)와 같이 UserDetailsService를 이용해 UserDetails를 조회한다
( UserDetails는 데이터베이스 등의 저장소에 저장된 사용자의 Username과 사용자의 자격을 증명해 주는 크리덴셜(Credential)인 Password, 그리고 사용자의 권한 정보를 포함하고 있는 컴포넌트이다 ) < UserDetails를 제공하는 컴포넌트가 바로 UserDetailsService이다 >
- UserDetailsService는 (5)에서 처럼 데이터베이스 등의 저장소에서 사용자의 크리덴셜(Credential)을 포함한 사용자의 정보를 조회한다
- 데이터베이스 등의 저장소에서 조회한 사용자의 크리덴셜(Credential)을 포함한 사용자의 정보를 기반으로 (7)과 같이 UserDetails를 생성한 후, 생성된 UserDetails를 다시 AuthenticationProvider에게 전달한다 (8).
- UserDetails를 전달받은 AuthenticationProvider는 PasswordEncoder를 이용해 UserDetails에 포함된 암호화된 Password와 인증을 위한 Authentication안에 포함된 Password가 일치하는지 검증한다
- 검증에 성공하면 UserDetails를 이용해 인증된 Authentication을 생성한다(9).
- 만약 검증에 성공하지 못하면 Exception을 발생시키고 인증 처리를 중단한다
- AuthenticationProvider는 인증된 Authentication을 ProviderManager에게 전달한다 (10).
- (2)에서의 Authentication은 인증을 위해 필요한 사용자의 로그인 정보를 가지고 있지만, ⭐ 이 단계에서 ProviderManager에게 전달한 Authentication은 인증에 성공한 사용자의 정보(Principal, Credential, GrantedAuthorities)를 가지고 있다
- ProviderManager는 (11)과 같이 인증된 Authentication을 다시 UsernamePasswordAuthenticationFilter에게 전달한다
- 인증된 Authentication을 전달받은 UsernamePasswordAuthenticationFilter는 마지막으로 (12)와 같이 SecurityContextHolder를 이용해 SecurityContext에 인증된 Authentication을 저장한다
( SecurityContext는 이후에 Spring Security의 세션 정책에 따라서 HttpSession에 저장되어 사용자의 인증 상태를 유지하기도 하고, HttpSession을 생성하지 않고 무상태를 유지하기도 한다 )
############################
- 사용자의 로그인 요청을 처리하는 Spring Security Filter는 UsernamePasswordAuthenticationFilter이다.
- UsernamePasswordAuthenticationToken은 Authentication 인터페이스를 구현한 구현 클래스이며, 여기에서 Authentication은 ⭐ 아직 인증이 되지 않은 Authentication을 의미한다.
- AuthenticationManager는 인증 처리를 총괄하는 매니저 역할을 하는 인터페이스이고, AuthenticationManager를 구현한 구현 클래스가 ProviderManager이다.
- UserDetails는 데이터베이스 등의 저장소에 저장된 사용자의 Username과 사용자의 자격을 증명해 주는 크리덴셜(Credential)인 Password, 그리고 사용자의 권한 정보를 포함하고 있는 컴포넌트이다.
- UserDetails를 제공하는 컴포넌트가 바로 UserDetailsService입니다.
- UserDetailsService는 데이터베이스 등의 저장소에서 사용자의 크리덴셜(Credential)을 포함한 사용자의 정보를 조회하여 AuthenticationProvider에게 제공한다.
- UsernamePasswordAuthenticationFilter가 생성하는 Authentication은 인증을 위해 필요한 사용자의 로그인 정보를 가지고 있지만, ⭐ AuthenticationProvider가 생성한 Authentication은 인증에 성공한 사용자의 정보(Principal, Credential, GrantedAuthorities)를 가지고 있다.
- 인증된 Authentication을 전달받은 UsernamePasswordAuthenticationFilter는 SecurityContextHolder를 이용해 SecurityContext에 인증된 Authentication을 저장한다. SecurityContext는 이후에 HttpSession에 저장되어 사용자의 인증 상태를 유지한다.
##############################
Spring Security의 인증 컴포넌트
UsernamePasswordAuthenticationFilter
- 사용자의 로그인 request를 제일 먼저 만나는 컴포넌트는 바로 Spring Security Filter Chain의 UsernamePasswordAuthenticationFilter 이다
- UsernamePasswordAuthenticationFilter는 일반적으로 로그인 폼에서 제출되는 Username과 Password를 통한 인증을 처리하는 Filter 이다
- UsernamePasswordAuthenticationFilter는 클라이언트로부터 전달받은 Username과 Password를 Spring Security가 인증 프로세스에서 이용할 수 있도록 UsernamePasswordAuthenticationToken을 생성 한다
public class UsernamePasswordAuthenticationFilter extends AbstractAuthenticationProcessingFilter { // (1)
public static final String SPRING_SECURITY_FORM_USERNAME_KEY = "username"; // (2)
public static final String SPRING_SECURITY_FORM_PASSWORD_KEY = "password"; // (3)
private static final AntPathRequestMatcher DEFAULT_ANT_PATH_REQUEST_MATCHER = new AntPathRequestMatcher("/login","POST"); // (4)
...
...
public UsernamePasswordAuthenticationFilter(AuthenticationManager authenticationManager) {
super(DEFAULT_ANT_PATH_REQUEST_MATCHER, authenticationManager); // (5)
}
// (6)
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
throws AuthenticationException {
// (6-1)
if (this.postOnly && !request.getMethod().equals("POST")) {
throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
}
String username = obtainUsername(request);
...
String password = obtainPassword(request);
...
// (6-2)
UsernamePasswordAuthenticationToken authRequest = UsernamePasswordAuthenticationToken.unauthenticated(username, password);
...
return this.getAuthenticationManager().authenticate(authRequest); // (6-3)
}
...
...
}
( UsernamePasswordAuthenticationFilter의 코드 일부 )
- UsernamePasswordAuthenticationFilter는 (1)과 같이 AbstractAuthenticationProcessingFilter를 상속 한다
UsernamePasswordAuthenticationFilter 클래스의 이름이 Filter로 끝나지만 UsernamePasswordAuthenticationFilter 클래스에는 doFilter() 메서드가 존재하지 않는데 Filter가 Filter의 역할을 하기 위해서는 doFilter() 메서드가 있어야 할 텐데 없다는 것은 이상하다.
그럼 doFilter() 메서드는 어디 있는 것일까?
- 상위 클래스인 AbstractAuthenticationProcessingFilter 클래스가 doFilter() 메서드를 포함하고 있다
결과적으로 사용자의 로그인 request를 제일 먼저 전달받는 클래스는 UsernamePasswordAuthenticationFilter의 상위 클래스인 AbstractAuthenticationProcessingFilter 클래스인 것이다.
- (2)와 (3)을 통해 클라이언트의 로그인 폼을 통해 전송되는 request parameter의 디폴트 name은 username과 password라는 것을 알 수 있다
- (4)의 AntPathRequestMatcher는 클라이언트의 URL에 매치되는 매처이다
(4)를 통해 클라이언트의 URL이 "/login"이고, HTTP Method가 POST일 경우 매치될 거라는 사실을 예측 할 수 있다
(4)에서 생성되는 AntPathRequestMatcher의 객체(DEFAULT_ANT_PATH_REQUEST_MATCHER)는 (5)에서 상위 클래스인 AbstractAuthenticationProcessingFilter 클래스에 전달되어 Filter가 구체적인 작업을 수행할지 특별한 작업 없이 다른 Filter를 호출할지 결정하는 데 사용된다
- (5)에서 AntPathRequestMatcher의 객체(DEFAULT_ANT_PATH_REQUEST_MATCHER)와 AuthenticationManager를 상위 클래스인 AbstractAuthenticationProcessingFilter에 전달한다
- (6)의 attemptAuthentication() 메서드는 메서드 이름에서도 알 수 있듯이 클라이언트에서 전달한 username과 password 정보를 이용해 인증을 시도하는 메서드 이다
attemptAuthentication() 메서드는 상위 클래스인 AbstractAuthenticationProcessingFilter의 doFilter() 메서드에서 호출되는데 Filter에서 어떤 처리를 하는 시작점은 doFilter() 가 된다!!
- (6-1)에서 HTTP Method가 POST가 아니면 Exception을 throw한다는 사실을 알 수 있다
- (6-2)에서는 클라이언트에서 전달한 username과 password 정보를 이용해 UsernamePasswordAuthenticationToken을 생성한다
( 여기서의 UsernamePasswordAuthenticationToken은 인증을 하기 위해 필요한 인증 토큰이지 인증에 성공한 인증 토큰과는 상관이 없다 )
- (6-3)에서 AuthenticationManager의 authenticate() 메서드를 호출해 인증 처리를 위임하는 것을 볼 수 있다
AbstractAuthenticationProcessingFilter
- AbstractAuthenticationProcessingFilter 클래스는 UsernamePasswordAuthenticationFilter가 상속하는 상위 클래스로써 Spring Security에서 제공하는 Filter 중 하나이다
- AbstractAuthenticationProcessingFilter는 HTTP 기반의 인증 요청을 처리하지만 실질적인 인증 시도는 하위 클래스에 맡기고, 인증에 성공하면 인증된 사용자의 정보를 SecurityContext에 저장하는 역할을 한다
public abstract class AbstractAuthenticationProcessingFilter extends GenericFilterBean
implements ApplicationEventPublisherAware, MessageSourceAware {
...
...
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
doFilter((HttpServletRequest) request, (HttpServletResponse) response, chain);
}
// (1)
private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws IOException, ServletException {
// (1-1)
if (!requiresAuthentication(request, response)) {
chain.doFilter(request, response);
return;
}
try {
Authentication authenticationResult = attemptAuthentication(request, response); // (1-2)
if (authenticationResult == null) {
// return immediately as subclass has indicated that it hasn't completed
return;
}
this.sessionStrategy.onAuthentication(authenticationResult, request, response);
// Authentication success
if (this.continueChainBeforeSuccessfulAuthentication) {
chain.doFilter(request, response);
}
successfulAuthentication(request, response, chain, authenticationResult); // (1-3)
}
catch (InternalAuthenticationServiceException failed) {
this.logger.error("An internal error occurred while trying to authenticate the user.", failed);
unsuccessfulAuthentication(request, response, failed); // (1-4)
}
catch (AuthenticationException ex) {
// Authentication failed
unsuccessfulAuthentication(request, response, ex);
}
}
// (2)
protected boolean requiresAuthentication(HttpServletRequest request, HttpServletResponse response) {
if (this.requiresAuthenticationRequestMatcher.matches(request)) {
return true;
}
if (this.logger.isTraceEnabled()) {
this.logger
.trace(LogMessage.format("Did not match request to %s", this.requiresAuthenticationRequestMatcher));
}
return false;
}
...
...
// (3)
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain,
Authentication authResult) throws IOException, ServletException {
SecurityContext context = SecurityContextHolder.createEmptyContext();
context.setAuthentication(authResult);
SecurityContextHolder.setContext(context);
this.securityContextRepository.saveContext(context, request, response);
if (this.logger.isDebugEnabled()) {
this.logger.debug(LogMessage.format("Set SecurityContextHolder to %s", authResult));
}
this.rememberMeServices.loginSuccess(request, response, authResult);
if (this.eventPublisher != null) {
this.eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent(authResult, this.getClass()));
}
this.successHandler.onAuthenticationSuccess(request, response, authResult);
}
// (4)
protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response,
AuthenticationException failed) throws IOException, ServletException {
SecurityContextHolder.clearContext();
this.logger.trace("Failed to process authentication request", failed);
this.logger.trace("Cleared SecurityContextHolder");
this.logger.trace("Handling authentication failure");
this.rememberMeServices.loginFail(request, response);
this.failureHandler.onAuthenticationFailure(request, response, failed);
}
...
...
}
( AbstractAuthenticationProcessingFilter 클래스의 코드 일부 )
- (1)을 통해 AbstractAuthenticationProcessingFilter 클래스가 Spring Security의 Filter임을 알 수 있다
= (1-1)에서는 AbstractAuthenticationProcessingFilter 클래스가 인증 처리를 해야 하는지 아니면 다음 Filter를 호출할지 여부를 결정하고 있다
(1-1)에서 호출하는 requiresAuthentication() 메서드는 (2)에서 확인할 수 있듯이 하위 클래스에서 전달받은 requiresAuthenticationRequestMatcher 객체를 통해 들어오는 요청이 인증 처리를 해야 하는지 여부를 결정하고 있다
( AntPathRequestMatcher("/login","POST")의 파라미터인 URL과 HTTP Method가 매칭 조건이 된다 )
= (1-2)에서는 하위 클래스에 인증을 시도해 줄 것을 요청하고 있습니다. 여기에서 하위 클래스는 UsernamePasswordAuthenticationFilter가 된다
= (1-3)에서는 인증에 성공하면 처리할 동작을 수행하기 위해 successfulAuthentication() 메서드를 호출한다
successfulAuthentication() 메서드는 (3)에서 확인할 수 있다시피 인증에 성공한 이후 SecurityContextHolder를 통해 사용자의 인증 정보를 SecurityContext에 저장한 뒤, SecurityContext를 HttpSession에 저장한다
= 인증에 실패한다면 (1-4)와 같이 unsuccessfulAuthentication() 메서드를 호출해 인증 실패 시 처리할 동작을 수행한다
( unsuccessfulAuthentication() 메서드는 (4)에서 확인할 수 있다시피 SeucurityContext를 초기화하고, AuthenticationFailureHandler를 호출한다 )
UsernamePasswordAuthenticationToken
- UsernamePasswordAuthenticationToken은 Spring Security에서 Username/Password로 인증을 수행하기 위해 필요한 토큰이며, 또한 인증 성공 후 인증에 성공한 사용자의 인증 정보가 UsernamePasswordAuthenticationToken에 포함되어 Authentication 객체 형태로 SecurityContext에 저장된다
public class UsernamePasswordAuthenticationToken extends AbstractAuthenticationToken {
...
private final Object principal;
private Object credentials;
...
...
// (1)
public static UsernamePasswordAuthenticationToken unauthenticated(Object principal, Object credentials) {
return new UsernamePasswordAuthenticationToken(principal, credentials);
}
// (2)
public static UsernamePasswordAuthenticationToken authenticated(Object principal, Object credentials,
Collection<? extends GrantedAuthority> authorities) {
return new UsernamePasswordAuthenticationToken(principal, credentials, authorities);
}
...
...
}
( UsernamePasswordAuthenticationToken 클래스의 코드 일부 )
- UsernamePasswordAuthenticationToken은 두 개의 필드를 가지고 있는데 principal은 Username 등의 신원을 의미하고, credentials는 Password를 의미 한다
- (1)의 unauthenticated() 메서드는 인증에 필요한 용도의 UsernamePasswordAuthenticationToken 객체를 생성하고, (2)의 authenticated() 메서드는 인증에 성공한 이후 SecurityContext에 저장될 UsernamePasswordAuthenticationToken 객체를 생성한다
Authentication
- pring Security에서의 인증 자체를 표현하는 인터페이스
- UsernamePasswordAuthenticationToken은 AbstractAuthenticationToken 추상 클래스를 상속하는 확장 클래스이자 Authentication 인터페이스의 메서드 일부를 구현하는 구현 클래스이기도 하다
- 애플리케이션의 코드상에서 인증을 위해 생성되는 인증 토큰 또는 인증 성공 후 생성되는 토큰은 UsernamePasswordAuthenticationToken과 같은 하위 클래스의 형태로 생성되지만 생성된 토큰을 리턴 받거나 SecurityContext에 저장될 경우에 Authentication 형태로 리턴 받거나 저장된다
public interface Authentication extends Principal, Serializable {
Collection<? extends GrantedAuthority> getAuthorities();
Object getCredentials();
Object getDetails();
Object getPrincipal();
boolean isAuthenticated();
void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException;
}
( Authentication 인터페이스의 코드 )
- Authentication 인터페이스를 구현하는 클래스는 다음과 같은 정보를 가지고 있다
- Principal
- Principal은 사용자를 식별하는 고유 정보입니다.UserDetails에 대해서는 뒤에서 다시 알아보겠습니다.
- 일반적으로 Username/Password 기반 인증에서 Username이 Principal이 되며, 다른 인증 방식에서는 UserDetails가 Principal이 됩니다.
- Credentials
- 사용자 인증에 필요한 Password를 의미하며 인증이 이루어지고 난 직후, ProviderManager가 해당 Credentials를 삭제합니다.
- Authorities
- AuthenticationProvider에 의해 부여된 사용자의 접근 권한 목록입니다. 일반적으로 GrantedAuthority 인터페이스의 구현 클래스는 SimpleGrantedAuthority입니다.
AuthenticationManager
- AuthenticationManager는 이름 그대로 인증 처리를 총괄하는 매니저 역할을 하는 인터페이스이다
public interface AuthenticationManager {
Authentication authenticate(Authentication authentication) throws AuthenticationException;
}
( AuthenticationManager 인터페이스 )
- AuthenticationManager에는 authenticate() 메서드 하나만 정의되어 있다
- 인증을 위한 Filter는 AuthenticationManager를 통해 느슨한 결합을 유지하고 있으며, 인증을 위한 실질적인 관리는 AuthenticationManager를 구현하는 구현 클래스를 통해 이루어진다
ProviderManager
- AuthenticationManager를 구현하는 것은 어떤 클래스이든 가능하지만 Spring Security에서 AuthenticationManager 인터페이스의 구현 클래스라고 하면 일반적으로 ProviderManager를 가리킨다
- ProviderManager는 이름에서 유추할 수 있듯이 AuthenticationProvider를 관리하고, AuthenticationProvider에게 인증 처리를 위임하는 역할을 한다
public class ProviderManager implements AuthenticationManager, MessageSourceAware, InitializingBean {
...
...
// (1)
public ProviderManager(List<AuthenticationProvider> providers, AuthenticationManager parent) {
Assert.notNull(providers, "providers list cannot be null");
this.providers = providers;
this.parent = parent;
checkState();
}
...
...
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
Class<? extends Authentication> toTest = authentication.getClass();
AuthenticationException lastException = null;
AuthenticationException parentException = null;
Authentication result = null;
Authentication parentResult = null;
int currentPosition = 0;
int size = this.providers.size();
// (2)
for (AuthenticationProvider provider : getProviders()) {
if (!provider.supports(toTest)) {
continue;
}
if (logger.isTraceEnabled()) {
logger.trace(LogMessage.format("Authenticating request with %s (%d/%d)",
provider.getClass().getSimpleName(), ++currentPosition, size));
}
try {
result = provider.authenticate(authentication); // (3)
if (result != null) {
copyDetails(authentication, result);
break;
}
}
catch (AccountStatusException | InternalAuthenticationServiceException ex) {
prepareException(ex, authentication);
throw ex;
}
catch (AuthenticationException ex) {
lastException = ex;
}
}
...
...
if (result != null) {
if (this.eraseCredentialsAfterAuthentication && (result instanceof CredentialsContainer)) {
((CredentialsContainer) result).eraseCredentials(); // (4)
}
if (parentResult == null) {
this.eventPublisher.publishAuthenticationSuccess(result);
}
return result;
}
...
...
}
...
...
}
( ProviderManager 클래스의 코드 일부 )
- (1)에서 ProviderManager 클래스가 Bean으로 등록 시, List<AuthenticationProvider> 객체를 DI 받는다는 것을 알 수 있다
- DI 받은 List를 이용해 (2)와 같이 for문으로 적절한 AuthenticationProvider를 찾는다
- 적절한 AuthenticationProvider를 찾았다면 (3)과 같이 해당 AuthenticationProvider에게 인증 처리를 위임 한다
- 인증이 정상적으로 처리되었다면 (4)와 같이 인증에 사용된 Credentials를 제거한다
AuthenticationProvider
- AuthenticationProvider는 AuthenticationManager로부터 인증 처리를 위임받아 실질적인 인증 수행을 담당하는 컴포넌트이다
- Username/Password 기반의 인증 처리는 DaoAuthenticationProvider가 담당하고 있으며, DaoAuthenticationProvider는 UserDetailsService로부터 전달받은 UserDetails를 이용해 인증을 처리한다
public class DaoAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider { // (1)
...
...
private PasswordEncoder passwordEncoder;
...
...
// (2)
@Override
protected final UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication)
throws AuthenticationException {
prepareTimingAttackProtection();
try {
UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username); // (2-1)
if (loadedUser == null) {
throw new InternalAuthenticationServiceException(
"UserDetailsService returned null, which is an interface contract violation");
}
return loadedUser;
}
catch (UsernameNotFoundException ex) {
mitigateAgainstTimingAttack(authentication);
throw ex;
}
catch (InternalAuthenticationServiceException ex) {
throw ex;
}
catch (Exception ex) {
throw new InternalAuthenticationServiceException(ex.getMessage(), ex);
}
}
// (3)
@Override
protected void additionalAuthenticationChecks(UserDetails userDetails,
UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
if (authentication.getCredentials() == null) {
this.logger.debug("Failed to authenticate since no credentials provided");
throw new BadCredentialsException(this.messages
.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
}
String presentedPassword = authentication.getCredentials().toString();
if (!this.passwordEncoder.matches(presentedPassword, userDetails.getPassword())) { // (3-1)
this.logger.debug("Failed to authenticate since password does not match stored value");
throw new BadCredentialsException(this.messages
.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
}
}
...
...
}
( DaoAuthenticationProvider 클래스 코드 일부 )
- (1)을 보면 DaoAuthenticationProvider는 AbstractUserDetailsAuthenticationProvider를 상속하는 것을 확인할 수 있다
AuthenticationProvider 인터페이스의 구현 클래스는 AbstractUserDetailsAuthenticationProvider이고, DaoAuthenticationProvider는 AbstractUserDetailsAuthenticationProvider를 상속한 확장 클래스이다
## AbstractUserDetailsAuthenticationProvider 추상 클래스의 authenticate() 메서드에서부터 실질적인 인증 처리가 시작된다
- (2)의 retrieveUser() 메서드는 UserDetailsService로부터 UserDetails를 조회하는 역할을 합니다. 조회된 UserDetails는 사용자를 인증하는 데 사용될 뿐만 아니라 인증에 성공할 경우, 인증된 Authentication 객체를 생성하는 데 사용된다
= (2-1)의 this.getUserDetailsService().loadUserByUsername(username); 에서 UserDetails를 조회하는 것을 확인할 수 있다
- (3)의 additionalAuthenticationChecks() 메서드에서 PasswordEncoder를 이용해 사용자의 패스워드를 검증하고 있다
= (3-1)에서 클라이언트로부터 전달받은 패스워드와 데이터베이스에서 조회한 패스워드가 일치하는지 검증하고 있는 것을 확인할 수 있다
DaoAuthenticationProvider와 AbstractUserDetailsAuthenticationProvider의 코드를 이해하기 위해서는 메서드가 호출되는 순서가 중요하다!!
- AbstractUserDetailsAuthenticationProvider의 authenticate() 메서드 호출
- DaoAuthenticationProvider의 retrieveUser() 메서드 호출
- DaoAuthenticationProvider의 additionalAuthenticationChecks() 메서드 호출
- DaoAuthenticationProvider의 createSuccessAuthentication() 메서드 호출
- AbstractUserDetailsAuthenticationProvider의 createSuccessAuthentication() 메서드 호출
- 인증된 Authentication을 ProviderManager에게 리턴
UserDetails
- UserDetails는 데이터베이스 등의 저장소에 저장된 사용자의 Username과 사용자의 자격을 증명해 주는 크리덴셜(Credential)인 Password 그리고 사용자의 권한 정보를 포함하는 컴포넌트이며, AuthenticationProvider는 UserDetails를 이용해 자격 증명을 수행한다
public interface UserDetails extends Serializable {
Collection<? extends GrantedAuthority> getAuthorities(); // (1) 권한 정보
String getPassword(); // (2) 패스워드
String getUsername(); // (3) Username
boolean isAccountNonExpired(); // (4)
boolean isAccountNonLocked(); // (5)
boolean isCredentialsNonExpired(); // (6)
boolean isEnabled(); // (7)
}
( UserDetails 인터페이스의 코드 )
- UserDetails 인터페이스는 사용자의 권한 정보(1), 패스워드(2), Username(3)을 포함하고 있으며, 사용자 계정의 만료 여부(4) 사용자 계정의 lock 여부(5), Credentials(Password)의 만료 여부(6), 사용자의 활성화 여부(7)에 대한 정보를 포함하고 있다
UserDetailsService
- UserDetailsService는 UserDetails를 로드(load)하는 핵심 인터페이스이다
public interface UserDetailsService {
UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}
( UserDetailsService 인터페이스 코드 )
- UserDetailsService는 loadUserByUsername(String username) 메서드 하나만 정의하고 있으며, UserDetailsService를 구현하는 클래스는 loadUserByUsername(String username)을 통해 사용자의 정보를 로드한다
( 사용자의 정보를 어디에서 로드하는지는 애플리케이션에서 사용자의 정보를 어디에서 관리하고 있는지에 따라서 달라진다 )
## 사용자의 정보를 메모리에서 로드하든 데이터베이스에서 로드하든 Spring Security가 이해할 수 있는 UserDetails로 리턴 해주기만 하면 된다
SecurityContext와 SecurityContextHolder
- SecurityContext는 인증된 Authentication 객체를 저장하는 컴포넌트이고, SecurityContextHolder는 SecurityContext를 관리하는 역할을 담당한다
- Spring Security 입장에서는 SecurityContextHolder에 의해 SecurityContext에 값이 채워져 있다면 인증된 사용자로 간주 한다
( SecurityContext와 SecurityContextHolder의 구조 )
- SecurityContext가 인증된 Authentication을 포함하고 있고, 이 SecurityContext를 다시 SecurityContextHolder가 포함하고 있는 것을 볼 수 있다
- SecurityContextHolder가 SecurityContext를 포함하고 있는 것은 SecurityContextHolder를 통해 인증된 Authentication을 SecurityContext에 설정할 수 있고 또한 SecurityContextHolder를 통해 인증된 Authentication 객체에 접근할 수 있다는 것을 의미한다
public class SecurityContextHolder {
...
...
private static SecurityContextHolderStrategy strategy; // (1)
...
...
// (2)
public static SecurityContext getContext() {
return strategy.getContext();
}
...
...
// (3)
public static void setContext(SecurityContext context) {
strategy.setContext(context);
}
...
...
}
( SecurityContextHolder 클래스의 코드 일부 )
- (1)은 SecurityContextHolder에서 사용하는 전략을 의미하며, SecurityContextHolder 기본 전략은 ThreadLocalSecurityContextHolderStrategy 이다
( 이 전략은 현재 실행 스레드에 SecurityContext를 연결하기 위해 ThreadLocal을 사용하는 전략이다 )
- (2)의 getContext() 메서드를 통해 현재 실행 스레드에서 SecurityContext를 얻을 수 있다
- (3)의 setContext() 메서드는 현재 실행 스레드에 SecurityContext를 연결한다
( setContext()는 대부분 인증된 Authentication을 포함한 SecurityContext를 현재 실행 스레드에 연결하는 데 사용된다 )
ThreadLocal
- ThreadLocal은 스레드 간에 공유되지 않는 스레드 고유의 로컬 변수 같은 영역을 말한다
#######################
- UsernamePasswordAuthenticationFilter는 클라이언트로부터 전달받은 Username과 Password를 Spring Security가 인증 프로세스에서 이용할 수 있도록 UsernamePasswordAuthenticationToken을 생성한다.
- AbstractAuthenticationProcessingFilter는 HTTP 기반의 인증 요청을 처리하지만 실질적인 인증 시도는 하위 클래스에 맡기고, 인증에 성공하면 인증된 사용자의 정보를 SecurityContext에 저장하는 역할을 한다.
- Authentication은 Spring Security에서의 인증 자체를 표현하는 인터페이스이다.
- AuthenticationManager는 이름 그대로 인증 처리를 총괄하는 매니저 역할을 하는 인터페이스이며, 인증을 위한 실질적인 관리는 AuthenticationManager를 구현하는 구현 클래스를 통해 이루어진다.
- ProviderManager는 이름에서 유추할 수 있듯이 AuthenticationProvider를 관리하고, AuthenticationProvider에게 인증 처리를 위임하는 역할을 한다.
- AuthenticationProvider는 AuthenticationManager로부터 인증 처리를 위임받아 실질적인 인증 수행을 담당하는 컴포넌트이다.
- UserDetails는 데이터베이스 등의 저장소에 저장된 사용자의 Username과 사용자의 자격을 증명해 주는 크리덴셜(Credential)인 Password 그리고 사용자의 권한 정보를 포함하는 컴포넌트이며, AuthenticationProvider는 UserDetails를 이용해 자격 증명을 수행한다.
- UserDetailsService는 UserDetails를 로드(load)하는 핵심 인터페이스이다.
- SecurityContext는 인증된 Authentication 객체를 저장하는 컴포넌트이고, SecurityContextHolder는 SecurityContext를 관리하는 역할을 담당한다.
#####################################