목차
<id="a1"h3 data-ke-size="size23">들어가기
로그인? 보안? Spring Security?
우리는 서비스를 이용하다보면 로그인을 마주하게 된다. 이 로그인을 통해 사용자를 분석하고, 불법적인 각종 행위들을 막으며 여러모로 유용한 기능 중 하나라고 생각한다.
그러나 이 중요한 로그인을 기능으로 구현한다면 어떻게 해야할까?
내가 아는 범주에선 우선 그냥 먼저 사용자 table을 만들어서 사용할 ID와 PW을 저장한 후, 사용자가 해당 페이지 혹은 기능으로 접근할 때, 저장된 데이터와 접근한 사람의 데이터를 대조하는 방식으로 진행할 것이다.
이렇게 일일히 모든 기능마다 검증 로직이 필요하고, 보안은 물론 이게 로그인이 맞나? 싶을 정도다...
로그인 관련 기능을 구현하던 중 Spring Security에 관한 정보를 얻게 되었다. ]
과거에 프로젝트를 진행할 때, Spring Security를 사용해서 로그인을 구현하고, 인증과 인가를 통해 서비스에 접근할 수 있도록 만든 적 있었는데, 이번에도 역시 학습할 기회가 생겨서 학습겸 정리겸 내 지식으로 만들기 위해 이렇게 글을 남긴다.
What is Spring Security
스프링 시큐리티?
스프링 시큐리티란
스프링 시큐리티(Spring Security)는 스프링 서버에서 인증과 인가를 위해 다양한 기능을 제공해주는 하나의 프레임워크이다. 이를 이용해 개발자는 더욱 간편하게 인증과 인가에 관련된 내용을 구현할 수 있으며 수고를 덜어주는 장점이 있다.
- 인증? ( Authentication ) : 사용자에 관한 검증 (로그인)
- 인가? ( Authorization) : 접근에 관한 권한이 있는지 검증 (게시물 삭제, 수정 - 본인꺼만 가능)
이 Security는 기본적으로 인증-인가 순서로 진행되는데, Security는 ID와 PW를 기준으로 인증을 진행하고, Role(역할)을 이용해 인가를 진행한다. 이 과정속에는 복잡한 기능들이 상호작용 하고있고 이 복잡한 기능을 통과해야지만 비로소 우리가 원하는 결과를 얻을 수 있다 ( 원하는 결과라 하면 로그인과 해당 기능을 사용할 수 있는 것)
그래서 이 Security 라이브러리를 이용해서 우리는 인증 과정과 인가 과정을 구현해야만 한다.
먼저 우리는 스프링 시큐리티를 사용하기 위해 의존성을 추가 해야한다.
1. Gradle 파일에 의존성 등록 해주기 ( Maven은 검색해서 찾아보시길....)
implementation 'org.springframework.boot:spring-boot-starter-security'
윗 처럼 의존성만 등록하고 Run 해보면 Security에서 제공하는 기본 로그인 페이지를 제공한다.
2. 스프링 시큐리티 활성화 하기
앞서 1번에서 설명한 내용은 그냥 단순히 시큐리티만 의존성만 등록했을 뿐인데, 스프링 시큐리티는 기본적인 로그인 페이지를 제공해주고, 아랫처럼 기본 계정을 제공해준다는 것을 알 수 있다.
아직 아무런 설정도 하지 않았고, 다른 계정, 권한, DB 등 신경쓴 부분이 없는데, 이는 아직 시큐리티의 진가를 확인하기 힘들다. 그래서 스프링 시큐리티의 웹 기능과 초기화를 설정해주기 위해서는 SecurityConfig를 설정해주어야 한다.
class 명을 꼭 SecurityConfig라고 설정하지 않아도 되지만, 이 클래스 명에서 의미를 찾아본다면 시큐리티의 설정이나 실행 일부를 저장해둔 파일이라고 해석할 수 있는데, 쉽게 그냥 SecurityConfig라고 나도 설정하겠다.
과거에는 시큐리티 웹 보안의 기능을 설정하고 초기화 하기 위해선 WebSecurityConfigurerAdapter라는 것을 상속받아야 했으나, 버전이 UP 됨에 따라 Deprecated 되었다. 그래서 사용하고자 하는 기능을 Bean으로 등록해주어야 한다.
(@EnableWebSecurity 어노테이션을 객체에 붙여주면 SpringSecurityFilterChain에 등록되지만, 이제는 직접 Bean으로 등록할 것!)
공식문서에서 확인할 수 있다. ( 아래는 공식문서 내용의 일 부 )
Deprecated. Use a SecurityFilterChain Bean to configure HttpSecurity or a WebSecurityCustomizer Bean to configure WebSecurity
WebSecurityConfigurerAdapter가 Deprecated 되었으니까 SecurityFilterChain을 Bean을 등록해서 사용하라는 의미이다.
이렇게 기본 설정을 할 수 있는데, HttpSecurity라는 세부적인 보안 기능을 설정할수 있는 API를 제공하는 클래스를 생성한다.
@Configuration
@EnableWebSecurity // 스프링 Security 지원을 가능하게 함
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
// CSRF 설정(disable = 사용 안한다는 의미 )
http.csrf().disable();
// 어떤 요청이든 인가된 사용자만 가능하게
http.authorizeRequests().anyRequest().authenticated();
// 기본 로그인 사용
http.formLogin();
return http.build();
}
}
< 참고사항>
//윗 코드는 SpringBoot 2.7 이상 버전으로 SpringBoot 3.0 이상 버전을 사용할 시 아랫처럼 코드를 수정
이 코드를 http.authorizeRequests().anyRequest().authenticated();
이 코드로 http.authorizeHttpRequests().anyRequest().authenticated(); 변경해야한다.
윗 코드는 완전 기본적인 코드이다. 각 코드마다 주석을 첨부해서 해당 코드가 어떤 동작을 하는지 알 수있는데, 여기서 CSRF 공격에 대해선 비활성을 설정한 이유는 해당 CSRF 공격은 쿠키 기반의 취약점을 이용해서 공격하는 방식이다.
이 포스팅에선 REST 방식을 이용할 것이므로 비활성 했다.
기본 설정에서 사용할 수 있는 API는 많으니 필요에 의해 사용하면 될 것 같다.
여기서 참고해야 할 점은 시큐리티를 공부하다보면 다 블로그마다 다르고 사용하는 방식이 다르기에 자신의 프로젝트에 맞게 적용하려면 여러 글을 참고하는 것이 맞다고 생각한다. 블로그마다 코드가 일관적이지 않은 이유는 Spring boot의 버전업이 게속 진행되면서 그에 따라 내부에 제공하는 라이브러리들도 바뀌는 경우가 많아서 코드를 사용하는 법도 다르다는 것을 알 수 있었다.
2-1 Spring Security Filter
웹 애플리케이션은 톰캣(Tomcat) Servlet Container에 의해 구동이 된다 ( 스프링 내부에서 )
이 (톰캣 서블릿 컨테이너)Tomcat servlet Container는 서블릿들을 관리해주고, 클라이언트의 요청을 처리하고 그에 맞는 결과를 반환할 수 있도록 도와주는 기술이라 생각하면쉽다.
그래서 Filter는 해당 요청이 올바른지 아닌지 말 그대로 Filter (필터) 해주는데, 스프링 시큐리티에서는 여러 기능을 활용하기 위해 필터(filter)를 사용해 인증과 인가를 구현한다.
우리가 스프링 시큐리티를 사용하고 싶으면, 스프링부트에서 자동으로 SecurityFilterAutoConfiguration를 등록해서
SpringSecurityFilterChain이라는 빈에 위임하는데, 이는 FilterChainProxy에 위임되어 Security Filter가 동작한다.
사용자의 요청이 들어오면 1) 우선 DelegatingFilterProxy가 먼저 요청을 받게되고 2)그 요청을 FilterChainProxy에게 위임하게 된다.
윗 사진처럼 Filter들이 Chain 형식으로 엮여있는 것을 확인할 수 있다.
(해당 필터가 어떤 기능을 하는지는 직접 검색해서 찾아보는 편이 더 빠르다.)
FilterChainProxy(필터 체인 프록시) 는 각 필터를 순서대로 호출해 인증/ 인가 처리와 요청에 대한 처리를 수행하는데, 이 외에도 사용자 정의 필터를 생성해서 기존의 필터의 전, 후로 추가가 가능하다. (dofilter()에 의해 다음 필터로 이동 )
이렇게 톰캣은 등록된 필터들의 클래스를 모두 객체화해서 내부에 저장하게되고, 설정된 순서를 유지한다.
(이 말은 톰캣(WAS)가 구동되면서 실행할 필터들의 정보를 수집한다.)
이러한 필터들이 엮인게 바로 Filter Chain(필터 체인)이다. 그래서 모든 필터 클래스들은 Filter 인터페이스를 상속받아야 하고, 실제 기능을 담당하게되는 doFilter를 구현할 때, 이 필터체인 객체를 넘겨주게 된다.
filterChain.doFilter()에 의해서 필터를 거치게 되는데, 만약 더이상 실행될 필터가 없다면 전달받은 request 객체와 response 객체를 서블릿으로 넘겨 처리하게 된다. (결국 필터를 거치면서 일을 진행하고, 일을 마무리하면 다음 필터로 넘겨주는 방식)
여기서 중요한 점은 바로 모든 doFIlter()메소드는 리턴값 없이 독립적으로 수행한다는 점인데, 이 필터에 의해서 데이터가 전달 되는 것이 아니라 request나 session, 전역 컨테이너 등에 의해 이루어진다는 점, 필터 간 상호 의존 관계가 없다는 점이다.
그래서 각 필터를 사용하는 메소드들이 달라서, 필터가 수행될 때 해당 구현체를 이용해 기능을 수행한다.
앞서 설명한 것처럼 DelegatingFilterProxy가 FilterChainProxy에 위임을 요청하면 SpringSecurityFilterChain이라는 Bean을 찾게 되는데, 이 Bean이 바로 FilterChainProxy다. (설정을 통해 사용하지 않을 필터를 )
시큐리티 활성화 방법과 시큐리티 필터 체인 요약
시큐리티 활성화
시큐리티 의존성 등록 시 기본적인 로그인 폼 제공 --> 비활성화 가능
과거 WebSecurityConfigurerAdapter을 상속받았어야 했지만, 지금은 사용하고자 하는 기능을 @Bean등록 해야함
SecurityFilterChain을 만들어서 Bean으로 등록하되, 이 속에는 내가 원하는 방식으로 커스텀이 가능하다는 점
WebSecurityConfigurerAdapter를 구현한 설정 파일의 내용을 기반으로 해당 필터를 생성하는 과정
이 때, 실제 필터를 생성하는 클래스가 HttpSecurity
이러한 과정 속에서 FilterChainProxy는 필터 목록을 들고있다.
스프링시큐리티 필터체인
웹 애플리케이션은 톰캣(WAS)에 의해 스프링 내부에서 구동되며, 등록된 필터의 클래스를 모두 객체화해서 내부에 저장
이 톰캣 서블릿 컨테이너는 필터(Filter) 기능을 사용할 수 있도록 도와주는데,
이러한 여러개의 필터들이 모이면 FilterChain, 필터는 등록된 순서에 의해 동작한다.
필터속 dofilter()에 의해 다음 필터로 이동이 가능하다.
필터간 상호 의존성은 없다 (반환형이 void)
2-2 SecurityContext
윗 사진을 보면 첫번째 필터인 SecurityContextPersistenceFilter가 있다. Security는 사용자의 정보를 담기위해 Security Context라는 것을 만드는데, SecurityContextRepository는 내부적으로 HttpSessionSecurityContextRepository 클래스를 가진다. 이 클래스가 Security Context 객체를 생성하고 ,세션에 저장한다. 그리고 이 필터의 마지막에는 ClearSecurityContext를 실행한다.
이 SecurityContext는 SecurityContextHolder로 접근이 가능하며 Authentication(인증) 객체를 가지고 있다 .
SecurityContextRepository에서는 처음 인증을 요청한 사용자의 정보가 세션에 있는지 확인한다(loadContext 메소드로)
그 후 만약 사용자의 정보가 없다. (처음 인증 혹은 익명 사용자) -> SecurityContext를 생성하고 ->
이 객체를 SecurityContextHolder안에 저장 -> 그리고 다음 필터를 실행
그 후 만약 사용자의 정보가 있다. (과거 인증 받은) -> SecurityContext를 꺼내와서, SecurityContextHolder에 저장
그래서 우리는 이 인증된 객체 (Authentication)를 전역에서 꺼내쓸 수 있는데, 그 이유는 ThreadLocal에 저장되어 있기 때문이다. 해당 객체에 대해 인증이 완료되면 HttpSession에 저장되어 애플리케이션 전역에서 참조 가능하다. (중요)
윗 빨간 박스처럼 인증 객체를 꺼내서 사용할 수 있다.
2-2 SecurityContext 간단 정리
인증이 완료된 사용자의 정보는 SecurityContextRepository의 내부에 있는 HttpSessionSecurityContextRepository 의해 생성된 SecurityContext에 저장되며, 이 속에는 인증 객체의 정보가 담겨있고, 인증이 완료되면 HttpSession에 저장되어 애플리케이션 전역에서 참조 가능하다는 점.
2-3 UsernamePasswordAuthenticationFilter
UsernamePasswordAuthenticationFilter이 필터는 AbstractAuthenticationProcessingFilter를 상속한 필터로 form기반의 유저 인증을 처리한다.
그래서 인증(Authentication)객체를 만들어서 아이디와 패스워드를 저장하고, AuthenticationManager에게 인증처리를
맞기고 이 AuthenticationManager는 AuthenticationProvider에게 또 인증처리를 위임한다.
AuthenticationProvider는 UserDetailsService와 같은 서비스를 이용해 인증을 검증한다.
그래서 인증을 성공하게 되면 인증 결과를 담은 인증객체를 생성하고, SecurityContext에 저장한다.
2-4 ConcurrentSessionFilter (동시 세션 제어 )
만약 한 계정으로 인증을 2명에서 받으면 어떻게 해야할까?
해당 필터에 답이 있다.
만약 이미 내가 로그인을 했는데 제 3자가 같은 계정으로 로그인을 시도하면 세션이 두개 이상으로 늘어날 수 있는데, 이 필터는 이런 상황에서 동작하는 필터이다.
그래서 이 필터는 매 요청마다 해당 사용자의 세션이 만료되었는지를 session.expireNow 값으로 만료 설정을 구분하고, 이 값은 SessionManagementFilter에 설정되며, 세션이 만료되었다면, 세션을 즉시 만료하고 현재 요청자를 로그아웃 시킨 뒤 세션 만료와 관련된 메시지로 응답을 한다 (물리적으로 세션을 만료시키는 역할)
2-5 RememberMeAuthenticationFilter (인증을 기억하는 필터)
세션이 만료되거나 종료된 후에도 서버에서 클라이언트의 인증 유무를 기억하는 필터인데, 세션에 있는 SecurityContext내의 인증 객체가 null이면 실행된다.
로그인 성공 시 서버에서 쿠키를 헤더에 실어 발급하면 클라이언트에서는 발급된 쿠키를 가지고 서버에 접근하는데, 이 필터가 사용자를 대신해서 인증처리를 시도한다. (쿠키를 이용해서 )
2-6 AnonymousAuthenticationFilter (익명인증필터)
앞 단의 필터에서 인증이 완료되지 않았으면 해당 유저는 익명 사용자라고 인증을 하는 필터인데 이는 인증 객체가 Null임을 방지하기 위해서이다.
그래서 이 필터는 익명 인증 토큰(annonymouseAuthenticationToken)을 만들어서 SecurityContext 객체에 저장한다.
2-7 SessionManagementFilter(세션관리 필터)
이 필터는 서버에서 설정한 세션 정책에 맞게 사용자가 사용하고 있는지에 대한 검사를 담당하는 필터인데, 현재 세션에 SecurityContext가 없거나 세션이 null일 경우에 동작한다. 그래서 이 필터에서 SessionInfo 등록하고, SessionFixation와 ConcurrentSession를 진행한다.
- Register SessionInfo : 사용자의 세션 정보를 등록
- SessionFixation : 세션 고정 보호로 인증 성공 시점에 새로운 쿠키를 발급하고, 이전 쿠키를 삭제한다
- ConcurrentSession : 사용자 인증 성공과 동시에 세션이 존재하는지를 확인하고 설정 내역에 따라 어떤 처리를 진행한다.
2-8 ExceptionTranslationFilter
인증과 인가에 관련된 예외가 발생할경우 필터는 실행되는데, 인증과 인가에 관해 각각 AccessDeniedException, AuthenticationException를 던진다. 그래서 chain.doFilter를 통해 바로 다음 필터로 넘기는 것을 예외처리를 통해 동작한다.
2-9 FilterSecurityInterceptor
권한과 관련된 결정을 AccessDecisionManager에 위임해 권한 부여를 결정하고 접근 제어를 처리하는데, 사용자가 요청한 request에 따라 권한을 체크하고, 권한이 없다면 앞단의 필터인 ExcpetionTranslationFilter에서 예외처리를 진행한다.
마무리
이번 챕터에서 시큐리티를 전부 정리하려 했는데 너무 양이 방대했다. 그래서 Filter부분만 정리하게 되었는데, 다음 챕터에서 조금 더 알아보기 쉽게 정리하도록 노력해야겠다.
뿐만 아니라 시큐리티 내부에서 동작하는 필터들이 정말 많았다. 그래서 그 필터들이 우리가 쉽게 구현할 수 있도록 도와주고있음을 다시 한번 느끼게 되었다.
참고자료
https://docs.spring.io/spring-security/reference/servlet/getting-started.html
https://velog.io/@seongwon97/Spring-Security-Filter%EB%9E%80
https://spring.io/blog/2022/02/21/spring-security-without-the-websecurityconfigureradapt
https://catsbi.oopy.io/f9b0d83c-4775-47da-9c81-2261851fe0d0
'스프링' 카테고리의 다른 글
@Mappings 사용하기 (0) | 2023.12.13 |
---|---|
[#2] Spring Securiy Context Holder (0) | 2023.01.03 |
[10분 테코톡] - @JDK Dynamic Proxy & CGLIB (0) | 2022.12.22 |
[10분 테코톡] - @Transactional (0) | 2022.12.21 |
Spring- Lombok의 이해와 @Annotation (계속 추가합니다.) (0) | 2022.12.14 |