Spring Security & JWT를 이용한 자체 Login & OAuth2 Login(네이버, 카카오) 구현 (6) - SecurityConfig 설정

오형상·2024년 9월 22일
0

CoinToZ

목록 보기
6/9
post-thumbnail

Spring Security 설정: SecurityConfig 클래스

이번 포스트에서는 Spring Security의 설정 파일SecurityConfig 클래스를 구현하고, 커스텀 로그인 필터OAuth2 로그인 기능을 추가하는 방법을 설명합니다.

📌 SecurityConfig 전체 코드

/**
 * 인증은 CustomUsernamePasswordAuthenticationFilter에서 authenticate()로 인증된 사용자로 처리
 * JwtAuthenticationProcessingFilter는 AccessToken, RefreshToken 재발급
 */
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig {

    private final LoginService loginService;

    private final BCryptPasswordEncoder bCryptPasswordEncoder;
    private final JwtService jwtService;
    private final UserRepository userRepository;

    private final RedisTemplate<String, String> redisTemplate;
    private final ObjectMapper objectMapper;
    private final OAuth2LoginSuccessHandler oAuth2LoginSuccessHandler;
    private final OAuth2LoginFailureHandler oAuth2LoginFailureHandler;
    private final CustomOAuth2UserService customOAuth2UserService;

    private final String[] SWAGGER = {
            "/v3/api-docs",
            "/swagger-resources/**", "/configuration/security", "/webjars/**",
            "/swagger-ui.html", "/swagger/**", "/swagger-ui/**"};


    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {

        http
                .formLogin().disable()
                .httpBasic().disable()
                .cors().configurationSource(corsConfigurationSource())
                .and()
                .csrf().disable()
                .headers().frameOptions().disable()
                .and()
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                .authorizeHttpRequests()
                .mvcMatchers("/**").permitAll()
                .antMatchers(SWAGGER).permitAll()
                .antMatchers(HttpMethod.GET, "/api/v1/**").authenticated()
                .antMatchers(HttpMethod.POST, "/api/v1/users/join", "/api/v1/users/login").permitAll()
                .antMatchers(HttpMethod.POST, "/api/v1/**").authenticated()
                .antMatchers("/api/v1/users/{userId}/role").hasRole("ADMIN")
                .antMatchers(HttpMethod.PUT).authenticated()
                .antMatchers(HttpMethod.DELETE).authenticated()
                .requestMatchers(PathRequest.toStaticResources().atCommonLocations()).permitAll()
                .anyRequest().permitAll() // 위의 경로 이외에는 모두 접근 가능
                .and()
                //== 소셜 로그인 설정 ==//
                .oauth2Login()
                .successHandler(oAuth2LoginSuccessHandler) // 동의하고 계속하기를 눌렀을 때 Handler 설정
                .failureHandler(oAuth2LoginFailureHandler) // 소셜 로그인 실패 시 핸들러 설정
                .userInfoEndpoint().userService(customOAuth2UserService); // customUserService 설정

        // 원래 스프링 시큐리티 필터 순서가 LogoutFilter 이후에 로그인 필터 동작
        // 따라서, LogoutFilter 이후에 우리가 만든 필터 동작하도록 설정
        // 순서 : LogoutFilter -> JwtAuthenticationProcessingFilter -> CustomUsernamePasswordAuthenticationFilter
        http.addFilterAfter(customUsernamePasswordAuthenticationFilter(), LogoutFilter.class);
        http.addFilterBefore(jwtAuthenticationProcessingFilter(), CustomUsernamePasswordAuthenticationFilter.class);

        return http.build();
    }

    @Bean
    CorsConfigurationSource corsConfigurationSource() {
        CorsConfiguration configuration = new CorsConfiguration();

        configuration.addAllowedOrigin("*");
        configuration.addAllowedHeader("*");
        configuration.addAllowedMethod("*");
        configuration.setAllowCredentials(false);

        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", configuration);
        return source;
    }

    /**
     * AuthenticationManager 설정 후 등록
     * PasswordEncoder를 사용하는 AuthenticationProvider 지정 (PasswordEncoder는 위에서 등록한 PasswordEncoder 사용)
     * FormLogin(기존 스프링 시큐리티 로그인)과 동일하게 DaoAuthenticationProvider 사용
     * UserDetailsService는 커스텀 LoginService로 등록
     * 또한, FormLogin과 동일하게 AuthenticationManager로는 구현체인 ProviderManager 사용(return ProviderManager)
     */
    @Bean
    public AuthenticationManager authenticationManager() {
        DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
        provider.setPasswordEncoder(bCryptPasswordEncoder);
        provider.setUserDetailsService(loginService);
        return new ProviderManager(provider);
    }

    /**
     * 로그인 성공 시 호출되는 LoginSuccessJWTProviderHandler 빈 등록
     */
    @Bean
    public LoginSuccessHandler loginSuccessHandler() {
        return new LoginSuccessHandler(jwtService, redisTemplate, objectMapper);
    }

    /**
     * 로그인 실패 시 호출되는 LoginFailureHandler 빈 등록
     */
    @Bean
    public LoginFailureHandler loginFailureHandler() {
        return new LoginFailureHandler(objectMapper);
    }

    /**
     * CustomUsernamePasswordAuthenticationFilter 빈 등록
     * 커스텀 필터를 사용하기 위해 만든 커스텀 필터를 Bean으로 등록
     * setAuthenticationManager(authenticationManager())로 위에서 등록한 AuthenticationManager(ProviderManager) 설정
     * 로그인 성공 시 호출할 handler, 실패 시 호출할 handler로 위에서 등록한 handler 설정
     */
    @Bean
    public CustomUsernamePasswordAuthenticationFilter customUsernamePasswordAuthenticationFilter() {
        CustomUsernamePasswordAuthenticationFilter customUsernamePasswordLoginFilter
                = new CustomUsernamePasswordAuthenticationFilter(objectMapper);
        customUsernamePasswordLoginFilter.setAuthenticationManager(authenticationManager());
        customUsernamePasswordLoginFilter.setAuthenticationSuccessHandler(loginSuccessHandler());
        customUsernamePasswordLoginFilter.setAuthenticationFailureHandler(loginFailureHandler());
        return customUsernamePasswordLoginFilter;
    }

    @Bean
    public JwtAuthenticationProcessingFilter jwtAuthenticationProcessingFilter() {
        return new JwtAuthenticationProcessingFilter(jwtService, userRepository,redisTemplate);
    }
}

