6~10개월간 spring과 멀어졌었기에 시큐리티를 이해하기 위한 사전 지식을 습득합니다.
클라이언트의 요청을 처리하고 동적으로 응답을 생성하는 서버측 프로그램입니다.
한번만 생성되며 여러 요청을 처리할 수 있습니다. 컨테이너가 종료될 때 정리됩니다.
다음과 같은 특징을 갖습니다.
클라 -> 웹 컨테이너(서블릿 컨테이너)가 요청 받음 -> 서블릿이 요청 처리 -> 서블릿이 응답 생성 -> 웹 컨테이너가 응답을 클라에게 전달
@WebServlet("/hello") // "/hello" 경로로 요청이 오면 실행됨
public class HelloServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
response.setContentType("text/html");
response.getWriter().println("<h1>Hello, Servlet!</h1>");
}
}
서블릿은 단독으로 실행되는 것이 아니라 서블릿 컨테이너(Tomcat, Jetty)에 의해 실행됩니다.
컨테이너는 서블릿의 생명주기 관리, 요청/응답 처리, 멀티스레드 처리 등의 역할을 합니다.
톰캣의 역할
클라이언트의 HTTP 요청을 받고 응답을 반환
서블릿(Servlet)과 JSP(JavaServer Pages) 실행
서블릿 컨테이너 역할 수행 (서블릿의 생명주기 관리)
디스패처 서블릿은 HTTP 프로토콜로 들어오는 모든 요청을 가장 먼저 받아 적합한 컨트롤러에 위임해주는 프론트 컨트롤러 라고 정의할 수 있습니다.
Spring MVC에서 클라이언트 요청을 적절한 컨트롤러로 전달하는 프론트 컨트롤러입니다.
즉, Spring MVC의 핵심 서블릿(Servlet)이며, 모든 요청을 중앙에서 제어합니다.
디스패처 서블릿은 다음과 같은 일을 합니다.
톰캣이 웹 애플리케이션 실행을 담당하고, DispatcherServlet은 Spring의 요청 흐름을 제어합니다. Apache Tomcat은 서블릿 컨테이너(Servlet Container) 역할을 수행하는 경량 WAS입니다.
web.xml 또는 SpringBootApplication 설정을 확인하고 DispatcherServlet 으로 요청 전달DispatcherServlet 이 적절한 컨트롤러에 요청 전달DispatcherServlet이 뷰를 결정하고 클라이언트에 응답톰캣은 전체적인 웹 애플리케이션 실행과 서블릿 관리를 담당하는 WASDispatcherServlet은 Spring MVC의 중심 컨트롤러로 요청을 컨트롤러로 전달필터(Filter)는 서블릿(Servlet) 실행 전에 요청과 응답을 가로채서 처리하는 역할을 합니다.
서블릿 실행 전/후로 동작하는 전처리(Post-Processing) 및 후처리(Pre-Processing) 기능을 수행합니다.
필터는 톰캣(WAS)의 서블릿 컨테이너 내부에서 동작합니다.

두가지 모두 전처리 후처리를 담당하지만 동작 위치가 다릅니다.
필터는 서블릿 실행 전에 동작하지만 (DispatcherServlet 앞) 인터셉터는 Spring MVC 내부에서 동작합니다.
위치가 다르므로 사용하는 상황도 다릅니다. 예를 들어 mvc의 특정 경로에만 전처리를 해야한다면 인터셉터를, 모든 http요청에 적용해야 한다면 필터를 사용합니다.
https://velog.io/@pi1199/Interceptor
https://docs.spring.io/spring-security/reference/servlet/architecture.html
Spring Security은 서블릿 필터를 기반으로 동작하므로 필터의 역할을 먼저 살펴 보는 것이 도움이됩니다. 스프링 시큐리티는 서블릿 필터로 동작합니다. 서블릿 필터중 1개로 등록되어 보안 설정을 수행합니다.

