Home [Spring Security JWT] 로그인 필터
Post
Cancel

[Spring Security JWT] 로그인 필터

이 게시글은 유튜브 개발자 유미님의 무료강의 스프링 시큐리티 JWT 7 : 로그인 필터 구현 을 수강하며 공부한 내용을 담고있습니다.
이 게시글은 공부한 내용을 제가 보기 편하게 정리한 내용이므로 정확한 정보는 위 영상을 시청해주세요.

이 게시글은 기본적인 Spring 기본 실습을 해봤다는 가정하에 기초적인 설명은 생략되어 있습니다.
또한 실습을 따라가며 기본적인 JWT 사용방법을 학습하기에 상세한 원리와 설명은 없는 경우가 있습니다.

개요

Servlet Filter Chain 사이에 Security Filter Chain 을 넣는다.
여기서 Chain 은 연쇄적인것을 의미하며, Servlet Filter 가 여러개 있고 이를 다 합쳐서 Chain 이라고 부르는 것이다.
삽입할 Filter 는 로그인 인증을 하는 Filter 이다.

Servlet Filter

Client 의 요청은 Servlet Filter Chain 을 거친 후 Servlet 에 도달한다. (여기서 Servlet 은 @Controller 를 의미한다.)
Spring Security 는 Form Login 방식을 기본적으로 지원하는데, 앞서 SecurityConfig 에서 Form Login 방식을 disable 하였으므로 직접 구현해야한다.
음..그럼 굳이 Form Login 을 직접구현하지 않고 Service 에서 직접 구현하면 된다는 아이디어가 떠오른다.

Controller 나 Service 에서 로그인을 구현하지 않는 이유

ChatGPT 에게 질문한 내용에 대한 답변을 정리했습니다. 오개념이 있을 수 있습니다.

  1. 보안의 관점
  2. 관심사의 분리

기본적으로 보안은 입구에서부터 걸러야한다. Servlet Filter Chain 을 거쳐야 Servlet 에 도달할 수 있다는 것은 처리할 필요 없는 Client 요청들을 걸러내고 응답해야하는 요청만 Servlet(@Controller) 에 도달한다는 의미이다.
예를 들어 회사 입구에서 경비원이 출입을 통제하면 반드시 용건이 필요한 사람만 회사에 출입을 하게되는데, 만약 이러한 필터가 없다면 회사에 아무 외부인이나 들락거리고 보안적으로 좋지 않다. 그리고 회사에 사람이 많아지면 북적거리듯이 서버의 관점에서도 응답할 가치가 없는 요청이 리소스를 잡아먹기 때문에 Servlet 에 도달하기 전에 걸러내는 것이다.

관심사의 분리 관점에서도 좋다.
기본적으로 로그인, 세션, 쿠키, JWT, OAuth, CORS 등..은 관심사가 동일한 것(보안)에 속한다.
이러한 기능들을 앞서 말했듯 Filter 에서 처리하는 것이 좋다.
예를 들어 채팅 서비스는 Service 에서 보안을 처리하고, 쇼핑몰 서비스는 보안을 Filter 에서 처리하고, 웹 뉴스 서비스는 또 다른 방법으로 보안을 처리를 한다면 유지보수와 확장이 어렵다.
모두 다른 서비스지만 보안 관련 기능은 동일한 프레임워크(ex Spring Security) 를 사용하면 일관된 방식으로 처리할 수 있다.

Login Filter

앞서 말했듯 http.formLogin((auth) -> auth.disable()); 을 했기 때문에 UsernamePasswordAuthenticationFilter 가 작동하지 않는다. 해당 필터 대신 수행할 나만의 필터를 만든다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// 1. Login Filter 클래스 생성
// 2. UsernamePasswordAuthenticationFilter 상속 받기
public class LoginFilter extends UsernamePasswordAuthenticationFilter {
    private final AuthenticationManager authenticationManager;

    public LoginFilter(AuthenticationManager authenticationManager) {
        this.authenticationManager = authenticationManager;
    }
    // 3. attemptAuthentication 외 2개 오버라이딩 하기
    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
        String username = obtainUsername(request);
        String password = obtainPassword(request);

        System.out.println("Filter 테스트, username : " + username);

        // 4. UsernamePasswordAuthenticationToken 라는 DTO 에 user name, pw 를 담기
        UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(username, password, null);

        // 5. authenticationManager 에게 authToken 에 담긴 user name, pw 가 유효한지 검사 맡기기
        return authenticationManager.authenticate(authToken);
    }

    @Override
    protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException {
        // 인증 성공 메서드
        // 5-1. authenticationManager.authenticate(authToken) 이 성공하면 이 메서드가 실행됨
    }

    @Override
    protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException {
        // 인증 실패 메서드
        // 5-2. authenticationManager.authenticate(authToken) 이 실패하면 이 메서드가 실행됨
    }
}
  1. Login Filter 클래스 생성
  2. UsernamePasswordAuthenticationFilter 상속 받기
  3. attemptAuthentication 외 2개 오버라이딩 하기
  4. UsernamePasswordAuthenticationToken 라는 DTO 에 username, pw 를 담기
  5. authenticationManager 에게 authToken 에 담긴 username, pw 가 유효한지 검사 맡기기

UsernamePasswordAuthenticationFilter 라는 필터 대신 LoginFilter 라는 걸 만들어서 사용자를 인증하려고 한다.

클래스를 만들고 UsernamePasswordAuthenticationFilter 상속받고 attemptAuthentication 를 Override 한다.

attemptAuthentication 를 오버라이드 하는 이유는 Security Filter Chain 내에 등록되어 있기 때문이다.
예를 들어, attemptAuthentication 를 오버라이딩 하지 않고 myAttemp 라는 메서드를 작성한다. 그리고 내부 코드를 똑같이 만들었다고 생각하자.
하지만 myAttempt 가 호출되지 않아 인증이 수행되지 않는다. 왜냐하면 myAttempt 를 호출하는 녀석이 없기 때문이다.

UsernamePasswordAuthenticationToken 라는 DTO 에 담는 이유는 그냥 authenticationManager.authenticate 의 매개변수 자료형이기 때문이라고 생각하자…

인증 과정으로 authenticationManager.authenticate(authToken) 을 수행하는 이유는 검색해보면 아주 길고 자세하게 나와있다… 일단 해당 방법을 사용하여 username, pw 를 인증해준다고 생각하자.

Filter 등록

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
@Configuration
@EnableWebSecurity
public class SecurityConfig {
    private final AuthenticationConfiguration authenticationConfiguration;

    public SecurityConfig(AuthenticationConfiguration authenticationConfiguration) {
        this.authenticationConfiguration = authenticationConfiguration;
    }

    @Bean
    public AuthenticationManager authenticationManager(AuthenticationConfiguration configuration) throws Exception {

        return configuration.getAuthenticationManager();
    }
    @Bean
    public BCryptPasswordEncoder bCryptPasswordEncoder() {
        return new BCryptPasswordEncoder();
    }
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        // 생략

        // 6. 필터 등록
        http.addFilterAt(new LoginFilter(authenticationManager(authenticationConfiguration)), UsernamePasswordAuthenticationFilter.class);

        // 생략
        return http.build();
    }
}

Filter 를 어느정도 만들었으니 SecurityConfig 로 가서 Filter 를 새로 등록해준다.
addFilter(필터추가), addFilterAt(~자리에 필터추가), addFilterBefore(~자리 전에 필터추가) 등이 있다.
지금은 UsernamePasswordAuthenticationFilter 자리에 직접만든 LoginFilter 를 등록해줄 것이므로 addFilterAt 을 사용하면 된다.

Test

LoginFilter 클래스의 코드를 보면 System.out.println 메서드가 있다.
아직 인증 과정을 제대로 완성하지 못했기 때문에 지금 테스트를 하면 오류가 발생한다.
그럼에도 테스트를 하는 이유는 필터가 제대로 등록되었는지 확인하기 위함이다.
Postman(또는 다른 툴을 이용해서) Body 에 username:test 를 담은 Post 메시지를 localhost:8080/login 으로 보내보자.
콘솔에서 오류를 스킵하고 스크롤을 위로 올리면 username 이 출력되는것을 볼 수 있다.
만약 출력되지 않는다면 Filter 등록이 되지 않았거나 다른 문제일 수도 있다.



이전 포스팅 : 회원가입 로직 작성
다음 포스팅 : DB기반 로그인 검증 로직 추가

This post is licensed under CC BY 4.0 by the author.

[백준][Java] 4779 칸토어 집합

[Spring Security JWT] DB기반 로그인 검증 로직 추가