1. 기본 설정

SecurityConfig 클래스는 Spring Security의 설정 파일로, 인증과 인가 관련된 모든 설정을 정의합니다.

@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig {
    ...
}

@EnableWebSecurity

@EnableWebSecuritySpring Security를 활성화하는 어노테이션입니다. 이 어노테이션을 사용하면 Spring Security와 관련된 설정들이 동작하게 됩니다.

@RequiredArgsConstructor

@RequiredArgsConstructorfinal 필드가 포함된 생성자를 자동으로 생성해주는 Lombok의 어노테이션입니다. 이로 인해 의존성 주입이 필요한 필드들을 자동으로 생성자에 주입할 수 있습니다.


2. SecurityFilterChain 설정

Spring Security 5.7 버전 이상부터는 SecurityFilterChain을 통해 필터 설정을 Bean으로 등록하여 컨테이너가 관리하도록 합니다.

기본 보안 설정 (PART 1)

http
    .formLogin().disable()
    .httpBasic().disable()
    .csrf().disable()
    .headers().frameOptions().disable()
    .and()
    .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
  1. formLogin().disable(): 기본 제공되는 폼 기반 로그인을 비활성화합니다.
  2. httpBasic().disable(): HTTP 기본 인증을 비활성화합니다. JWT 인증을 사용할 것이므로 비활성화합니다.
  3. csrf().disable(): CSRF 보안을 비활성화합니다. REST API의 경우에는 필요하지 않기 때문에 비활성화합니다.
  4. sessionCreationPolicy(SessionCreationPolicy.STATELESS): 세션을 사용하지 않도록 설정합니다. JWT 기반 인증을 사용하기 때문에 세션을 상태 비저장으로 설정합니다.

소셜 로그인 설정 (PART 2)