스프링 시큐리티가 서블릿 필터로 동작하지만 실제 구현은 스프링에게 위임합니다. 시큐리티가 스프링에게 구현한 자원을 보호하기 위해 Bean접근이 불가능하기 때문입니다.
스프링이 위임받아 스프링 시큐리티 필터를 구현하는 인터페이스를 DelegatingFilterProxy라고 합니다.
이를 이해하려면 Servlet Filter와 Spring Bean의 관계를 알아야 합니다.
Filter는 spring과 무관하게 동작합니다. Tomcat과 같은 서블릿 컨테이너가 실행할 때 로드됩니다.
따라서 @Bean을 직접 참조할 수 없기에 Filter는 Bean에 접근이 불가능하다는 것 입니다.
왜 직접 Bean을 접근할 수 없을까?
1.서블릿 필터와 스프링 컨텍스트의 생명주기가 다르기 때문입니다.
서블릿 필터는 spring이 성성하기 전에 로드됩니다. 따라서 security 필터가 spring context 가 생성되기 전에 실행되므로 Bean을 참조할 수 없습니다.

스프링 시큐리티는 서블릿 컨테이너(WAS) 수준에서 보안 정책을 적용해야 하므로 필터를 사용합니다.
인터셉터는 스프링 MVC 내부에서 동작하기 때문에 서블릿 외부에서 발생하는 요청(Security Headers, REST API 요청 등)에 대한 제어가 어렵습니다.
💡 인터셉터는 인증된 사용자의 요청을 검사할 수는 있지만, 인증(Authentication) 자체를 수행하기에는 적합하지 않습니다.
👉 즉, 스프링 시큐리티는 필터를 사용하여 "모든 HTTP 요청"을 보호하는 것이 필요합니다.
위에서 언급한 SecurityFilterChain의 동작 순서입니다. 이를 통해 인증을 관리합니다.


사용자가 로그인 정보를 입력하고 요청을 보냄
AuthenticationFilter에서 Request의 Username, password를 이용하여 UsernamePasswordAuthenticationToken 객체를 생성한다. (JWT Filter도 이 필터의 한 종류)
AuthenticationFilter는 AuthenticationManager에 생성한 토큰 객체를 넘겨준다.
AuthenticationManager는 토큰을 실제 인증을 진행할 AuthenticationProvider에게 토큰을 넘겨준다.
AuthenticationProvider에서는 DB에서 사용자 인증 정보를 가져올 UserDetailsService 객체에게 사용자 아이디를 넘겨 사용자 정보를 조회한다. (JwtTokenProvider도 AuthenticationProvider 중 하나)
DB에서 인증에 사용할 사용자 정보(사용자 아이디, 암호화된 패스워드, 권한 등)를 UserDetails(인증용 객체와 도메인 객체를 분리하지 않기 위해서 실제 사용되는 도메인 객체에 UserDetails를 상속하기도 한다.)라는 객체로 전달 받는다.
조회한 UserDetails를 AuthenticationProvider로 응답을 되돌려 준다.
AuthenticationProvider에서는 조회한 데이터를 통해 사용자 인증을 진행한다. (패스워드 일치, 권한 확인 등과 같은 로직)
응답을 요청을 보낸곳으로 전달한다.
AuthenticationProvider -> AuthenticationManager -> AuthenticationFilter
인증이 완료되면 사용자 정보를 가진 Authentication 객체를 SecurityContextHolder에 담은 이후 AuthenticationSuccessHandle를 실행한다. (실패시 AuthenticationFailureHandler를 실행)
https://velog.io/@on5949/SpringSecurity-Authentication-과정-정리
아래에서 authenticatie 라는 메서드에서 실제 비밀번호 인증을 진행합니다.
하지만 authenticationManager 에게 인증을 요청하는 것 뿐이지 세부 구현은 제가 구현하지 않았는데 인증이 진행되고 있었습니다.
이에 의문점이 생겨서 코드를 뜯어보게 되었습니다.
@Transactional
public TokenDto signIn(SignInDto signInDto) {
//아이디 패스워드로 Authentication 객체 생성
UsernamePasswordAuthenticationToken authenticationToken =
new UsernamePasswordAuthenticationToken(signInDto.getUserId(), signInDto.getPassword());
//실제 검증
//AuthenticationManger - provider - userdetail service 로 인증
Authentication authentication = authenticationManagerBuilder
.getObject()
.authenticate(authenticationToken);
TokenDto tokenDto = jwtTokenProvider.generateToken(authentication);
return tokenDto;
}
위 코드에서 UsernamePasswordAuthenticationToken 은 AbstractAuthenticationToken 을 상속(extends) 받아 사용하고, 이는 Authentication를 구현하고 있습니다.
따라서 Authentication 에 해당하는 변수에 사용할 수 있습니다.

