2023. 7. 10. 23:15ㆍCode
Spring Security
- Spring MVC 기반 애플리케이션의 인증(Authentication)과 인가(Authorization or 권한 부여) 기능을 지원하는 보안 프레임워크로써, Spring MVC 기반 애플리케이션에 보안을 적용하기 위한 사실상의 표준이다
- Spring에서 지원하는 Interceptor나 Servlet Filter를 이용해서 보안 기능을 직접 구현할 수 있지만 웹 애플리케이션 보안을 대부분의 기능을 Spring Security에서 안정적으로 지원하고 있으므로 구조적으로 잘 만들어진 검증된 Spring Security를 이용하는 것이 안전한 선택이라고 볼 수 있다
Spring Security로 할 수 있는 보안 강화 기능
- 다양한 유형(폼 로그인 인증, 토큰 기반 인증, OAuth 2 기반 인증, LDAP 인증)의 사용자 인증 기능 적용
- 애플리케이션 사용자의 역할(Role)에 따른 권한 레벨 적용
- 애플리케이션에서 제공하는 리소스에 대한 접근 제어
- 민감한 정보에 대한 데이터 암호화
- SSL 적용
- 일반적으로 알려진 웹 보안 공격 차단
- 이 외에도 SSO, 클라이언트 인증서 기반 인증, 메서드 보안, 접근 제어 목록(Access Control List) 같은 보안을 위한 기능들을 지원한다
Spring Security에서 사용하는 용어 정리
1. Principal(주체)
- Spring Security에서 사용되는 Principal은 애플리케이션에서 작업을 수행할 수 있는 사용자, 디바이스 또는 시스템 등이 될 수 있으며, 일반적으로 인증 프로세스가 성공적으로 수행된 사용자의 계정 정보를 의미한다
2. Authentication(인증)
- Authentication은 애플리케이션을 사용하는 사용자가 본인이 맞음을 증명하는 절차를 의미한다
- Authentication을 정상적으로 수행하기 위해서는 사용자를 식별하기 위한 정보가 필요한데 이를 Credential(신원 증명 정보)이라고 한다 ( 예를들어 주민등록증 이나 로그인 아이디를 증명하기 위한 패스워드)
3. Authorization(인가 또는 권한 부여)
- Authorization은 Authentication이 정상적으로 수행된 사용자에게 하나 이상의 권한(authority)을 부여하여 특정 애플리케이션의 특정 리소스에 접근할 수 있게 허가하는 과정을 의미한다
- Authorization은 반드시 Authentication 과정 이후 수행되어야 하며 권한은 일반적으로 역할(Role) 형태로 부여된다
4. Access Control(접근 제어)
- Access Control은 사용자가 애플리케이션의 리소스에 접근하는 행위를 제어하는 것을 의미한다
#############################
- Spring Security는 Spring MVC 기반 애플리케이션의 인증(Authentication)과 인가(Authorization or 권한 부여) 기능을 지원하는 보안 프레임워크로써, Spring MVC 기반 애플리케이션에 보안을 적용하기 위한 사실상의 표준이다.
- Principal(주체)은 일반적으로 인증 프로세스가 성공적으로 수행된 사용자의 계정 정보를 의미한다.
- Authentication(인증)은 애플리케이션을 사용하는 사용자가 본인이 맞음을 증명하는 절차를 의미한다.
- Authorization(인가 또는 권한 부여)은 Authentication이 정상적으로 수행된 사용자에게 하나 이상의 권한(authority)을 부여하여 특정 애플리케이션의 특정 리소스에 접근할 수 있게 허가하는 과정을 의미한다.
- Credential(신원 증명 정보)은 Authentication을 정상적으로 수행하기 위해서는 사용자를 식별하기 위한 정보를 의미한다.
- Access Control(접근 제어)은 사용자가 애플리케이션의 리소스에 접근하는 행위를 제어하는 것을 의미한다.
################################
Hello, Spring Security로 알아보는 Spring Security의 기본 구조 (1)
- Spring Security Configuration을 적용하면 우리가 원하는 인증 방식과 웹 페이지에 대한 접근 권한을 설정할 수 있다
InMemory User로 인증하기
- Spring Boot에서 Spring Security Configuration을 위한 기본 구조는 @Configuration 애너테이션만 추가해 주면 된다
- (1)의 UserDetails 인터페이스는 인증된 사용자의 핵심 정보를 포함하고 있으며, UserDetails 구현체인 (2)의 User 클래스를 이용해서 사용자의 인증 정보를 생성하고 있다
- withDefaultPasswordEncoder()는 디폴트 패스워드 인코더를 이용해 사용자 패스워드를 암호화한다
- username() 메서드는 사용자의 usrname을 설정한다
( username은 “Kevin”, “Tom” 같은 사람의 이름을 의미하는 게 아니라 고유한 사용자를 식별할 수 있는 사용자 아이디 같은 값이다 )
- password() 메서드는 사용자의 password를 설정합니다. 파라미터로 지정한 값은 (1-1)의 withDefaultPasswordEncoder()로 인해 암호화된다
- roles() 메서드는 사용자의 Role 즉, 역할을 지정하는 메서드이다
( 실제 서비스로 운영되는 대부분의 애플리케이션은 크게 일반 사용자 또는 관리자 역할로 구분되어 관리자가 접속할 수 있는 기능이 별도로 존재하는데, roles() 메서드가 User의 역할을 지정해 주는 기능을 한다 )
- Spring Security에서는 사용자의 핵심 정보를 포함한 UserDetails를 관리하는 UserDetailsManager라는 인터페이스를 제공한다
( 그런데 우리는 메모리상에서 UserDetails를 관리하므로 InMemoryUserDetailsManager라는 구현체를 사용한다 )
( new InMemoryUserDetailsManager(userDetails)를 통해 UserDetailsManager 객체를 Bean으로 등록하면 Spring에서는 해당 Bean이 가지고 있는 사용자의 인증 정보가 클라이언트의 요청으로 넘어올 경우 정상적인 인증 프로세스를 수행한다)
## withDefaultPasswordEncoder() 메서드의 Deprecated는 특이하게도 향후 버전에서 제거됨을 의미하기보다는 Production 환경에서 인증을 위한 사용자 정보를 고정해서 사용하지 말라는 경고의 의미를 나타내고 있는 것이니 반드시 테스트 환경이나 데모 환경에서만 사용하는 것을 권장한다
HTTP 보안 구성 기본
- (1)과 같이 HttpSecurity를 파라미터로 가지고, SecurityFilterChain을 리턴하는 형태의 메서드를 정의하면 HTTP 보안 설정을 구성할 수 있다
- 파라미터로 지정한 HttpSecurity는 HTTP 요청에 대한 보안 설정을 구성하기 위한 핵심 클래스이다
커스텀 로그인 페이지 지정하기
- (1)에서는 CSRF(Cross-Site Request Forgery) 공격에 대한 Spring Security에 대한 설정을 비활성화하고 있다
Spring Security는 기본적으로 아무 설정을 하지 않으면 csrf() 공격을 방지하기 위해 클라이언트로부터 CSRF Token을 수신 후, 검증한다
만약, csrf().disable() 설정을 하지 않는다면 403 에러로 인해 로컬환경에서 정상적인 접속이 불가능하다
- (2)의 formLogin()을 통해 기본적인 인증 방법을 폼 로그인 방식으로 지정한다
- (3)의 loginPage("/auths/login-form") 메서드를 통해 우리가 템플릿 프로젝트에서 미리 만들어 둔 커스텀 로그인 페이지를 사용하도록 설정한다
loginPage()의 파라미터인 "/auths/login-form"은 AuthController의 loginForm() 핸들러 메서드에 요청을 전송하는 요청 URL이다
- (4)의 loginProcessingUrl("/process_login") 메서드를 통해 로그인 인증 요청을 수행할 요청 URL을 지정한다
loginProcessingUrl()의 파라미터인 "/process_login"은 우리가 만들어 둔 login.html에서 form 태그의 action 속성에 지정한 URL과 동일하다
- 커스텀 로그인 화면에서 [로그인] 버튼을 클릭하게 되면 form 태그의 action 속성에 지정된 /process_login URL로 사용자 인증을 위한 email 주소와 패스워드를 전송하게 된다
- (5)의 failureUrl("/auths/login-form?error") 메서드를 통해 로그인 인증에 실패할 경우 어떤 화면으로 리다이렉트 할 것인가를 지정한다
- 로그인 인증에 실패할 경우, 인증에 실패했다는 메시지를 표시하기 위한 로직을 추가한 login.html 코드
- ${param.error}의 값을 통해 로그인 인증 실패 메시지 표시 여부를 결정하고 있다
- ${param.error}는 Spring Security Configuration에서 failureUrl("/auths/login-form?error")의 ?error 부분에 해당하는 쿼리 파라미터를 의미한다
- (6)의 and() 메서드를 통해 Spring Security 보안 설정을 메서드 체인 형태로 구성할 수 있다
- (7), (8), (9)를 통해서 클라이언트의 요청에 대해 접근 권한을 확인합니다. 접근을 허용할지 여부를 결정한다
- (7)의 authorizeHttpRequests() 메서드를 통해 클라이언트의 요청이 들어오면 접근 권한을 확인하겠다고 정의한다
- (8)과 (9)의 anyRequest().permitAll() 메서드를 통해 클라이언트의 모든 요청에 대해 접근을 허용한다
request URI에 접근 권한 부여
- .authorizeHttpRequests().anyRequest().permitAll(); 설정을 통해 로그인 인증에 성공할 경우, 모든 화면에 접근할 수 있도록 했던 부분을 용자의 Role 별로 request URI에 접근 권한이 부여되도록 수정하였다
- (1)에서는 exceptionHandling().accessDeniedPage("/auths/access-denied")를 통해 권한이 없는 사용자가 특정 request URI에 접근할 경우 발생하는 403(Forbidden) 에러를 처리하기 위한 페이지를 설정하였다
- exceptionHandling() 메서드는 메서드의 이름 그대로 Exception을 처리하는 기능을 하며, 리턴하는 ExceptionHandlingConfigurer 객체를 통해 구체적인 Exception 처리를 할 수 있다
- accessDeniedPage() 메서드는 403 에러 발생 시, 파라미터로 지정한 URL로 리다이렉트 되도록 해준다
- 화면으로 보이는 html 페이지는 access-denied.html(srce/main/resources/templates) 이다
- authorizeHttpRequests() 메서드는 (2)와 같이 람다 표현식을 통해 request URI에 대한 접근 권한을 부여할 수 있다
- antMatchers() 메서드는 이름 그대로 ant라는 빌드 툴에서 사용되는 Path Pattern을 이용해서 매치되는 URL을 표현한다
- (2-1)의 .antMatchers("/orders/**").hasRole("ADMIN")은 ADMIN Role을 부여받은 사용자만 /orders로 시작하는 모든 URL에 접근할 수 있다는 의미이다
/orders/에서 `는 /orders로 시작하는 모든 하위 URL을 포함한다
( 예를 들어/orders/1,/orders/1/coffees,/orders/1/coffees/1` 같은 모든 하위 URL을 포함 한다 )
만약 /orders/*라는 URL을 지정했다면 /orders/1과 같이 /orders의 하위 URL의 depth가 1인 URL만 포함 한다
- (2-2)의 antMatchers("/members/my-page").hasRole("USER")은 USER Role을 부여받은 사용자만 /members/my-page URL에 접근할 수 있음을 나타낸다
- (2-3)의 .antMatchers("/**").permitAll()은 앞에서 지정한 URL 이외의 나머지 모든 URL은 Role에 상관없이 접근이 가능함을 의미한다
관리자 권한을 가진 사용자 정보 추가
- (1)과 같이 admin@gmail.com이라는 InMemory User 하나를 더 추가하였으며, admin@gmail.com에게는 ADMIN Role이 부여되었다
로그인 한 사용자 아이디 표시 및 사용자 로그아웃
- 현재 화면에서는 사용자가 로그인한 후에 어떤 사용자가 로그인했는지 알 수 없고, 로그인 한 사용자가 로그 아웃을 할 수 있는 기능도 없다
- 타임리프 기반의 HTML 템플릿에서 사용자의 인증 정보나 권한 정보를 이용해 어떤 로직을 처리하기 위해서는 먼저 (1)과 같이 sec 태그를 사용하기 위한 XML 네임스페이스를 지정한다
- (2)와 같이 태그 내부에서 sec:authorize="isAuthenticated()"를 지정하면 현재 페이지에 접근한 사용자가 인증에 성공한 사용자인지를 체크한다
( isAuthenticated()의 값이 true이면 태그 하위에 포함된 콘텐츠를 화면에 표시한다 )
- 마이페이지의 경우 ADMIN Role을 가진 사용자는 필요 없는 기능이므로 (3)과 같이 sec:authorize="hasRole('USER')"를 지정해서 USER Role을 가진 사용자에게만 표시되도록 한다
- (2)에서 isAuthenticated()의 값이 true라는 의미는 이미 로그인 한 사용자라는 의미이므로 [로그인] 메뉴 대신에 (4)와 같이 [로그아웃] 메뉴를 표시합니다. (4)의 href="/logout"에서 “/logout” URL은 SecutiryConfiguration 클래스에서 설정한 값과 같아야 한다
- (5)에서는 th:text="${#authentication.name}"를 통해 로그인 사용자의 username을 표시하고 있습니다. 이곳에는 우리가 로그인할 때 사용한 username이 표시된다
- (6)에서는 sec:authorize="!isAuthenticated()"를 통해 로그인한 사용자가 아니라면 [로그인] 버튼이 표시되도록 한다
- 로그아웃에 대한 추가 설정을 위해서는 (1)과 같이 logout()을 먼저 호출해야 합니다. logout() 메서드는 로그아웃 설정을 위한 LogoutConfigurer를 리턴한다
- (2)에서는 logoutUrl("/logout")을 통해 사용자가 로그아웃을 수행하기 위한 request URL을 지정한다
( header.html의 로그아웃 메뉴에 지정한 href=”/logout”과 동일해야 한다 )
- (3)에서는 로그아웃을 성공적으로 수행한 이후 리다이렉트 할 URL을 지정한다
#####################################
- pring Security의 기본 구조와 기본적인 동작 방식을 이해하기 가장 좋은 인증 방식은 폼 로그인 인증 방식이다.
- Spring Security를 이용한 보안 설정은 HttpSecurity를 파라미터로 가지고, SecurityFilterChain을 리턴하는 Bean을 생성하면 된다.
- HttpSecurity를 통해 Spring Security에서 지원하는 보안 설정을 구성할 수 있다.
- 로컬 환경에서 Spring Security를 테스트하기 위해서는 CSRF 설정을 비활성화해야 한다.
- InMemoryUserDetailsManager를 이용해 데이터베이스 연동 없이 테스트 목적의 InMemory User를 생성할 수 있다.
#####################################
Hello, Spring Security로 알아보는 Spring Security의 기본 구조 (2)
- Spring Security에서 지원하는 InMemory User에 저장되는 User를 등록하는 작업 추가.
( User 등록은 메모리에 등록하는 InMemory User 이다 )
회원 가입 폼을 통한 InMemory User 등록
작업 순서
1.PasswordEncoder Bean 등록
2. MemberService Bean 등록을 위한 JavaConfiguration 구성
3. InMemoryMemberService 클래스 구현
PasswordEncoder Bean 등록
- PasswordEncoder는 Spring Security에서 제공하는 패스워드 암호화 기능을 제공하는 컴포넌트 이다
- 우리가 회원 가입 폼을 통해 애플리케이션에 전달되는 패스워드는 암호화되지 않은 플레인 텍스트(Plain Text)이기 때문에 회원 가입 폼에서 전달받은 패스워드는 InMemory User로 등록하기 전에 암호화되어야 한다
- (1)과 같이 SecurityConfiguration 클래스에서 PasswordEncoder를 Bean으로 등록하고 있다
- (1-1)의 PasswordEncoderFactories.createDelegatingPasswordEncoder();를 통해 DelegatingPasswordEncoder를 먼저 생성하는데, 이 DelegatingPasswordEncoder가 실질적으로 PasswordEncoder 구현 객체를 생성해준다
## 우리가 userDetailsService() 메서드에서 미리 생성하는 InMemoryUser의 패스워드는 내부적으로 디폴트 PasswordEncoder를 통해 암호화된다
MemberService Bean 등록을 위한 JavaConfiguration 구성
package com.codestates.member;
public interface MemberService {
Member createMember(Member member);
}
- Hello Spring Security 샘플 애플리케이션은 회원 가입 폼에서 전달받은 정보를 이용해 새로운 사용자를 추가하는 기능만 있으면 되므로 createMember() 하나만 구현하는 구현체가 있으면 된다
InMemory User 등록을 위한 InMemoryMemberService 클래스
package com.codestates.member;
public class InMemoryMemberService implements MemberService {
public Member createMember(Member member) {
return null;
}
}
- InMemory User를 등록하기 위한 MemberService 인터페이스의 구현 클래스인 InMemoryMemberService 클래스이다
데이터베이스에 User를 등록하기 위한 DBMemberService 클래스
- 데이터베이스에 User를 등록하기 위한 MemberService 인터페이스의 구현 클래스인 DBMemberService 클래스이다
JavaConfiguration 구성
- JavaConfiguration 클래스에서는 MemberService 인터페이스의 구현 클래스인 InMemoryMemberService를 Spring Bean으로 등록한다
- (1)에서는 MemberService 인터페이스의 구현체인 InMemoryMemberService 클래스의 Bean 객체를 생성한다
InMemoryMemberService 클래스는 데이터베이스 연동 없이 메모리에 Spring Security의 User를 등록해야 하므로 UserDetailsManager 객체가 필요하다
또한 User 등록 시, 패스워드를 암호화한 후에 등록해야 하므로 Spring Security에서 제공하는 PasswordEncoder 객체가 필요하다
따라서 이 두 객체를 InMemoryMemberService 객체 생성 시, DI 해준다
InMemoryMemberService 구현
- InMemoryMemberService 클래스는 MemberService 인터페이스를 구현하는 구현 클래스임으로 (1)과 같이 implements MemberService를 지정한다
( 우리가 여태껏 @Service 애너테이션을 사용해 특정 서비스 클래스를 Bean으로 등록하는 방법을 사용해 왔지만 여기서는 @Service을 사용하지 않고, JavaConfiguration을 이용해 Bean을 등록하고 있다 )
- (2)에서는 UserDetailsManager와 PasswordEncoder를 DI 받는다
UserDetailsManager는 Spring Security의 User를 관리하는 관리자 역할을 하는데 우리가 SecurityConfiguration에서 Bean으로 등록한 UserDetailsManager는 InMemoryUserDetailsManager이므로 여기서 DI 받은 UserDetailsManager 인터페이스의 하위 타입은InMemoryUserDetailsManager 이다
- PasswordEncoder는 Spring Security User를 등록할 때 패스워드를 암호화해 주는 클래스 이다
( Spring Security 5에서는 InMemory User도 패스워드의 암호화가 필수입니다. 따라서 DI 받은 PasswordEncoder를 이용해 User의 패스워드를 암호화해 주어야 합니다. )
- Spring Security에서 User를 등록하기 위해서는 해당 User의 권한(Authority)을 지정해 주어야 한다
- (3)의 createAuthorities(Member.MemberRole.ROLE_USER.name());를 이용해 User의 권한 목록을 List<GrantedAuthority>로 생성하고 있다
- Member 클래스에는 MemberRole이라는 enum이 정의되어 있고, ROLE_USER와 ROLE_ADMIN이라는 enum 타입이 정의되어 있다
( Spring Security에서는 SimpleGrantedAuthority를 사용해 Role 베이스 형태의 권한을 지정할 때 ‘ROLE_’ + 권한 명 형태로 지정해 주어야 합니다. 그렇지 않을 경우 적절한 권한 매핑이 이루어지지 않는다 )
- (3-1)에서는 Java의 Stream API를 이용해 생성자 파라미터로 해당 User의 Role을 전달하면서 SimpleGrantedAuthority 객체를 생성한 후, List<SimpleGrantedAuthority> 형태로 리턴해 준다
- (4)에서는 PasswordEncoder를 이용해 등록할 User의 패스워드를 암호화하고 있다
( 패스워드를 암호화하지 않고 User를 등록한다면 User 등록은 되지만 로그인 인증 시, 다음과 같은 에러를 만나게 되므로 User의 패스워드는 반드시 암호화해야 한다 )
( java.lang.IllegalArgumentException: There is no PasswordEncoder mapped for the id "null” )
- (5)에서는 Spring Security User로 등록하기 위해 UserDetails를 생성한다
Spring Security에서는 Spring Security에서 관리하는 User 정보를 UserDetails로 관리한다
- (6)에서는 UserDetailsManager의 createUser() 메서드를 이용해서 User를 등록한다
## 위 코드를 적용시킨후 애플리케이션을 실행하고 나서 회원가입을 통해 회원 정보를 등록한 후, 로그인을 수행하며 정상적으로 로그인이 된다.
데이터베이스 연동을 통한 로그인 인증
- 커피 주문 애플리케이션에서 Member 클래스 같은 엔티티 클래스로 회원 정보를 MEMBER 테이블에 저장했듯이 Hello Spring Security 샘플 애플리케이션에서도 간단한 Member 엔티티 클래스를 이용해서 회원의 인증 정보를 포함한 회원 정보를 데이터베이스 테이블에서 관리할 수 있다
Custom UserDetailsService를 사용하는 방법
- Spring Security에서는 User의 인증 정보를 테이블에 저장하고, 테이블에 저장된 인증 정보를 이용해 인증 프로세스를 진행할 수 있는 몇 가지 방법이 존재하는데 그중 한 가지 방법이 바로 Custom UserDetailsService를 이용하는 방법 이다
일반적으로 Spring Security에서는 인증을 시도하는 주체를 User(비슷한 의미로 Principal도 있음)라고 부릅니다.
Principal은 User의 더 구체적인 정보를 의미하며, 일반적으로 Spring Security에서의 Username을 의미합니다.
샘플 애플리케이션에서는 Member 엔티티 클래스가 로그인 인증 정보를 포함할 텐데 이 Member 엔티티가 Spring Security의 User 정보를 포함한다고 보면 됩니다.
SecurityConfiguration의 설정 변경 및 추가
- (1)은 여러분들이 웹 브라우저에서 H2 웹 콘솔을 정상적으로 사용하기 위한 설정이다
frameOptions()는 HTML 태그 중에서 <frame>이나 <iframe>, <object> 태그에서 페이지를 렌더링 할지의 여부를 결정하는 기능을 합니다.
Spring Security에서는 Clickjacking 공격을 막기 위해 기본적으로 frameOptions() 기능이 활성화되어 있으며 디폴트 값은 DENY입니다. 즉, 위에서 언급한 HTML 태그를 이용한 페이지 렌더링을 허용하지 않겠다는 의미입니다.
(1)과 같이 .frameOptions().sameOrigin()을 호출하면 동일 출처로부터 들어오는 request만 페이지 렌더링을 허용합니다.
H2 웹 콘솔의 화면 자체가 내부적으로 태그를 사용하고 있으므로 개발 환경에서는 H2 웹 콘솔을 정상적으로 사용할 수 있도록 (1)과 같이 설정하면 됩니다.
JavaConfiguration의 Bean 등록 변경
- (1)과 같이 데이터베이스에 User의 정보를 저장하기 위해 MemberService 인터페이스의 구현 클래스를 DBMemberService로 변경한다
- DBMemberService는 내부에서 데이터를 데이터베이스에 저장하고, 패스워드를 암호화해야 하므로 (1-1)과 같이 MemberRepository와 PasswordEncoder 객체를 DI 해준다
DBMemberService 구현
- DBMemberService는 User의 인증 정보를 데이터베이스에 저장하는 역할을 하는데, 앞에서도 언급했지만 Spring Security 입장에서 User라고 부르는 정보는 우리가 회원 가입 시 등록하는 회원 정보 안에 포함이 되어 있다고 보면 된다
- (1)의 생성자를 통해 MemberRepository와 PasswordEncoder Bean 객체를 DI 받는다
- (2)에서 PasswordEncoder를 이용해 패스워드를 암호화 한다
- (3)에서 암호화된 패스워드를 password 필드에 다시 할당한다
패스워드의 암호화
회원의 패스워드를 암호화해서 데이터베이스에 저장하는 건 개발자 입장에서는 정말 당연한 이야기인데도 불구하고, 회원 등록 로직을 구현할 때 패스워드를 암호화하지 않고 평문(Plain Text) 그대로 저장하는 경우는 실무에서도 종종 볼 수 있는 일입니다.
패스워드 같은 민감한(sensitive) 정보는 반드시 암호화되어 저장되어야 합니다.
그리고 패스워드는 암호화된 상태에서 복호화할 이유가 없으므로 단방향 암호화 방식으로 암호화되어야 한다
Custom UserDetailsService 구현
- 데이터베이스에서 조회한 User의 인증 정보를 기반으로 인증을 처리하는 Custom UserDetailsService
UserDetailsService
Spring Security에서 제공하는 컴포넌트 중 하나인 UserDetailsService는 User 정보를 로드(load)하는 핵심 인터페이스입니다.
여기서 로드(load)의 의미는 인증에 필요한 User 정보를 어딘가에서 가지고 온다는 의미이며, 여기서 말하는 ‘어딘가’는 메모리가 될 수도 있고, DB 등의 영구 저장소가 될 수도 있습니다.
우리가 InMemory User를 등록하는 데 사용했던 InMemoryUserDetailsManager는 UserDetailsManager 인터페이스의 구현체이고, UserDetailsManager는 UserDetailsService를 상속하는 확장 인터페이스라는 점이다
HelloUserDetailsService
- 데이터베이스에서 조회한 인증 정보를 기반으로 인증을 처리하는 Custom UserDetailsService인 HelloUserDetailsService 클래스의 코드
- HelloUserDetailsService와 같은 Custom UserDetailsService를 구현하기 위해서는 (1)과 같이 UserDetailsService 인터페이스를 구현해야 한다
- HelloUserDetailsService는 데이터베이스에서 User를 조회하고, 조회한 User의 권한(Role) 정보를 생성하기 위해 (2)와 같이 MemberRepository와 HelloAuthorityUtils 클래스를 DI 받는다
- UserDetailsService 인터페이스를 implements 하는 구현 클래스는 (3)과 같이 loadUserByUsername(String username)이라는 추상 메서드를 구현해야 한다
- (4)에서는 HelloAuthorityUtils를 이용해 데이터베이스에서 조회한 회원의 이메일 정보를 이용해 Role 기반의 권한 정보(GrantedAuthority) 컬렉션을 생성 한다
- 데이터베이스에서 조회한 인증 정보와 (4)에서 생성한 권한 정보를 Spring Security에서는 아직 알지 못하기 때문에 Spring Security에 이 정보들을 제공해 주어야 하며, (5)에서는 UserDetails 인터페이스의 구현체인 User 클래스의 객체를 통해 제공하고 있다
- (5)와 같이 데이터베이스에서 조회한 User 클래스의 객체를 리턴하면 Spring Security가 이 정보를 이용해 인증 절차를 수행한다
## 데이터베이스에서 User의 인증 정보만 Spring Security에 넘겨주고, 인증 처리는 Spring Security가 대신해 준다
UserDetails
UserDetails는 UserDetailsService에 의해 로드(load)되어 인증을 위해 사용되는 핵심 User 정보를 표현하는 인터페이스입니다.
UserDetails 인터페이스의 구현체는 Spring Security에서 보안 정보 제공을 목적으로 직접 사용되지는 않고, Authentication 객체로 캡슐화되어 제공됩니다.
HelloAuthorityUtils
- (1)은 application.yml에 추가한 프로퍼티를 가져오는 표현식입니다.
(1)과 같이 @Value("${프로퍼티 경로}")의 표현식 형태로 작성하면 application.yml에 정의되어 있는 프로퍼티의 값을 클래스 내에서 사용할 수 있습니다.
(1)에서는 application.yml에 미리 정의한 관리자 권한을 가질 수 있는 이메일 주소를 불러오고 있습니다.
application.yml 파일에 정의한 관리자용 이메일 주소는 회원 등록 시, 특정 이메일 주소에 관리자 권한을 부여할 수 있는지를 결정하기 위해 사용된다
application.yml 파일에는 다음과 같이 관리자 이메일 주소를 정의해야 한다
...
...
mail:
address:
admin: <이메일 작성>
- (2)에서는 Spring Security에서 지원하는 AuthorityUtils 클래스를 이용해서 관리자용 권한 목록을 List<GrantedAuthority> 객체로 미리 생성합니다.
( 관리자 권한의 경우, 일반 사용자의 권한까지 추가로 포함되어 있습니다.)
- (3)에서는 Spring Security에서 지원하는 AuthorityUtils 클래스를 이용해서 일반 사용 권한 목록을 List<GrantedAuthority> 객체로 미리 생성합니다.
- (4)에서는 파라미터로 전달받은 이메일 주소가 application.yml 파일에서 가져온 관리자용 이메일 주소와 동일하다면 관리자용 권한인 List<GrantedAuthority> ADMIN_ROLES를 리턴한다
H2 웹 콘솔에서 등록한 회원 정보 확인 및 로그인 인증 테스트
- 애플리케이션 실행 후, 회원 가입 메뉴에서 회원을 등록한 뒤에 H2 웹 콘솔에 접속해서 보면 맴버를 볼 수 있는데
회원 가입 메뉴에서 입력했던 패스워드 정보가 암호화 되어있다
Custom UserDetails 구현
- 기존에는 loadUserByUsername() 메서드의 리턴 값으로 new User(findMember.getEmail(), findMember.getPassword(), authorities);을 리턴했지만 개선된 코드에서는 (1)과 같이 new HelloUserDetails(findMember);라는 Custom UserDetails 클래스의 생성자로 findMember를 전달하면서 코드가 조금 더 깔끔해졌다
- 코드를 유심히 보면 기존에는 loadUserByUsername() 메서드 내부에서 User의 권한 정보를 생성하는 Collection<? extends GrantedAuthority> authorities = authorityUtils.createAuthorities(findMember); 코드가 사라졌는데
(2)에서 정의한 HelloUserDetails 클래스 내부로 포함되었다
- (2)의 HelloUserDetails 클래스는 UserDetails 인터페이스를 구현하고 있고 또한 Member 엔티티 클래스를 상속하고 있다
이렇게 구성하면 데이터베이스에서 조회한 회원 정보를 Spring Security의 User 정보로 변환하는 과정과 User의 권한 정보를 생성하는 과정을 캡슐화할 수 있다
또한 HelloUserDetails 클래스는 Member 엔티티 클래스를 상속하고 있으므로 HelloUserDetails를 리턴 받아 사용하는 측에서는 두 개 클래스의 객체를 모두 다 손쉽게 캐스팅해서 사용 가능하다는 장점이 있다
- (2-3)에서는 HelloAuthorityUtils의 createAuthorities() 메서드를 이용해 User의 권한 정보를 생성하고 있다
이 코드는 기존에는 loadUserByUsername() 메서드 내부에 있었지만 지금은 HelloUserDetails 클래스 내부에서 사용되도록 캡슐화되었습니다.
- (2-4)에서는 Spring Security에서 인식할 수 있는 username을 Member 클래스의 email 주소로 채우고 있습니다. getUsername()의 리턴 값은 null일 수 없다
- 기타 UserDetails 인터페이스의 추상 메서드를 구현한 부분은 지금은 크게 중요하지 않은 부분이므로 모두 true값을 리턴하고 있다
User의 Role을 DB에서 관리하기
- User의 권한 정보를 데이터베이스에서 관리하기 위해서는 다음과 같은 과정이 필요합니다.
- User의 권한 정보를 저장하기 위한 테이블 생성
- 회원 가입 시, User의 권한 정보(Role)를 데이터베이스에 저장하는 작업
- 로그인 인증 시, User의 권한 정보를 데이터베이스에서 조회하는 작업
User의 권한 정보 테이블 생성
User의 권한 정보 테이블을 생성하기 전에 User와 User의 권한 정보 간에 관계를 먼저 생각해야 합니다.
여기서 의미하는 ‘관계’는 테이블 간의 연관 관계를 의미하며, 이 테이블 간의 연관 관계는 우리가 샘플 애플리케이션에서 사용하고 있는 ORM 기술인 JPA를 통해 손쉽게 연관 관계를 맺을 수 있습니다.
- Member 엔티티 클래스와 User의 권한 정보를 매핑하는 것은 (1)과 같이 간단하게 처리할 수 있습니다.
- (1)과 같이 List, Set 같은 컬렉션 타입의 필드는 @ElementCollection 애너테이션을 추가하면 User 권한 정보와 관련된 별도의 엔티티 클래스를 생성하지 않아도 간단하게 매핑 처리가 됩니다.
- Member 엔티티 클래스와 연관 관계 매핑에 대한 테이블이 생성되었다
한 명의 회원이 한 개 이상의 Role을 가질 수 있으므로, MEMBER 테이블과 MEMBER_ROLES 테이블은 1대 N의 관계이다
회원 가입을 통해 회원 정보가 MEMBER 테이블에 저장될 때, MEMBER_ROLES 테이블의 MEMBER_MEMBER_ID 열에는 MEMBER 테이블의 기본키 값이 그리고 ROLES 열에는 권한 정보가 저장될 것입니다.
회원 가입 시, User의 권한 정보(Role)를 데이터베이스에 저장
DBMemberService
- (1)에서는 authorityUtils.createRoles(member.getEmail());를 통해 회원의 권한 정보(List<String> roles)를 생성한 뒤 member 객체에 넘겨주고 있습니다.
createRoles() 메서드가 추가된 HelloAuthorityUtils 클래스의 코드
- (1)에서는 파라미터로 전달된 이메일 주소가 application.yml 파일의 mail.address.admin 프로퍼티에 정의된 이메일 주소와 동일하면 관리자 Role 목록(ADMIN_ROLES_STRING)을 리턴하고, 그 외에는 일반 사용자 Role 목록(USER_ROLES_STRING)을 리턴합니다.
로그인 인증 시, User의 권한 정보를 데이터베이스에서 조회하는 작업
개선된 HelloUserDetailsService(V3)
- 데이터베이스의 MEMBER_ROLES 테이블에서 조회한 Role을 기반으로 User의 권한 목록(List<GrantedAuthority>)을 생성하는 로직이 추가된 HelloUserDetailsService 클래스
- (1)에서는 HelloUserDetails가 상속하고 있는 Member(extends Member)에 데이터베이스에서 조회한 List<String> roles를 전달합니다.
- (2)에서 다시 Member(extends Member)에 전달한 Role 정보를 authorityUtils.createAuthorities() 메서드의 파라미터로 전달해서 권한 목록(List<GrantedAuthority>)을 생성합니다.
HelloAuthorityUtils
- (1)을 보면 기존에는 application.yml 파일의 mail.address.admin 프로퍼티에 정의된 관리자용 이메일 주소를 기준으로 관리자 Role을 추가했지만 이제는 그럴 필요가 없다
- 단순히 데이터베이스에서 가지고 온 Role 목록(List<String> roles)을 그대로 이용해서 권한 목록(authorities)을 만들면 되기 때문이다
- (2)와 같이 SimpleGrantedAuthority 객체를 생성할 때 생성자 파라미터로 넘겨주는 값이 “ USER" 또는 “ADMIN"으로 넘겨주면 안 되고 “ROLE_USER" 또는 “ROLE_ADMIN" 형태로 넘겨주어야 한다는 것이다
Custom AuthenticationProvider를 사용하는 방법
- Custom UserDetailsService를 사용해 로그인 인증을 처리하는 방식은 Spring Security가 내부적으로 인증을 대신 처리해 주는 방식입니다.
Custom AuthenticationProvider를 이용해 우리가 직접 로그인 인증을 처리하는 방법
@Component
public class HelloUserAuthenticationProvider implements AuthenticationProvider { // (1)
private final HelloUserDetailsService userDetailsService;
private final PasswordEncoder passwordEncoder;
public HelloUserAuthenticationProvider(HelloUserDetailsService userDetailsService,
PasswordEncoder passwordEncoder) {
this.userDetailsService = userDetailsService;
this.passwordEncoder = passwordEncoder;
}
// (3)
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
UsernamePasswordAuthenticationToken authToken = (UsernamePasswordAuthenticationToken) authentication; // (3-1)
// (3-2)
String username = authToken.getName();
Optional.ofNullable(username).orElseThrow(() -> new UsernameNotFoundException("Invalid User name or User Password"));
// (3-3)
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
String password = userDetails.getPassword();
verifyCredentials(authToken.getCredentials(), password); // (3-4)
Collection<? extends GrantedAuthority> authorities = userDetails.getAuthorities(); // (3-5)
// (3-6)
return UsernamePasswordAuthenticationToken.authenticated(username, password, authorities);
}
// (2) HelloUserAuthenticationProvider가 Username/Password 방식의 인증을 지원한다는 것을 Spring Security에 알려준다.
@Override
public boolean supports(Class<?> authentication) {
return UsernamePasswordAuthenticationToken.class.equals(authentication);
}
private void verifyCredentials(Object credentials, String password) {
if (!passwordEncoder.matches((String)credentials, password)) {
throw new BadCredentialsException("Invalid User name or User Password");
}
}
}
- (1)과 같이 AuthenticationProvider 인터페이스의 구현 클래스로 정의합니다.Spring Security는 코드 4-34와 같이 AuthenticationProvider를 구현한 구현 클래스가 Spring Bean으로 등록되어 있다면 해당 AuthenticationProvider를 이용해서 인증을 진행합니다.
- 따라서 클라이언트 쪽에서 로그인 인증을 시도하면 우리가 구현한 HelloUserAuthenticationProvider가 직접 인증을 처리하게 됩니다.
- 따라서 우리는 AuthenticationProvider의 구현 클래스로써의 HelloUserAuthenticationProvider를 구현해야 합니다.
- AuthenticationProvider 인터페이스의 구현 클래스는 authenticate(Authentication authentication) 메서드와 supports(Class<?> authentication) 메서드를 구현해야 합니다.supports() 메서드의 리턴값이 true일 경우, Spring Security는 해당 AuthenticationProvider의 authenticate() 메서드를 호출해서 인증을 진행합니다.
- 그중에서 (2)의 supports(Class<?> authentication) 메서드는 우리가 구현하는 Custom AuthenticationProvider(HelloUserAuthenticationProvider)가 Username/Password 방식의 인증을 지원한다는 것을 Spring Security에 알려주는 역할을 합니다.
- (3)의 authenticate(Authentication authentication)에서 우리가 직접 작성한 인증 처리 로직을 이용해 사용자의 인증 여부를 결정합니다.
- (3-1)에서 authentication을 캐스팅하여 UsernamePasswordAuthenticationToken을 얻습니다.
- 이 UsernamePasswordAuthenticationToken 객체에서 (3-2)와 같이 해당 사용자의 Username을 얻은 후, 존재하는지 체크합니다.
- Username이 존재한다면 (3-3)과 같이 userDetailsService를 이용해 데이터베이스에서 해당 사용자를 조회합니다.
- (3-4)에서 로그인 정보에 포함된 패스워드(authToken.getCredentials())와 데이터베이스에 저장된 사용자의 패스워드 정보가 일치하는지를 검증합니다.
- (3-4)의 검증 과정을 통과했다면 로그인 인증에 성공한 사용자이므로 (3-5)와 같이 해당 사용자의 권한을 생성합니다.
- 마지막으로 (3-6)과 같이 인증된 사용자의 인증 정보를 리턴값으로 전달합니다.
- 이 인증 정보는 내부적으로 Spring Security에서 관리하게 됩니다.
만약 회원 가입을 하지 않고 로그인을 시도할 경우(회원 가입 이후에는 상관없습니다) 인증에 실패하고 하얀 에러 화면을 만나게 됩니다
- HelloUserDetailsService를 이용해 인증을 처리할 경우에는 인증 실패 시, Spring Security 내부에서 인증 실패에 대한 전용 Exception인 AuthenticationException을 throw 하게 되고 이 AuthenticationException이 throw 되면 결과적으로 SecurityConfiguration에서 설정한 .failureUrl("/auths/login-form?error") 을 통해 로그인 폼으로 리다이렉트 하면서 아래의 사진과 같이 “로그인 인증에 실패했습니다.”라는 인증 실패 메시지를 표시합니다.
- Custom AuthenticationProvider를 이용할 경우에는 회원가입 전 인증 실패 시 Whitelebel Error Page”가 표시되는 걸까?
이유는 MemberService에서 등록된 회원 정보가 없으면, BusinessLogicException을 throw 하는데 이 BusinessLogicException이 Cusotm AuthenticationProvider를 거쳐 그대로 Spring Security 내부 영역으로 throw 되기 때문입니다.
Spring Security에서는 인증 실패 시, AuthenticationException이 throw 되지 않으면 Exception에 대한 별도의 처리를 하지 않고, 서블릿 컨테이너인 톰캣 쪽으로 이 처리를 넘깁니다.
결국 서블릿 컨테이너 영역에서 해당 Exception에 대해 “/error” URL로 포워딩하는데 우리가 특별히 “/error” URL로 포워딩되었을 때 보여줄 뷰 페이지를 별도로 구성하지 않았기 때문에 디폴트 페이지인 “Whitelebel Error Page”를 브라우저에 표시하는 것입니다.
- 해결책은 Cusotm AuthenticationProvider에서 Exception이 발생할 경우, 이 Exception을 catch 해서 AuthenticationException으로 rethrow를 해주면 됩니다.
@Component
public class HelloUserAuthenticationProvider implements AuthenticationProvider {
private final HelloUserDetailsService userDetailsService;
private final PasswordEncoder passwordEncoder;
public HelloUserAuthenticationProvider(HelloUserDetailsService userDetailsService,
PasswordEncoder passwordEncoder) {
this.userDetailsService = userDetailsService;
this.passwordEncoder = passwordEncoder;
}
// V2: AuthenticationException을 rethrow 하는 개선 코드
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
UsernamePasswordAuthenticationToken authToken = (UsernamePasswordAuthenticationToken) authentication;
String username = authToken.getName();
Optional.ofNullable(username).orElseThrow(() -> new UsernameNotFoundException("Invalid User name or User Password"));
try {
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
String password = userDetails.getPassword();
verifyCredentials(authToken.getCredentials(), password);
Collection<? extends GrantedAuthority> authorities = userDetails.getAuthorities();
return UsernamePasswordAuthenticationToken.authenticated(username, password, authorities);
} catch (Exception ex) {
throw new UsernameNotFoundException(ex.getMessage()); // (1) AuthenticationException으로 다시 throw 한다.
}
}
@Override
public boolean supports(Class<?> authentication) {
return UsernamePasswordAuthenticationToken.class.equals(authentication);
}
private void verifyCredentials(Object credentials, String password) {
if (!passwordEncoder.matches((String)credentials, password)) {
throw new BadCredentialsException("Invalid User name or User Password");
}
}
}
- AuthenticationException이 아닌 다른 Exception이 발생할 경우 AuthenticationException으로 다시 rethrow 하도록 개선된 HelloUserAuthenticationProvider 코드
(1)에서 UsernameNotFoundException을 throw 하도록 수정되었는데, UsernameNotFoundException은 AuthenticationException을 상속하는 하위 Exception이기 때문에 이 UsernameNotFoundException이 throw되면 Spring Security 쪽에서 정상적으로 catch해서 정상적인 인증 실패 화면으로 리다이렉트 시켜준다
## Custom AuthenticationProvider에서 AuthenticationException이 아닌 Exception이 발생할 경우에는 꼭 AuthenticationException을 rethrow 하도록 코드를 구성해야 한다
AuthenticationProvider AuthenticationProvider는 Spring Security에서 클라이언트로부터 전달받은 인증 정보를 바탕으로 인증된 사용자인지에 대한 인증 처리를 수행하는 Spring Security 컴포넌트입니다.
AuthenticationProvider는 인터페이스 형태로 정의되어 있으며, Spring Security에서는 AnonymousAuthenticationProvider, DaoAuthenticationProvider, JwtAuthenticationProvider, RememberMeAuthenticationProvider, OAuth2LoginAuthenticationProvider 등 다양한 유형의 AuthenticationProvider 구현체를 제공합니다.
#######################
- Spring Security에서 지원하는 InMemory User는 말 그대로 메모리에 등록되어 사용되는 User이므로 애플리케이션 실행이 종료되면 InMember User 역시 메모리에서 사라진다.
- InMemory User를 사용하는 방식은 테스트 환경이나 데모 환경에서 사용할 수 있는 방법이다.
- Spring Security는 사용자의 크리덴셜(Credential, 자격증명을 위한 구체적인 수단)을 암호화하기 위한 PasswordEncoder를 제공하며, PasswordEncoder는 다양한 암호화 방식을 제공하며, Spring Security에서 지원하는 PasswordEncoder의 디폴트 암호화 알고리즘은 bcrypt이다.
- 패스워드 같은 민감한(sensitive) 정보는 반드시 암호화되어 저장되어야 합니다. 패스워드는 복호화할 이유가 없으므로 단방향 암호화 방식으로 암호화되어야 한다.
- Spring Security에서 SimpleGrantedAuthority를 사용해 Role 베이스 형태의 권한을 지정할 때 ‘ROLE_’ + 권한명 형태로 지정해 주어야 한다.
- Spring Security에서는 Spring Security에서 관리하는 User 정보를 UserDetails로 관리한다.
- UserDetails는 UserDetailsService에 의해 로드(load)되는 핵심 User 정보를 표현하는 인터페이스입니다.
- UserDetailsService는 User 정보를 로드(load)하는 핵심 인터페이스이다.
- 일반적으로 Spring Security에서는 인증을 시도하는 주체를 User(비슷한 의미로 Principal도 있음)라고 부른다. Principal은 User의 더 구체적인 정보를 의미하며, 일반적으로 Username을 의미한다.
- Custom UserDetailsService를 사용해 로그인 인증을 처리하는 방식은 Spring Security가 내부적으로 인증을 대신 처리해 주는 방식이다.
- AuthenticationProvider는 Spring Security에서 클라이언트로부터 전달받은 인증 정보를 바탕으로 인증된 사용자인지를 처리하는 Spring Security의 컴포넌트이다.
######################################
Spring Security의 웹 요청 처리 흐름
- 우리가 Hello, Spring Security 샘플 애플리케이션을 구현해 보면서 알 수 있었던 사실은 우리가 구현한 코드상으로는 잘 드러나지 않지만, 내부적으로는 Spring Security에서 제공하는 컴포넌트들이 애플리케이션 내부에서 User의 인증과 권한에 대한 처리를 알아서 진행해 준다
Spring Security를 애플리케이션에 적용하는 데 어려움을 겪는 큰 이유 중의 하나는 Spring Security의 아키텍처와 Spring Security의 컴포넌트들이 어떻게 인터랙션 해서 인증, 권한 등의 보안 작업을 처리하는지 이해하지 못하기 때문이다
그리고 이러한 Spring Security의 동작 방식을 조금 더 잘 이해하기 위해서는 보호된 웹 요청을 처리하는 일반적인 처리 흐름과 Spring Security에서 지원하는 Filter의 역할을 이해하는 것이 선행되어야 한다
보안이 적용된 웹 요청의 일반적인 처리 흐름
- 1)에서 사용자가 보호된 리소스를 요청합니다.
- (2)에서 인증 관리자 역할을 하는 컴포넌트가 사용자의 크리덴셜(Credential)을 요청합니다.
- 사용자의 크리덴셜(Credential)이란 해당 사용자를 증명하기 위한 구체적인 수단을 의미합니다. 일반적으로는 사용자의 패스워드가 크리덴셜에 해당합니다.
- (3)에서 사용자는 인증 관리자에게 크리덴셜(Credential)을 제공합니다.
- (4)에서 인증 관리자는 크리덴셜 저장소에서 사용자의 크리덴셜을 조회합니다.
- (5)에서 인증 관리자는 사용자가 제공한 크리덴셜과 크리덴셜 저장소에 저장된 크리덴셜을 비교해 검증 작업을 수행합니다.
- (6) 유효한 크리덴셜이 아니라면 Exception을 throw 합니다.
- (7) 유효한 크리덴셜이라면 (8)에서 접근 결정 관리자 역할을 하는 컴포넌트는 사용자가 적절한 권한을 부여받았는지 검증합니다.
- (9) 적절한 권한을 부여받지 못한 사용자라면 Exception을 throw합니다.
- (10) 적절한 권한을 부여받은 사용자라면 보호된 리소스의 접근을 허용합니다.
웹 요청에서의 서블릿 필터와 필터 체인의 역할
- 사용자의 웹 요청이 Controller 같은 엔드포인트를 거쳐 접근하려는 리소스에 도달하기 전에 인증 관리자나 접근 결정 관리자 같은 컴포넌트가 중간에 웹 요청을 가로채 사용자의 크리덴셜과 접근 권한을 검증하는 것을 볼 수 있다
- 서블릿 기반 애플리케이션의 경우, 애플리케이션의 엔드포인트에 요청이 도달하기 전에 중간에서 요청을 가로챈 후 어떤 처리를 할 수 있는 적절한 포인트를 제공하는데 그것은 바로 서블릿 필터(Servlet Filter) 이다
서블릿 필터는 자바에서 제공하는 API이며, javax.servlet 패키지에 인터페이스 형태로 정의되어 있습니다.
javax.servlet.Filter 인터페이스를 구현한 서블릿 필터는 웹 요청(request)을 가로채어 어떤 처리(전처리)를 할 수 있으며, 또한 엔드포인트에서 요청 처리가 끝난 후 전달되는 응답(reponse)을 클라이언트에게 전달하기 전에 어떤 처리(후처리)를 할 수 있습니다.
서블릿 필터는 하나 이상의 필터들을 연결해 필터 체인(Filter Chain)을 구성할 수 있습니다.
- 서블릿 필터는 각각의 필터들이 doFilter()라는 메서드를 구현해야 하며, doFilter() 메서드 호출을 통해 필터 체인을 형성하게 됩니다.
- 만약 Filter 인터페이스를 구현한 다수의 Filter 클래스를 구현했다면 여러분들이 생성한 서블릿 필터에서 여러분들이 작성한 특별한 작업을 수행한 뒤, HttpServlet을 거쳐 DispatcherServlet에 요청이 전달되며, 반대로 DispatcherServlet에서 전달한 응답에 대해 역시 특별한 작업을 수행할 수 있습니다.
Spring Security에서의 필터 역할
- Spring Security에서 사용하는 필터는 보안과 관련된 작업을 추가해 준다
- 빨간색 점선으로 된 박스 영역이 바로 Spring Security Filter 영역인데 뭔가 우리가 앞에서 살펴봤던 서블릿 필터와는 조금 다른 xxxxProxy라고 붙은 이름이 보이는데 이름만 조금 다를 뿐이지 DelegatingFilterProxy와 FilterChainProxy 클래스는 Filter 인터페이스를 구현하기 때문에 엄연히 서블릿 필터로써의 역할을 합니다.
DelegatingFilterProxy
- Spring Security 역시 Spring의 핵심인 ApplicationContext를 이용한다
- 서블릿 필터와 연결되는 Spring Security만의 필터를 ApplicationContext에 Bean으로 등록한 후에 이 Bean들을 이용해서 보안과 관련된 여러 가지 작업을 처리하게 되는데 DelegatingFilterProxy 가 Bean으로 등록된 Spring Security의 필터를 사용하는 시작점이라고 생각하면 되겠습니다.
- DelegatingFilterProxy라는 이름에서 알 수 있듯이 보안과 관련된 어떤 작업을 처리하는 것이 아니라 서블릿 컨테이너 영역의 필터와 ApplicationContext에 Bean으로 등록된 필터들을 연결해 주는 브리지 역할을 한다
FilterChainProxy의 역할
FilterChainProxy
- Spring Security의 Filter Chain은 말 그대로 Spring Security에서 보안을 위한 작업을 처리하는 필터의 모음
- Spring Security의 Filter를 사용하기 위한 진입점이 바로 FilterChainProxy 이다
## FilterChainProxy부터 Spring Security에서 제공하는 보안 필터들이 필요한 작업을 수행한다
- pring Security의 Filter Chain은 URL 별로 여러 개 등록할 수 있으며, Filter Chain이 있을 때 어떤 Filter Chain을 사용할지는 FilterChainProxy가 결정하며, 가장 먼저 매칭된 Filter Chain을 실행합니다.
Ex)
- /api/** 패턴의 Filter Chain이 있고, /api/message URL 요청이 전송하는 경우
- /api/** 패턴과 제일 먼저 매칭되므로, 디폴트 패턴인 /**도 일치하지만 가장 먼저 매칭되는 /api/** 패턴과 일치하는 Filter Chain만 실행합니다.
- /message/** 패턴의 Filter Chain이 없는데 /message/ URL 요청을 전송하는 경우
- 매칭되는 Filter Chain이 없으므로 디폴트 패턴인 /** 패턴의 Filter Chain을 실행합니다.
##########################
- Spring Security를 애플리케이션에 적용하는 데 어려움을 겪는 큰 이유 중에 하나는 Spring Security의 아키텍처와 Spring Security의 컴포넌트들이 어떻게 인터랙션 해서 인증, 권한 등의 보안 작업을 처리하는지 이해하지 못하기 때문이다.
- 서블릿 필터(Servlet Filter)는 서블릿 기반 애플리케이션의 엔드포인트에 요청이 도달하기 전에 중간에서 요청을 가로챈 후 어떤 처리를 할 수 있도록 해주는 Java의 컴포넌트이다.
- Spring Security의 필터는 클라이언트의 요청을 중간에서 가로챈 뒤, 보안에 특화된 작업을 처리하는 역할을 한다.
- DelegatingFilterProxy라는 이름에서 알 수 있듯이 서블릿 컨테이너 영역의 필터와 ApplicationContext에 Bean으로 등록된 필터들을 연결해 주는 브리지 역할을 합니다.
- Spring Security의 Filter Chain은 Spring Security에서 보안을 위한 작업을 처리하는 필터의 모음이며, Spring Security의 Filter를 사용하기 위한 진입점이 바로 FilterChainProxy입니다.
############################
'Code' 카테고리의 다른 글
2023.07.12 코드스테이츠 65회차. ( Spring Security의 권한 부여 처리 흐름 ) (0) | 2023.07.12 |
---|---|
2023.07.11 코드스테이츠 64회차. ( Filter와 FilterChain, Spring Security 인증 구성요소 이해 ) (0) | 2023.07.11 |
2023.07.07 코드스테이츠 62회차. ( 인증/보안 (기초) ) (0) | 2023.07.07 |
2023.07.06 코드스테이츠 61회차. ( 회고 ) (0) | 2023.07.06 |
2023.07.05 코드스테이츠 60회차. ( 애플리케이션 빌드/ 실행/ 배포 ) (0) | 2023.07.05 |