넘블 회고록

appti·2022년 12월 1일
0

TIL

목록 보기
1/1

CORS

다른 문제는 큰 문제 없이 해결했으나, 프론트엔드와의 연동 중 CORS로 인해 상당히 많은 시간을 소모해야 했습니다.

쿠키 사용 방식

Spring Session을 통해 RedisSession을 관리하고, Session ID는 기본 설정값인 SESSION이라는 이름의 쿠키로 전달하는 방식으로 진행했습니다.

@Component
@Order(Ordered.HIGHEST_PRECEDENCE)
public class CorsFilter implements Filter {

    public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
        HttpServletResponse response = (HttpServletResponse) res;
        HttpServletRequest request = (HttpServletRequest) req;
        response.setHeader("Access-Control-Allow-Origin", "http://localhost:3000");
        response.setHeader("Access-Control-Allow-Credentials", "true");
        response.setHeader("Access-Control-Allow-Methods","*");
        response.setHeader("Access-Control-Max-Age", "36000");
        response.setHeader("Access-Control-Allow-Headers",
            "Origin, X-Requested-With, Content-Type, Accept, Key");

        if ("OPTIONS".equalsIgnoreCase(request.getMethod())) {
            response.setStatus(HttpServletResponse.SC_OK);
        } else {
            chain.doFilter(req, res);
        }
    }

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
    }

    @Override
    public void destroy() {
    }
}

현재 Spring Security OAuth2 Client로 소셜 로그인을 처리하고 있었기 때문에 위와 같이 별도의 Filter를 만들어 @Order(Ordered.HIGHEST_PRECEDENCE)를 통해 최우선적으로 요청이 해당 필터를 거쳐가도록 설정했습니다.

@Configuration
public class SessionConfig {

    @Bean
    public CookieSerializer cookieSerializer() throws MalformedURLException {
        DefaultCookieSerializer serializer = new DefaultCookieSerializer();
        serializer.setCookieName("SESSION");
        serializer.setSameSite("None");
        serializer.setUseSecureCookie(true);
        return serializer;
    }

}

CookieSerializer를 통해 기본적인 쿠키 설정을 해주었습니다.

SameSite=Lax

serializer.setSameSite("None")를 통해 쿠키 정책을 변경하고자 했고, 이를 위해 serializer.setUseSecureCookie(true)를 통해 쿠키를 secure로 설정하고자 했습니다.

그런데 여기서 HTTPS가 아닌 HTTP로 통신을 진행하고 있어 문제가 발생했습니다.

serializer.setUseSecureCookie(true)의 경우 HTTPS에서만 적용될 수 있기 때문에SameSite=None으로 설정할 수가 없었고, SameSite=Lax가 적용되어 프론트엔드에서는 쿠키를 전달받을 수 없었습니다.

이전 프로젝트에서는 HTTPS 환경에서 작업했기 때문에 크게 신경쓰지 않았으나 지금 환경에서는 큰 걸림돌이 되었습니다.

이로 인해 다른 방법을 찾아봤으나, 결국 쿠키 방식은 포기하고 HeaderSession ID를 넘겨주는 방식으로 변경했습니다.

헤더 사용 방식

@Configuration
public class SessionConfig {

    @Bean
    public HttpSessionIdResolver httpSessionStrategy() {
        return HeaderHttpSessionIdResolver.xAuthToken();
    }
}

HeaderHttpSessionIdResolver를 통해 X-Auth-Token으로 헤더로 전송하고자 했습니다.

그런데 여기서 또 프론트엔드가 해당 헤더에 접근할 수 없는 문제가 발생했습니다.

프론트엔드에서의 헤더 접근

@Component
@Order(Ordered.HIGHEST_PRECEDENCE)
public class CorsFilter implements Filter {

    public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
        HttpServletResponse response = (HttpServletResponse) res;
        HttpServletRequest request = (HttpServletRequest) req;
        response.setHeader("Access-Control-Allow-Origin", "http://localhost:3000");
        response.setHeader("Access-Control-Allow-Credentials", "true");
        response.setHeader("Access-Control-Allow-Methods","*");
        response.setHeader("Access-Control-Max-Age", "36000");
        response.setHeader("Access-Control-Allow-Headers",
            "Origin, X-Requested-With, Content-Type, Accept, Key, X-Auth-Token");

        if ("OPTIONS".equalsIgnoreCase(request.getMethod())) {
            response.setStatus(HttpServletResponse.SC_OK);
        } else {
            chain.doFilter(req, res);
        }
    }

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
    }

    @Override
    public void destroy() {
    }
}

