Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[2차 과제] 트러블 슈팅 - 성준 #143

Open
sjk4618 opened this issue Jan 23, 2025 · 0 comments
Open

[2차 과제] 트러블 슈팅 - 성준 #143

sjk4618 opened this issue Jan 23, 2025 · 0 comments

Comments

@sjk4618
Copy link
Collaborator

sjk4618 commented Jan 23, 2025

서버 트러블 슈팅 - 성준 filter

Custom Filter

💡

케이키는 Auth 쪽 요구사항에서 크게 3가지 api로 나뉜다.

  • 로그인이 필수인 api - 찜 기능, 마이페이지 관련 등등
  • 로그인 여부에 따라 결과값이 달라지는 api - 케이크 조회, 스토어 조회 등등(로그인 여부에 따라 좋아요 하트 표시가 달라짐)
  • 로그인이 상관없는 api - 좌표 조회, 지하철역 조회 등등

위와 같은 요구사항을 만족시키기 위해서 처음에는 argument resolver를 사용해서 나누어서 구현하려고 하였다.하지만 argument resolver까지 가기 전인 filter 단에서 처리하면 더 좋다고 판단하였고, customFilter를 두가지(로그인 필수 api, 로그인 여부에 따라 달라지는 api) 만들어서 구현하였다.

처음에는 spring security를 이용해서 securityConfig 안에서 custom filter를 구현하고, 원하는 api uri를 해당하는 custom filter에 적용시켰다. 하지만 원하는 대로 custom filter들에게 uri가 적용되지 않았다.

그래서 여러 군데 검색도 하고 알아보니까, SecurityConfig는 요청이 필터 체인을 통과하기 전에 모든 필터가 한 번씩 실행되도록 설계되어 있었다. 또한 SecurityConfig에 필터를 추가하면, 해당 필터는 Spring Security의 체인 내부에서 동작하게 된다. 이는 특정 API 경로에만 필터를 선택적으로 적용하는 것을 어렵게 만든다고 한다.

그래서 위 요구사항에 맞게 custom filter를 사용하려면 spring security의 filter chain이 아닌, 서블릿 컨테이너 수준에서 작동되는 filter chain에서 custom filter를 만들어야했다. 이때 FilterRegistrationBean 를 사용하여 filter를 등록하고, addUrlPatterns() 를 이용해서 원하는 url을 원하는 filter에 넣었다. setOrder는 filter의 우선순위를 설정하는데, 현재 요구사항에서는 해당 url들이 각각의 해당하는 customfilter에 들어가기 때문에 상관없어서 아무 숫자나 넣었다. 그리고 addUrlPatterns()만 사용하였을때, 원하는 대로 완벽하게 각각의 customFilter에 잘 들어가지 않았다. 그래서 shouldNotFilter 도 사용하여서 각 custom filter의 해당하지 않는 url들을 넣어주었다. 그랬더니 잘 작동하였다.
또한 모든 url들은 우선 securityFilter를 통과하게 permitAll 을 하였다.

아래는 구현 코드이다.

spring security에 관한 securtyConfig는 다 기본 세팅만 해두고, 나머지 customfilter는 filterConfig에서 설정하였다.

securtyConfig

@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig {

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        return http.csrf(AbstractHttpConfigurer::disable)
                .formLogin(AbstractHttpConfigurer::disable)
                .httpBasic(AbstractHttpConfigurer::disable)
                .sessionManagement(
                        session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
                .authorizeHttpRequests( auth -> auth.anyRequest().permitAll())
                .build();
    }
}

filterConfig

@Configuration
@RequiredArgsConstructor
public class FilterConfig {
    private final OptionalAuthenticationFilter optionalAuthenticationFilter;
    private final RequiredAuthenticationFilter requiredAuthenticationFilter;

    @Bean
    public FilterRegistrationBean<OptionalAuthenticationFilter> optionalAuthenticationFilterRegistration() {
        FilterRegistrationBean<OptionalAuthenticationFilter> registrationBean = new FilterRegistrationBean<>();
        registrationBean.setFilter(optionalAuthenticationFilter);

        // 필수 아닌 거
        registrationBean.addUrlPatterns(
                "/api/v1/cake/rank",
                "/api/v1/store/latest/*",
                "/api/v1/store/popularity/*",
                "/api/v1/cake/station/latest/*",
                "/api/v1/cake/station/popularity/*",
                "/api/v1/store/select/*",
                "/api/v1/cake/latest/*",
                "/api/v1/cake/popularity/*",
                "/api/v1/cake/select/*",
                "/api/v1/store/design/*"
                );

        /// 필터 우선순위 설정
        registrationBean.setOrder(1);

        return registrationBean;
    }

    @Bean
    public FilterRegistrationBean<RequiredAuthenticationFilter> requiredAuthenticationFilterRegistration() {
        FilterRegistrationBean<RequiredAuthenticationFilter> registrationBean = new FilterRegistrationBean<>();
        registrationBean.setFilter(requiredAuthenticationFilter);

        // 필수
        registrationBean.addUrlPatterns(
                "/api/v1/store/likes/latest/*",
                "/api/v1/store/likes/popularity/*",
                "/api/v1/store/likes/coordinate",
                "/api/v1/cake/store/likes/cake/latest/*",
                "/api/v1/cake/store/likes/cake/popularity/*",
                "/api/v1/store/likes/*",
                "/api/v1/cake/likes/*",
                "/api/v1/cake/likes/latest/*",
                "/api/v1/cake/likes/popularity/*",
                "/api/v1/user/name-email",
                "/api/v1/user/logout"
        );

        registrationBean.setOrder(2);

        return registrationBean;
    }
}

