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
https://lemontia.tistory.com/928
참조:
https://taes-k.github.io/2019/12/05/spring-cors/
'공부 > 프로그래밍' 카테고리의 다른 글
[springboot] ControllerAdvice 응용해 return 꾸미기(HttpStatus 지정 포함) (0) | 2020.04.11 |
---|---|
[mysql] REPEATABLE-READ에서 dead lock이 걸린 이유 (0) | 2020.04.09 |
[DBMS] 트랜잭션 격리수준 (isolation level) (0) | 2020.04.03 |
[querydsl, mysql] DATE_FORMAT 등 이용해 groupby 사용하기 (0) | 2020.04.02 |
[springboot, oauth] Resource Server(자원서버) 구축하기 (0) | 2020.03.29 |
댓글