ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • jwt - JwtAuthenticationFilter
    스프링/스프링 시큐리티 2024. 1. 13. 16:54

    로그인 동작 구조

    @RequiredArgsConstructor
    public class JwtAuthenticationFilter extends UsernamePasswordAuthenticationFilter {
        private final AuthenticationManager authenticationManager;
    
        // login 요청을 하면 로그인 시도를 위해서 실행되는 함수이다.
        @Override
        public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
                throws AuthenticationException {
            System.out.println("JwtAuthenticationFilter : 로그인 시도중");
            return super.attemptAuthentication(request, response);
        }
    }

    스프링 시큐리티에서 UsernamePasswordAuthenticationFilter가 있는데, /login으로 post 요청해서 username, password를 전송하면 동작을 한다.

    SecurityConfig에서 생성자의 파라미터로 authenticationManager를 넣어줘야 되기 때문에 선언해줬다.

     

    하지만 SecurityConfig에서 formLogin.disable()을 했기 때문에 작동을 안하며, 위의 필터를 다시 SecurityConfig에 등록해줘야 한다.

     

    @Configuration
    @EnableWebSecurity
    @RequiredArgsConstructor
    public class SecurityConfig {
    
        private final CorsConfig corsConfig;
        private final PrincipalDetailsService userDetailsService;
    
        @Bean
        public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    
            ...(생략)
    
            AuthenticationManagerBuilder sharedObject = http.getSharedObject(AuthenticationManagerBuilder.class);
    
            sharedObject.userDetailsService(this.userDetailsService);
            AuthenticationManager authenticationManager = sharedObject.build();
    
            http.authenticationManager(authenticationManager);
    
            http.addFilter(corsConfig.corsFilter());
            http.addFilterBefore(new JwtAuthenticationFilter(authenticationManager), UsernamePasswordAuthenticationFilter.class);
    
            ...(생략)
    
            return http.build();
        }
    }

    위의 코드처럼 AuthenticationManagerBuilder를 이용해서 authenticationManager를 만든 다음에, 그거를 JwtAuthenticationFilter의 생성자에 넣어주면 된다.

     

    로그인을 authenticationManager가 진행시켜주기 때문에 필터의 생성자에 넣어줘야 한다.

     

    username, password를 입력받으면 authenticationManager가 로그인 시도를 한다. 그 다음 principalDetailsSerivce가 호출되며 loadUserByUsername() 함수가 실행된다.

     

    이후에 PrincipalDetails를 세션에 담고, jwt 토큰을 만들어서 응답해주면 된다.

     

    지금까지의 코드는 /login을 입력했을 때 함수들이 작동하는지 보는 코드고, 실제 로그인을 한다면 필터에 코드들을 추가해야한다.

     

    (스프링 시큐리티 버전이 많이 달라져서 구글링해도 거의 나오지 않아서 많이 헤맸다.....)

     

     

     

     

    request에서 사용자 객체 받기

    이제 JwtAuthenticationFilter에서 request에 담긴 username과 password를 받아볼 것이다.

    @RequiredArgsConstructor
    public class JwtAuthenticationFilter extends UsernamePasswordAuthenticationFilter {
        private final AuthenticationManager authenticationManager;
    
        // login 요청을 하면 로그인 시도를 위해서 실행되는 함수이다.
        @Override
        public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
                throws AuthenticationException {
           System.out.println("JwtAuthenticationFilter : 진입");
    
            ObjectMapper om = new ObjectMapper();
            LoginRequestDto loginRequestDto = null;
            try {
                loginRequestDto = om.readValue(request.getInputStream(), LoginRequestDto.class);
            } catch (Exception e) {
                e.printStackTrace();
            }
    
            System.out.println("JwtAuthenticationFilter : " + loginRequestDto);
    
            return super.attemptAuthentication(request, response);
        }

    위의 코드를 보면 om이라는 ObjectMapper를 만들고, loginRequestDto에다 request의 input 스트림을 넣는 것이다.

     

    postman에서 JSON 방식으로 원하는 데이터를 넣어주고 POST 요청을 보내면 아래의 사진과 같이 username, password가 출력된다.

     

     

     

     

    토큰 생성 후 로그인 확인

    @RequiredArgsConstructor
    public class JwtAuthenticationFilter extends UsernamePasswordAuthenticationFilter {
        private final AuthenticationManager authenticationManager;
    
        // login 요청을 하면 로그인 시도를 위해서 실행되는 함수이다.
        @Override
        public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
                throws AuthenticationException {
            System.out.println("JwtAuthenticationFilter : 진입");
    
            ObjectMapper om = new ObjectMapper();
            LoginRequestDto loginRequestDto = null;
            try {
                loginRequestDto = om.readValue(request.getInputStream(), LoginRequestDto.class);
            } catch (Exception e) {
                e.printStackTrace();
            }
    
            System.out.println("JwtAuthenticationFilter : " + loginRequestDto);
    
            UsernamePasswordAuthenticationToken authenticationToken =
                    new UsernamePasswordAuthenticationToken(loginRequestDto.getUsername(), loginRequestDto.getPassword());
            // user의 정보를 가지고 토큰을 만든다.
    
            Authentication authentication = authenticationManager.authenticate(authenticationToken);
            // authenticationManager를 만들어서 위에서 생성한 토큰을 넣는다.
            // 이게 실행될 때 PrincipalDetailsService의 loadUserByUsername() 함수가 실행됨
    
            PrincipalDetails principalDetailis = (PrincipalDetails) authentication.getPrincipal();
            System.out.println("Authentication : " + principalDetailis.getUser().getUsername());
            // 인증이 정상적으로 되어서 authentication 객체가 session 영역에 저장됨.
            // session 영역에 있는 authentication의 principal 객체가 출력된다는건 -> 로그인이 된다는 것이다.
            
            return authentication;
            // retrun을 하면 authentication 객체가 세션에 저장됨
        }
    }

    위에서 request에서 데이터를 받아온 다음에 토큰을 만들어준다. UserNamePasswordAuthenticationToken으로 토큰을 만든 다음에 Authentication 객체를 만들어서 authenticationManager에 토큰을 넣어준다.

     

    토큰을 가지고 시큐리티가 id를 받은 다음, 비밀번호와 매칭을 통해 사용자의 로그인을 도와주는 것이다.

     

     

    로그인 동작

    authentication() 함수가 호출되면 인증 프로바이더가 userDetailsService의 loadUserByUsername(토큰의 첫 번째 파라미터)을 호출한다. UserDetails를 리턴받고 토큰의 두 번째 파라미터(credential)과 UserDetails(DB의 값)의 getPassword() 함수로 비교해서 동일하면 Authentication 객체를 만들어서 필터체인으로 리턴해준다.

     

    출력된 로그를 보니 username을 이용해서 쿼리문이 제대로 요청되었으며, Authentication에 username이 출력되어서 로그인이 잘 된 것을 확인할 수 있었다.

     

     

    마지막으로 return authentication;으로 authentication이 세션에 저장된다.

     

    JWT 토큰을 사용하면서 세션을 만들 이유가 없는데 만드는 이유는 권한 처리를 해주기 때문이다.

     


    main class에 비밀번호 암호화 추가

    @SpringBootApplication
    public class JwtApplication {
    
    	@Bean
    	public BCryptPasswordEncoder passwordEncoder() {
    		return new BCryptPasswordEncoder();
    	}
    
    	public static void main(String[] args) {
    		SpringApplication.run(JwtApplication.class, args);
    	}
    
    }

    여기서 암호화된 비밀번호와의 비교를 위해서 메인 클래스에 BCryptPasswordEncoder를 스프링 빈으로 등록해줬다.

     

     

     

    JWT 토큰 만들기 - successfulAuthentication()

    @RequiredArgsConstructor
    public class JwtAuthenticationFilter extends UsernamePasswordAuthenticationFilter {
    
        ... (생략)
    
        @Override
        protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain,
                                                Authentication authResult) throws IOException, ServletException {
    
            System.out.println("successfulAuthentication 실행 : 인증이 완료됨 ");
            PrincipalDetails principalDetailis = (PrincipalDetails) authResult.getPrincipal();
    
            String jwtToken = JWT.create()
                    .withSubject(principalDetailis.getUsername())
                    .withExpiresAt(new Date(System.currentTimeMillis() + JwtProperties.EXPIRATION_TIME)) // 만료시간
                    .withClaim("id", principalDetailis.getUser().getId())
                    .withClaim("username", principalDetailis.getUser().getUsername())
                    .sign(Algorithm.HMAC512(JwtProperties.SECRET)); // 알고리즘 서명
    
            response.addHeader(JwtProperties.HEADER_STRING, JwtProperties.TOKEN_PREFIX+jwtToken);
        }
    }

    토큰을 만드는 과정이다. JwtAuthenticationFilter 파일의 attemptAuthentication 함수 아래에 존재한다. successfulAuthentication 함수는 attemptAuthentication이 실행되고 인증이 정상적으로 처리되었으며 실행되는 함수이다.

     

    여기서 JWT 토큰을 만들어서 reqeust를 요청한 사용자에게 JWT 토큰을 response한다.

     

     

     

    JwtProperties 인터페이스

    public interface JwtProperties {
        String SECRET = "조찬희"; // 우리 서버만 알고 있는 비밀값
        int EXPIRATION_TIME = 864000000; // 10일 (1/1000초)
        String TOKEN_PREFIX = "Bearer ";
        String HEADER_STRING = "Authorization";
    }

     

     

     

    postman에서 다시 로그인을 진행하면 헤더에 Authorization의 value에 Bearer ~~~가 들어간다. 저게 바로 지금까지 만든 JWT 토큰이다. (eyJ0~~~)

     

     

    로그인이 정상적으로 이루어지면 JWT 토큰을 생성하고 클라이언트 쪽으로 JWT 토큰을 응답해준다.

    요청할 때마다 JWT 토큰을 가지고 요청하며 서버는 JWT 토큰이 유효한지를 판단한다.

     

    그런데 아직 JWT 토큰이 유효한지 판단하는 필터가 없다.

     

     

    다음 포스팅에서는 JWT 토큰이 유효한지 판단하는 필터를 만들어보겠다.

Designed by Tistory.