Trouble
쿠키인증 방식일 때 브라우저가 인증Cookie를 포함해서 전송하지 않는다. 따라서 login 후 모든 요청에 대해서 인증에러가 발생한다.
처음에는 파이어폭스에서, 그 다음은 크롬에서, 현재는 IE까지 발생.
개발환경
서버 : ASP.NET WebAPI, System.Web.Security.Membership을 이용한 인증 ( Cookie 기반인증 )
클라이언트: angularJS를 이용한 Single Page Application
방법
ASP.NET WebAPI에 ASP.NET 2.0 시절의 예전 방식의 인증을 적용하여 WebAPI에 로그인을 하면, aspxauth 명으로 인증쿠키을 돌려 준다. 이후의 비동기 XMLHttpRequest(Ajax 방식) 들 모두 인증여부를 체크하여야 보안을 해주어야 한다.
비동기 XMLHttpRequest는 보안상 위험이 크기 때문에 최근 브라우져들은 보안을 위해 쿠키를 주고 받거나 same-origin security policy를 적용하고 있다.
same-origin security policy는 프로토콜, 호스트, 포트로 구성되는 origin 정보(서버 주소)가 같을 때만 XMLHtpRequest를 허용한다는 것이다.이는 WebService나 Firebase나 MongoLab 등을 이용해 Web서버와 WebAPI서버를 따로 구성해야 하는 상황에서 곤란한 제약조건이다.
즉 Web서버에 있는 자바스크립트가 WebAPI서버에 접근할 수 없는 것이다.
이러한 상황을 위한 해결법은 2가지로 JSONP를 이용하거나 CORS(Cross-origin resource sharing) 방법이 있는데, JSONP는 GET 요청만 사용할 수 있고, 보안에 취약하며 오류처리가 복잡하기 때문에 CORS 방식을 사용한다.
CORS는 W3C 명세로 브라우저와 서버가 잘 협력해서 적절한 요청과 응답 헤더를 보냄으로서 sam-origin security policy를 만족할 수 있게 해준다. 첫번째로 브라우저는 GET, HEAD 메서드 외의 요청인 경우, 탐색용(preflight) OPTIONS 메서드 요청을 보낸다. 서버는 이 요청을 받을 수 있도록 셋팅이 되어 있어야 하며, 허용하는 Origin, Method, Header를 브라우저에게 다시 알려 줘야 한다. 브라우저는 이 허가정보를 받고 허가된 요청만 할 수 있다.
따라서 서버는 GET, POST, HEAD 메서드 이외의 메서드에 대해서 모두 OPTIONS 요청을 받을 수 있는 API를 추가로 구성해야 한다.
서버가 보내주는 허가정보는 아래와 같다. 주의할 점은 Access-control-Allow-Headers: * 식으로는 에러가 발생한다. Content-type을 명시적으로 작성해 주어야 한다.
Access-Control-Allow-Credentials: true
Access-Control-Allow-Headers: Accept, Origin, X-Requested-With, Content-Type
Access-Control-Allow-Methods: POST, GET, PUT, DELETE
Access-Control-Allow-Origin: http://localhost:9000
또한 최신 브라우저는 Cookie를 기본적으로는 동기(synchronous) XMLHttpRequest일 경우에만 자동으로 요청시 포함해 준다.
따라서 Cookie 인증 방식일 경우 Login 후 비동기 XMLHttpRequest를 보내면 인증오류가 나게 된다.
비동기 XMLHttpRequest에 cookie를 포함시켜 요청하려면 요청시 withCredentials를 True로 설정해서 브라우져에게 알려줘야 한다. 주의할 점을 이 경우는 동기 XMLHttpRequest시 cookie를 포함시키지 않는다는 것이다.( 직접 해보진 않았음 ) 따라서 동기와 비동기 XMLHttpRequest를 혼합해서 프로그램을 만들지 말아야 한다.
결론
# Client에서 자바스크립트로 XMLHttpRequest 시 withCredentials를 True로 설정 ( 초기에 config로 설정 시 모든 요청에 적용 )
app.config(['$httpProvider',function($httpProvider){
$httpProvider.defaults.withCredentials = true;
}]);
# 서버에서 응답 HTTP HEADER에 허가 정보 추가. ( Web.config 파일에 설정시 전체 적용, 보다 다양한 설정법은 아래 참고링크 참조 )
<system.webServer>
<!-- CORS를 위한 설정 -->
<httpProtocol>
<customHeaders>
<add name="Access-Control-Allow-Credentials" value="true"/>
<add name="Access-Control-Allow-Origin" value="http://localhost:9000"/>
<add name="Access-Control-Allow-Headers" value="Accept, Origin, X-Requested-With, Content-Type"/>
<add name="Access-Control-Allow-Methods" value="POST, GET, PUT, DELETE"/>
</customHeaders>
</httpProtocol>
<!-- CORS -->
</system.webServer>
# 서버 WebAPI에 GET, HEAD 메서드 외의 응답Controller에 OPTIONS 메서드를 위한 Controller에 Action 추가.
public HttpResponseMessage Options()
{
var rsp = new HttpResponseMessage();
rsp.StatusCode = HttpStatusCode.OK;
return rsp;
}
# 서버 WebAPI에서 OPTIONS 메서드를 위한 Action을 모든 Action에 대해서 생성해 주지 않고 아래 함수를 Global.asax파일에 지정하여 한 번에 처리하는 방법도 있다. 하지만 보안적인 부분을 염두해 둬야 할 듯.
protected void Application_BeginRequest(object sender, EventArgs e)
{
HttpContext.Current.Response.AddHeader("Access-Control-Allow-Origin", "*");
if (HttpContext.Current.Request.HttpMethod == "OPTIONS")
{
HttpContext.Current.Response.AddHeader("Access-Control-Allow-Methods",
"GET, POST, PUT, DELETE");
HttpContext.Current.Response.AddHeader("Access-Control-Allow-Headers",
"Content-Type, Accept");
HttpContext.Current.Response.End();
}
}
참고
CORS 에 대해서( https://developer.mozilla.org/ko/docs/Web/HTTP/Access_control_CORS )
ASP.NET Web API 2 에서 CORS 허용하기
/ 한글 http://www.egocube.pe.kr/Translation/Content/asp-net-web-api/201402100001#enable-cors )
( http://www.jefclaes.be/2012/09/supporting-options-verb-in-aspnet-web.html )