UsernamePasswordAuthenticationToken 는 아이디 비밀번호만 가지고 있고 Authenticated 가 false로 설정되어 있습니다.
이를 AuthenticationManager 에 전달하고, AuthenticationProvider 에서 인증을 진행합니다. 이때 유저 정보를 DB에서 불러오기 위해서 UserDetailsService 인터페이스를 사용하게 됩니다.
Authentication 객체는 간단하게 설명하자면 Authentication에는 두가지 목적이 있다.
AuthenticationManager에 전달하기 위해 (이 경우 isAuthenticated() 는 false를 반환한다.Authentication은 다음 세가지를 포함한다.
principal : 유저 식별/구별하는 요소 , username/password에서는 UserDetail이 사용됨credentials : 주로 비밀번호, 보통 인증이 완료되면 지워진다.authorities : 권한이 인터페이스는 ProviderManager 가 구현하고 있습니다.
이 구현체의 역할은 AuthenticationProvider 들을 가지고 있고, 이 provider 들을 순회하면서 null 또는 Exception 이 아닌 값이 나올 때까지 동작합니다.
값이 나왔다면 Authentication 객체에서 비밀번호만 지우고 반환하게 됩니다.
provider 는 AuthenticationManager 에서 호출하여 진짜 인증을 할 때 사용합니다.
authenticate 메서드를 통해 인증을 하고, supports 메서드를 통해 해당 타입의 Authentication을 통해 인증가능한지 boolean 으로 리턴합니다.
(Authenticaiton 인증이 아니라 다른 인증방법이 있을 시 오버라이드하여 사용)
public interface AuthenticationProvider {
// Authentication 에 대해 인증을 진행한다.
// ProviderManager(AuthenticationManager) 에서 루프를 돌면서 호출함
Authentication authenticate(Authentication authentication) throws AuthenticationException;
// 이 Authentication 이 처리 가능한지 알려주는 메서드
// 이 또한 ProviderManager 에서 호출한다.
boolean supports(Class<?> authentication);
}
DaoAuthenticationProvider 를 보면 AuthenticationProvider 를 구현하고 있습니다.
DaoAuthenticationProvider 에서 다양한 메서드가 있는데
추상 클래스에 있는 authenticate 메서드가 실질적인 인증을 담당합니다.
//추상 클래스 상속
public class DaoAuthenticationProvider
extends AbstractUserDetailsAuthenticationProvider
//추상 클래스는 인터페이스 구현
public abstract class AbstractUserDetailsAuthenticationProvider
implements AuthenticationProvider, InitializingBean, MessageSourceAware
이때 구현체에서 구현하는 retrieveUser 메서드를 통해 DB에서 유저 정보를 가져오게 되는데 이때 사용하는 메서드가 loadUserByUserName 메서드입니다.
따라서 우리는 UserDetailsService 를 구현하는 구현체를 만들어 등록하면 됩니다.


https://malwareanalysis.tistory.com/143
https://myeongju00.tistory.com/88
https://velog.io/@on5949/SpringSecurity-Authentication-%EA%B3%BC%EC%A0%95-%EC%A0%95%EB%A6%AC