위와 같이 Access-Control-Allow-HeadersX-Auth-Token에 대한 접근을 허용하고 있다고 생각했기 때문에 계속 방법을 찾기는 했으나 결과가 잘 나오지 않았습니다.

그러던 도중 프론트엔드 개발자분께서 Access-Control-Expose-Headers를 언급해주셨고, 제가 Access-Control-Allow-Headers에 대해 착각하고 있다는 것을 깨닫게 되었습니다.

Access-Control-Allow-Headers의 경우 preflight에 관련된 설정이였으며, Access-Control-Expose-Headers가 프론트엔드에서 접근할 수 있는 헤더의 종류를 허용해주는 역할이였습니다.

@Component
@Order(Ordered.HIGHEST_PRECEDENCE)
public class CorsFilter implements Filter {

    public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
        HttpServletResponse response = (HttpServletResponse) res;
        HttpServletRequest request = (HttpServletRequest) req;
        response.setHeader("Access-Control-Allow-Origin", "http://localhost:3000");
        response.setHeader("Access-Control-Allow-Credentials", "true");
        response.setHeader("Access-Control-Allow-Methods","*");
        response.setHeader("Access-Control-Max-Age", "36000");
        response.setHeader("Access-Control-Allow-Headers",
            "Origin, X-Requested-With, Content-Type, Accept, Key, X-Auth-Token");
        response.setHeader("Access-Control-Expose-Headers",
            "Origin, X-Requested-With, Content-Type, Accept, Key, X-Auth-Token");

        if ("OPTIONS".equalsIgnoreCase(request.getMethod())) {
            response.setStatus(HttpServletResponse.SC_OK);
        } else {
            chain.doFilter(req, res);
        }
    }

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
    }

    @Override
    public void destroy() {
    }
}

그래서 위와 같이 Access-Control-Expose-Headers를 추가해 해결할 수 있었습니다.

헤더 이름 변경

프론트엔드에서 헤더의 이름을 변경해달라는 요청이 들어와, 이를 다음과 같이 처리했습니다.

@Configuration
public class SessionConfig {

    @Bean
    public HttpSessionIdResolver httpSessionStrategy() {
        return new HeaderHttpSessionIdResolver("x_auth_token");
    }
}

HeaderHttpSessionIdResolver.xAuthToken() 또한 내부적으로 X-Auth-Token이라는 상수를 통해 HeaderHttpSessionIdResolver를 생성하고 있었기 때문에 이 정도면 충분하다고 판단했고, 로컬에서 테스트 시 정상적으로 동작함을 확인할 수 있었습니다.

이후 배포 후 배포 환경에서 테스트를 해보자, Session을 찾지 못하고 401을 반환하는 오류를 확인했습니다.

이에 대해서 여러 시도를 해봤으나, 원인조차 찾지 못해 결국 기본 값인 HeaderHttpSessionIdResolver.xAuthToken()을 사용하는 것으로 롤백하게 되었습니다.

소셜 로그인

일반 로그인 시에는 정상적으로 동작하나, 소셜 로그인 시에는 CORS가 발생하는 현상이 발견되었습니다.

@Component
@Order(Ordered.HIGHEST_PRECEDENCE)
@WebFilter("/*")
public class CorsFilter implements Filter {

    public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
        HttpServletResponse response = (HttpServletResponse) res;
        HttpServletRequest request = (HttpServletRequest) req;
        response.setHeader("Access-Control-Allow-Origin", "http://localhost:3000");
        response.setHeader("Access-Control-Allow-Credentials", "true");
        response.setHeader("Access-Control-Allow-Methods","*");
        response.setHeader("Access-Control-Max-Age", "36000");
        response.setHeader("Access-Control-Allow-Headers",
            "Origin, X-Requested-With, Content-Type, Accept, Key");
        response.setHeader("Access-Control-Expose-Headers",
            "Origin, X-Requested-With, Content-Type, Accept, Key");

        if ("OPTIONS".equalsIgnoreCase(request.getMethod())) {
            response.setStatus(HttpServletResponse.SC_OK);
        } else {
            chain.doFilter(req, res);
        }
    }

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
    }

    @Override
    public void destroy() {
    }
}

이를 해결하기 위해 @WebFilter("/*")를 통해 Spring Security OAuth2 Client에서 자동으로 처리해주는 요청을 포함한 모든 요청이 Cors 설정 필터를 거치도록 설정해주었습니다.

profile
안녕하세요

0개의 댓글