본문 바로가기
공부/프로그래밍

[spring oauth2 ResourceServer] oauth2 에서 CORS 설정 테스트

by demonic_ 2020. 4. 4.
반응형

 

CORS란?

교차 출처 리소스 공유(Cross-Origin Resource Sharing) 이라 불리며 다른 사이트에서 우리쪽 서버에 접근할 수 있는 권한을 부여하도록 브라우저에 알려주는 체제다. 대표적으로 모바일과 서버(back-end)가 그렇고, 요즘 Front-end 쪽에도 독립적으로 서버를 구성하는 경우가 있어 Oauth2를 사용한다면 반드시 알아두어야 할 점이다.

 

 

 

서비스를 구축하는 과정에서 cors 에러가 발생했고, 관련하여 정리하는 글이다.

 

아래는 브라우저에서 호출할 때 발생한 에러다

 

 

서버측 로그는 다음과 같다.

org.springframework.security.access.AccessDeniedException: Access is denied

          at org.springframework.security.access.vote.AffirmativeBased.decide(AffirmativeBased.java:84) ~[spring-security-core-5.2.2.RELEASE.jar:5.2.2.RELEASE]

 

 

검색하면 등록하는 방법이 2가지 있는데 Servlet단위의 Filter를 구현하는 방법과, WebMvcConfigurer와 ResourceServerConfigurerAdapter 내 설정을 하는 방법으로 나뉘었다.

 

그리고 테스트해본 결과, /oauth/token 을 호출해 토큰을 받는 과정에서 다음의 문제가 발생했다.

 

일단 인터넷에 나와 있는 방법을 보면 ResourceServerConfigurerAdapter 구현체에서 Option으로 들어오는 것을 permitAll 로, 그리고 WebMvcConfigurer의 구현체에서 allowedOrigins를 '*'(모두허용)으로 하라고 되어있다. 설정하면 다음과 같겠다.

 

(아래설정은 내 경우 실패했다)

@Configuration
@EnableResourceServer
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {

    @Override
    public void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .antMatchers(HttpMethod.OPTIONS).permitAll()  // 추가
                .antMatchers("/api/**").hasRole("USER")
                .anyRequest().authenticated();
    }
}
@Configuration
@EnableWebMvc
public class WebConfig implements WebMvcConfigurer {
    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry
                .addMapping("/**")
                .allowedOrigins("*")  // 추가
                .allowedMethods("*")  // 추가
        ;
    }
}

 

그런데 막상 해보면 계속 막힌다. 정확히는 Access is denied 에러가 계속 나는 것. 그래서 다른 방법인 Filter를 등록해봤더니 무사히 테스트가 통과된다.

 

(위에서 ResourceServerConfig에 '추가'로 적은 부분 제거, WebConfig를 제거하고 난 후, 아래 클래스를 새로 등록하니 성공했다)

package ko.demo.oauth.config.oauth2.resource;//package com.kjobcor.website.config.security.oauth2.resource;

import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;

import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
 * CORS 설정
 */
@Component
@Order(Ordered.HIGHEST_PRECEDENCE)
public class CorsFilter implements Filter {
    @Override
    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", "*");
        response.setHeader("Access-Control-Allow-Credentials", "true");
        response.setHeader("Access-Control-Allow-Methods","*");
        response.setHeader("Access-Control-Max-Age", "3600");
        response.setHeader("Access-Control-Allow-Headers",
                "Origin, X-Requested-With, Content-Type, Accept, Key, Authorization");

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

 

 

왜 그런걸까 생각해보니 option에 대한 접근의 문제였다.

 

우선 /oauth/token 으로 호출을 했을 떄는 POST로 호출하더라도 HttpMethod 는 Options로 된다. 문제는 이 Options가 WebMvcConfigurer에서 .allowedOrigins("*") 로 설정한대로 모두 허용했음에도 불구하고, 자꾸 Access is denied 된다는 것이다.

 

실제로 위에서 구현한 CorsFilter 클래스를 사용하고 나서 부터는 Filter를 수행하지 않는다. 중간부분에 있는 IF문이 필터수행을 하지 않도록 유도하기 떄문이다.

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

 

 

그리고 하나 더 알아낸 것은 CorsFilter의 파일 내에 response.setHeader("Access-Control-Allow-Origin", "*"); 을 주석처리 하게되면 똑같은 에러가 발생한다. 호출하는 도메인이 아닌 다른걸 등록해도 마찬가지다.

# 주석했을때 반응

Access to XMLHttpRequest at 'http://localhost:8080/oauth/token' from origin 'http://localhost:3000' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource.

# 다른 도메인을 등록했을때 반응

Access to XMLHttpRequest at 'http://localhost:8080/oauth/token' from origin 'http://localhost:3000' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: The 'Access-Control-Allow-Origin' header has a value 'http://localhost:3001' that is not equal to the supplied origin.

 

그래서 WebMvcConfigurer를 구현한 WebConfig 파일 내 설정이 제대로 작동하지 않거나, 내 의도와는 다르게 작동된다고 생각했다. 그런데 로그를 자세히 살펴보니 Access is denied 바로 위에 다음의 로그가 찍혀있다.

Voter: org.springframework.security.web.access.expression.WebExpressionVoter@21ff7e68, returned: -1
Access is denied (user is anonymous); redirecting to authentication entry point

즉 익명의 유저로 접근하다가 Access is denied 당하는 거였다. 살펴보면 CorsFilter 에서 IF문으로 filter를 넘기게 되어있는데, 이것때문에 Access is denied를 안당한 거였지, 저것만 제거되면 사실 똑같은 입장인 샘이다.

 

ResourceServerConfig 에서 OPTIONS를 permitAll을 줬음에도 불구하고 이런 현상이 일어나는 이유가 무엇인지 아직 잘 모르겠다.

 

아 그리고 이런 현상이 일어나는 이유는 에러표시에도 나와있듯이 response 의 http status가 정상이 아니기 때문이다.

(It does not have HTTP ok status.) 화면이나 콘솔에서 확인되진 않았지만 Access is denied 이기에 401에러가 리턴된 것이다.

 

그래서 관련 테스트를 한다면 다음과같이 작성하면 되지 않을까 싶다.

@ExtendWith(SpringExtension.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT)
@AutoConfigureMockMvc
public class CorsFilterTest {

    @Autowired
    MockMvc mockMvc;
    @Autowired
    TestRestTemplate restTemplate;
    @Value("${local.server.port:8080}")
    int localServerPort;

    @Test
    @DisplayName("CORS Filter 테스트")
    void corsFilterTest() throws Exception {
        // given
        String url = "/oauth/token";

        // when - option 전송
        MvcResult mvcResult = mockMvc.perform(options(url)
                        .header("Access-Control-Request-Method", "OPTIONS")
                        .header("Origin", "http://localhost:3000")
        )
                .andReturn();

        int status = mvcResult.getResponse().getStatus();
        Assertions.assertThat(status).isEqualTo(200);

        // when - post 전송
        MvcResult mvcResult2 = mockMvc.perform(post(url)
                        .header("Access-Control-Request-Method", "POST")
                        .header("Origin", "http://localhost:3000")
        )
                .andReturn();

        int status2 = mvcResult2.getResponse().getStatus();
        Assertions.assertThat(status2).isEqualTo(401);

        System.out.println("=========================");
        System.out.println("option status => " + statusOption);
        System.out.println("post status => " + statusPost);
        System.out.println("=========================");
    }
}

테스트 실행결과

 

끝.

 

 

관련글:

https://lemontia.tistory.com/927

 

[springboot, oauth] Authorization Server(인증서버) 구축하기

OAuth2 역할은 크게 4가지로 분류된다 - Resource Owner - Authorization Server - Resource Server - Client :: Resource Owner는 유저를 뜻한다. 내가 만든 서비스를 이용하고자 하는 고객을 의미한다 :: Author..

lemontia.tistory.com

https://lemontia.tistory.com/928

 

[springboot, oauth] Resource Server(자원서버) 구축하기

이전 포스팅에 이어 자원서버 구축하는 과정이다. Authorization Server(인증서버) 구축하기 https://lemontia.tistory.com/927 [springboot, oauth] Authorization Server(인증서버) 구축하기 OAuth2 역할은 크게..

lemontia.tistory.com

 

 

 

 

참조:

https://taes-k.github.io/2019/12/05/spring-cors/

 

Spring OAuth2 사용시 cors 설정

CORS Cross Origin Resource Sharing(CORS)는 이름에서 알 수 있듯이 Origin (출처)를 교차하여 자원을 공유한다는 뜻으로, 도메인혹은 포트가 다른 서비스의 자원을 요청하는 내용을 담고 있습니다. 일반적으로 자바스크립트에서는 Same origin policy (동일 출처 정책) 보안 정책을 가지고있어, 다른 Origin에 접근시 No Access-Control-Allow-Origin Header라는 오류를 노출하게 되어 있

taes-k.github.io

 

 

 

반응형

댓글