OptionalAuthenticationFilter - 로그인 여부에 따라 달라지는 api filter

@Component
@RequiredArgsConstructor
public class OptionalAuthenticationFilter extends OncePerRequestFilter {
    private final JwtProvider jwtProvider;

    private static final String ACCESS_TOKEN = "accessToken";

    // 필터를 건너뛸 API 경로 목록
    private static final List<String> EXCLUDED_PATHS = List.of(
            "/api/v1/store/likes/latest/*",
            "/api/v1/store/likes/popularity/*",
            "/api/v1/store/likes/coordinate",
            "/api/v1/cake/store/likes/cake/latest/*",
            "/api/v1/cake/store/likes/cake/popularity/*",
            "/api/v1/store/likes/*",
            "/api/v1/cake/likes/*",
            "/api/v1/cake/likes/latest/*",
            "/api/v1/cake/likes/popularity/*",
            "/api/v1/user/name-email",
            "/api/v1/user/logout",

            "/api/v1/store/rank",
            "/api/v1/store/coordinate-list/*",
            "/api/v1/store/station",
            "/api/v1/store/*/select/coordinate",
            "/api/v1/store/*/size",
            "/api/v1/store/*/information",
            "/api/v1/store/*/kakaoLink"
    );

    @Override
    protected boolean shouldNotFilter(HttpServletRequest request) throws ServletException {
        String requestURI = request.getRequestURI();
        // 요청 경로가 제외 목록에 포함되어 있는지 확인
        return EXCLUDED_PATHS.contains(requestURI);
    }

    @Override
    protected void doFilterInternal(
            @NonNull HttpServletRequest request,
            @NonNull HttpServletResponse response,
            @NonNull FilterChain filterChain
    ) throws ServletException, IOException {

        final String accessToken = getAccessTokenFromCookie(request);
        if (accessToken != null) {
            final Long userId = jwtProvider.getUserIdFromSubject(accessToken);
            SecurityContextHolder.getContext().setAuthentication(new UserAuthentication(userId, null, null));
        } else {
            SecurityContextHolder.getContext().setAuthentication(new UserAuthentication(null, null, null));
        }

        filterChain.doFilter(request, response);
    }

    public String getAccessTokenFromCookie(@NonNull HttpServletRequest request) {
        final Cookie[] cookies = request.getCookies();
        if (cookies != null) {
            for (Cookie cookie : cookies) {
                if (ACCESS_TOKEN.equals(cookie.getName())) {
                    return cookie.getValue();
                }
            }
        }
        return null;
    }
}

RequiredAuthenticationFilter - 로그인 필수 api filter

@Component
@RequiredArgsConstructor
public class RequiredAuthenticationFilter extends OncePerRequestFilter {
    private final JwtProvider jwtProvider; 
    private final ObjectMapper objectMapper;

    // 필터를 건너뛸 API 경로 목록
    private static final List<String> EXCLUDED_PATHS = List.of(
            "/api/v1/cake/rank",
            "/api/v1/store/latest/*",
            "/api/v1/store/popularity/*",
            "/api/v1/cake/station/latest/*",
            "/api/v1/cake/station/popularity/*",
            "/api/v1/store/select/*",
            "/api/v1/cake/latest/*",
            "/api/v1/cake/popularity/*",
            "/api/v1/cake/select/*",
            "/api/v1/store/design/*",

            "/api/v1/store/rank",
            "/api/v1/store/coordinate-list/*",
            "/api/v1/store/station",
            "/api/v1/store/*/select/coordinate",
            "/api/v1/store/*/size",
            "/api/v1/store/*/information",
            "/api/v1/store/*/kakaoLink"

    );

    @Override
    protected boolean shouldNotFilter(HttpServletRequest request) throws ServletException {
        String requestURI = request.getRequestURI();
        // 요청 경로가 제외 목록에 포함되어 있는지 확인
        return EXCLUDED_PATHS.contains(requestURI);
    }

    @Override
    protected void doFilterInternal(
            @NonNull HttpServletRequest request,
            @NonNull HttpServletResponse response,
            @NonNull FilterChain filterChain
    ) throws ServletException, IOException {
        try {
            final String token = getAccessTokenFromCookie(request);
            final Long userId = jwtProvider.getUserIdFromSubject(token);
            SecurityContextHolder.getContext().setAuthentication(new UserAuthentication(userId, null, null));
            filterChain.doFilter(request, response); // 
        } catch (Exception e) {
		        //response 세팅
            final ErrorBaseCode errorCode = ErrorBaseCode.UNAUTHORIZED;

            response.setContentType(MediaType.APPLICATION_JSON_VALUE);
            response.setCharacterEncoding(Constant.CHARACTER_TYPE);
            response.setStatus(errorCode.getHttpStatus().value()); // HTTP 상태 코드 401 설정

            // `ApiResponseUtil.failure`를 이용해 응답 작성
            final PrintWriter writer = response.getWriter();
            writer.write(objectMapper.writeValueAsString(
                    ApiResponseUtil.failure(errorCode).getBody()
            ));
            writer.flush();
            return; // 체인 호출 중단
        }
    }

    private String getAccessTokenFromCookie(final HttpServletRequest request) throws Exception {
        Cookie[] cookies = request.getCookies();
        if (cookies != null) {
            for(Cookie cookie : cookies) {
                if (cookie.getName().equals("accessToken")) {
                    return cookie.getValue();
                }
            }
        }
        throw new UserBadRequestException(ErrorBaseCode.UNAUTHORIZED);
    }
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant