일친 (IlChin) - 회원 로그인, 회원가입(5)

no.oneho·2025년 5월 30일
0

일친 개발기

목록 보기
10/17

이제 security config 파일에서 필터체인을 걸며 해야하는 부분이다

  1. CORS, CSRF, 폼 로그인: 모두 비활성화
    로컬에서만 돌릴 서버기 때문에 전부 비활성화

  2. Stateless 모드로 세션 비활성화
    sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) 를 통해 서버가 세션을 저장하지 않고, 매 요청마다 토큰 기반으로 인증을 처리하는 구조를 명시

  3. 커스텀 TokenFilter 등록
    addFilterBefore(new TokenFilter(), UsernamePasswordAuthenticationFilter.class)를 통해 이전포스트에서 생성한 Filter를 등록해준다.

  4. 경로 보호
    requestMatchers("/api/**/auth/**").authenticated()
    인증이 필요한 url을 명시
    반면, 그 외 모든 요청은 공개(permitAll) 처럼 설정해준다.

  5. 기타
    추후 추가할 나머지 예외처리exceptionHandling()은 확장성 고려해 비워둔다

이제 이대로 파일을 만들어보자

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    CorsConfigurationSource corsConfigurationSource() {
        return request -> {
            CorsConfiguration config = new CorsConfiguration();
            config.setAllowedHeaders(Collections.singletonList("*"));
            config.setAllowedMethods(Collections.singletonList("*"));
            config.setAllowedOriginPatterns(Collections.singletonList("*"));
            config.setAllowCredentials(true);
            return config;
        };
    }

    @Bean
    public SecurityFilterChain securityFilterChai(HttpSecurity http) throws Exception {
        http
                .cors(cors -> cors.configurationSource(corsConfigurationSource()))
                .csrf(AbstractHttpConfigurer::disable)
                .formLogin(AbstractHttpConfigurer::disable)
                .sessionManagement(sessionManagement ->
                        sessionManagement
                                .sessionCreationPolicy(SessionCreationPolicy.STATELESS))
                .addFilterBefore(new TokenFilter(), UsernamePasswordAuthenticationFilter.class)
                .authorizeHttpRequests(
                        authorizeRequests -> authorizeRequests
                                .requestMatchers("/api/**/auth/**")
                                .authenticated()
                                .anyRequest().permitAll()
                )
                .headers(
                        httpSecurityHeadersConfigurer -> httpSecurityHeadersConfigurer
                                .frameOptions(HeadersConfigurer.FrameOptionsConfig::disable)
                )
                .exceptionHandling((exceptionConfig) -> {});

        return http.build();
    }
}

이제 진짜 거의 다왔다.

일단 현재까지 개발한 내용으로 권한 인증이 필요한 api를 테스트용으로 하나 만들어보자

인증토큰을 제대로 넣은 경우 의도한대로 동작을 하고있다.
그럼 토큰이 잘못되었거나 변조되었다면? 아니면 기간만료일경우
아무튼 토큰 유효성 검증을 통과 못한 경우엔 어떻게 될까?


에러는 뜬다.. 근데 이 에러는 우리가 이전에 만든 에러 response하고 형태가 전혀 다르다


이런 json으로 반환하는게 기대값이였는데 왜 그럴까?

Spring security 필터 단계에서 발생하는 예외는 DispatcherServlet가 호출되기 전에 발생하게 된다.
반면 저번에 만들어둔 GlobalExceptionHandler은 DispatcherServlet이 요청을 받아서 처리할 때 호출된다.

그렇기 때문에 Advice로는 해당 예외를 캐치할수가없다

흠.. 그렇다면 어떤식으로 해결하면 될까?
바로 Spring security 내부적으로 exception을 담당하는 필터를 한번 더 거치게 만들면 된다.

바로 만들어보자

public class TokenExceptionHandleFilter extends OncePerRequestFilter {

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
                                    FilterChain filterChain) throws
            ServletException, IOException {
        try {
            filterChain.doFilter(request, response);
        } catch (SignatureException | ExpiredJwtException e) {
            setErrorResponse(response, UserException.CANT_ACCESS);
        } catch (AccessDeniedException e) {
            setErrorResponse(response, UserException.HANDLE_ACCESS_DENIED);
        } catch (JwtException e) {
            filterChain.doFilter(request, response);
        }
    }

    private void setErrorResponse(HttpServletResponse response, Error errorCode) throws IOException {
        Response<String> exceptionResponse = new Response<>(errorCode.getStatus().value(), errorCode.getMessage());

        // 응답 상태 코드를 401로 설정 (인증 실패).
        response.setStatus(errorCode.getStatus().value());

        response.setContentType("application/json");

        response.setCharacterEncoding("utf-8");
        
        try (PrintWriter writer = response.getWriter()) {
            ObjectMapper objectMapper = new ObjectMapper();
            writer.write(objectMapper.writeValueAsString(exceptionResponse));
        }
    }

}

그 후 새로 만든 핸들필터를 securityConfig에 필터체인을 걸어준다

.addFilterBefore(new TokenExceptionHandleFilter(), UsernamePasswordAuthenticationFilter.class)

추가적으로 작업할 예외가 있다면 커스텀해서 security config에

.exceptionHandling((exceptionConfig) -> {});

부분에 추가하면 된다

예를들어

@Component
public class CustomAccessDeniedHandler implements AccessDeniedHandler {
    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response,
                       AccessDeniedException accessDeniedException) throws IOException, ServletException {

        Response<String> exceptionResponse = new Response<>(403, "권한이 없는 메뉴에 접근하셨습니다.");
        
        response.setStatus(HttpServletResponse.SC_FORBIDDEN);
        
        response.setContentType("application/json");

        response.setCharacterEncoding("utf-8");
        
        try (PrintWriter writer = response.getWriter()) {
            ObjectMapper objectMapper = new ObjectMapper();
            writer.write(objectMapper.writeValueAsString(exceptionResponse));
        }
    }
}

이런 파일을 하나 생성하고
Security Config 파일에 의존성 주입을 해준 후

.exceptionHandling((exceptionConfig) -> {
                    exceptionConfig.accessDeniedHandler(accessDeniedHandler);
                });

이런식으로 추가하면 된다.

@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig {

    private final CustomAccessDeniedHandler accessDeniedHandler;
    private final CustomAuthenticationEntryPoint authenticationEntryPoint;

    CorsConfigurationSource corsConfigurationSource() {
        return request -> {
            CorsConfiguration config = new CorsConfiguration();
            config.setAllowedHeaders(Collections.singletonList("*"));
            config.setAllowedMethods(Collections.singletonList("*"));
            config.setAllowedOriginPatterns(Collections.singletonList("*"));
            config.setAllowCredentials(true);
            return config;
        };
    }

    @Bean
    public SecurityFilterChain securityFilterChai(HttpSecurity http) throws Exception {
        http
                .cors(cors -> cors.configurationSource(corsConfigurationSource()))
                .csrf(AbstractHttpConfigurer::disable)
                .formLogin(AbstractHttpConfigurer::disable)
                .sessionManagement(sessionManagement ->
                        sessionManagement
                                .sessionCreationPolicy(SessionCreationPolicy.STATELESS))
                .addFilterBefore(new TokenExceptionHandleFilter(), UsernamePasswordAuthenticationFilter.class)
                .addFilterBefore(new TokenFilter(), UsernamePasswordAuthenticationFilter.class)
                .authorizeHttpRequests(
                        authorizeRequests -> authorizeRequests
                                .requestMatchers("/api/**/auth/**")
                                .authenticated()
                                .anyRequest().permitAll()
                )
                .headers(
                        httpSecurityHeadersConfigurer -> httpSecurityHeadersConfigurer
                                .frameOptions(HeadersConfigurer.FrameOptionsConfig::disable)
                )
                .exceptionHandling((exceptionConfig) -> {
                    exceptionConfig.accessDeniedHandler(accessDeniedHandler);
                    exceptionConfig.authenticationEntryPoint(authenticationEntryPoint);
                });

        return http.build();
    }
}

이게 완성된 내 security config 파일이다


이런 느낌으로 원하는대로 잘 떨어진다.

profile
이렇게 짜면 요구사항이나 기획이 변경됐을 때 불편하지 않을까? 라는 생각부터 시작해 설계를 해나가는 개발자

0개의 댓글