.oauth2Login()
.successHandler(oAuth2LoginSuccessHandler)
.failureHandler(oAuth2LoginFailureHandler)
.userInfoEndpoint().userService(customOAuth2UserService);
  • oauth2Login(): 소셜 로그인을 설정하는 메서드입니다. 소셜 로그인 성공 시와 실패 시 각각의 핸들러를 설정합니다.
  • successHandler(): 소셜 로그인 성공 시 호출되는 핸들러를 설정합니다. (예: oAuth2LoginSuccessHandler)
  • failureHandler(): 소셜 로그인 실패 시 호출되는 핸들러를 설정합니다. (예: oAuth2LoginFailureHandler)
  • userInfoEndpoint().userService(): OAuth2 로그인 시 사용자 정보를 처리하는 CustomOAuth2UserService를 설정합니다.

3. 커스텀 필터 설정

Spring Security는 기본적으로 다양한 필터를 제공하지만, 필요한 경우 커스텀 필터를 구현할 수 있습니다. 아래는 우리가 만든 필터를 설정하는 부분입니다.

http.addFilterAfter(customUsernamePasswordAuthenticationFilter(), LogoutFilter.class);
http.addFilterBefore(jwtAuthenticationProcessingFilter(), CustomUsernamePasswordAuthenticationFilter.class);
  • addFilterAfter(A, B): B 필터 이후에 A 필터가 동작하도록 설정합니다. 여기서는 LogoutFilter 이후에 커스텀 필터가 동작하도록 설정했습니다.
  • addFilterBefore(A, B): B 필터 이전에 A 필터가 동작하도록 설정합니다. JWT 인증 필터(JwtAuthenticationProcessingFilter)가 커스텀 로그인 필터(CustomUsernamePasswordAuthenticationFilter)보다 먼저 실행되도록 설정합니다.

4. Bean 등록

Spring Security에서 필요한 핸들러필터 등을 Bean으로 등록합니다.

PasswordEncoder 설정 (PART 1)

@Bean
public PasswordEncoder passwordEncoder() {
    return PasswordEncoderFactories.createDelegatingPasswordEncoder();
}

Spring Security에서 비밀번호를 암호화하는 데 사용되는 PasswordEncoder를 빈으로 등록합니다.

AuthenticationManager 설정 (PART 2)


java
@Bean
public AuthenticationManager authenticationManager() {
    DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
    provider.setPasswordEncoder(passwordEncoder());
    provider.setUserDetailsService(loginService);
    return new ProviderManager(provider);
}
  • AuthenticationManager인증을 처리하는 핵심 객체입니다. 이 메서드를 통해 DaoAuthenticationProvider를 사용하여 인증을 처리할 수 있도록 설정합니다. LoginService는 사용자 인증 정보를 조회하는 UserDetailsService의 역할을 합니다.

LoginSuccessHandler 및 LoginFailureHandler 등록 (PART 3)

@Bean
public LoginSuccessHandler loginSuccessHandler() {
    return new LoginSuccessHandler(jwtService, userRepository);
}

@Bean
public LoginFailureHandler loginFailureHandler() {
    return new LoginFailureHandler();
}

로그인 성공 및 실패 시 호출되는 핸들러를 설정합니다. 로그인 성공 시 JWT 토큰을 발급하고, 실패 시에는 에러 메시지를 반환하는 로직이 실행됩니다.

커스텀 로그인 필터 등록 (PART 4)

@Bean
public CustomUsernamePasswordAuthenticationFilter customUsernamePasswordAuthenticationFilter() {
    CustomUsernamePasswordAuthenticationFilter customFilter = new CustomUsernamePasswordAuthenticationFilter(objectMapper);
    customFilter.setAuthenticationManager(authenticationManager());
    customFilter.setAuthenticationSuccessHandler(loginSuccessHandler());
    customFilter.setAuthenticationFailureHandler(loginFailureHandler());
    return customFilter;
}

커스텀 로그인 필터를 빈으로 등록합니다. 이 필터는 UsernamePasswordAuthenticationFilter를 대체하여 로그인 요청을 처리합니다.

JWT 인증 필터 등록 (PART 5)

@Bean
public JwtAuthenticationProcessingFilter jwtAuthenticationProcessingFilter() {
    return new JwtAuthenticationProcessingFilter(jwtService, userRepository);
}

JWT 인증 필터는 클라이언트가 전송한 JWT 토큰을 검증하여 유효성을 확인하는 역할을 합니다.


0개의 댓글