Web API : 사용자 인증에 관한 메모

[제목] Web API : 사용자 인증에 관한 메모

웹서비스를 개발하는 방식으로 최근 각광을 받고 있는 기술 중의 하나로 RESTful Web Service를 들 수 있다. 그리고 .NET에서 RESTful Web Service를 개발하는 방식으로는 WCF REST 혹은 ASP.NET WebAPI 가 일반적이다. 이 노트에서 REST 서비스를 개발함에 있어 공통적으로 경험할 수 있는 몇가지 문제들을 정리하고자 한다.

REST 웹서비스를 개발할 때, 어떤 웹 클라이언트가 웹서비스를 이용하는지에 따라 고려할 사항이 있다. 만약 웹서비스를 이용하는 클라이언트가 동일한 도메인 하에 있는 다른 Web Site 혹은 다른 Web Service 이라면 REST API 호출에 문제가 없다. 하지만 다른 도메인에 있는 웹사이트의 웹페이지에서 AJAX로 REST 웹서비스를 호출할  경우 Same Origin Policy에 위반되기 때문에 문제가 발생한다. REST API를 제공하는 경우 일반적으로 파트너 혹은 고객들이 직접 외부에서 호출할 수 있도록 허용해 주는 경우가 많다. 따라서 REST API 개발시 CORS(Cross Origin Resource Sharing)를 Enable 해주는 코드를 별도 넣어 주어야 한다. WebAPI에서 이러한 기능을 편리하게 코딩하기 위해서 ASP.NET Web API Cross-Origin Resource Sharing NuGet 패키지를 활용할 수 있다. CORS를 사용하면 어떤 웹사이트에서만 접근할 수 있는지를 제어할 수 있으며, 특정 메서드 (POST, GET 등)만을 허용할 수도 있다. CORS를 지원하는 웹브라우져는 Web Request를 모두 REST 서버로 보내지만, 웹서버로부터 돌아오는 Response를 차단하게 된다.

Same Origin Policy는 웹브라우져의 Security Feature이므로, 만약 클라이언트가 Desktop이거나 모바일 Native App인 경우 이러한 제약을 받지 않는다. 예를 들어, WebClient, HttpWebRequest, HttpClient 같은 .NET의 HTTP API들을 사용하여 직접 REST API를 호출하는WinForm Application의 경우, 별도의 RESP API CORS 코딩 없이 REST 서비스를 제공할 수 있다.

REST 웹서비스를 개발할 때 또 다른 주목할 만한 사항은 사용자 인증에 관한 것이다. REST API가 제공하는 데이타가 Public 데이타가 아닌 경우 대부분 사용자 인증을 필요로 하는데, 이를 위해 여러 가지 방법이 사용될 수 있다. 만약 웹서비스를 호출하는 웹페이지가 (Local Storage가 아닌) 웹서버 상에 있고 이를 웹클라이언트가 호출하는 방식이라면, ASP.NET Forms Authentication을 사용할 수 있다. 예를 들어, Web API의 로그인 메서드에서 아래와 같이 Forms Authentication을 사용하여 세션을 생성하고, 사용자에게 1일간 사용을 허락하는 인증 쿠키를 보낼 수 있다.

// 로그인 (Forms Authentication)
FormsAuthentication.SetAuthCookie(username, true);
HttpCookie cookie = FormsAuthentication.GetAuthCookie(username, true);
cookie.Expires = DateTime.Now.AddDays(1);

// 로그오프
FormsAuthentication.SignOut();				
if (session != null) {
	session.RemoveAll();
	session.Abandon();
}       

위의 Web API는 웹사이트 서버에서 뿐만 아니라, Desktop Application에서 HTTP 클라이언트 API들을 사용하여 호출할 수도 있다. 단, Desktop Application에서 사용자 인증이 필요한 REST API를 호출하기 위해서는 Forms Authentication과 관련된 세션 쿠키를 개발자가 넣어 주어야 한다. 이는 웹브라우져가 쿠키를 자동으로 넣어주는 것과 다른 점이다. 아래 예제는 로그인시 세션 값을 받아 (여기서는 직접 리턴값으로 받고 있다고 가정) 이 값을 저장해 두었다가 다음 API 호출에서 헤더에 세션 쿠키로 쓰고 있는 것을 보여준다.

private void btnLogin_Click(object sender, EventArgs e)
{
	string url = "http://test.com/api/login";
	string u = txtUser.Text;
	string p = txtPwd.Text;            

	using (var wc = new WebClient())
	{
		wc.Headers[HttpRequestHeader.ContentType] = "application/json";

		var usr = new UserInfo() { Username = u, Password = p };
		string jsonData = JsonConvert.SerializeObject(usr);
		var result = wc.UploadString(url, jsonData);
		
		//로그인 후 Access Token 받아 저장
		//차후 REST API 호출시 사용
		this._token = JsonConvert.DeserializeObject(result);                
		txtResult.Text = result;
	}            
}

private void btnGetData_Click(object sender, EventArgs e)
{
	string url = "http://test.com/api/account/1";
	using (var wc = new WebClient())
	{
		wc.Headers[HttpRequestHeader.ContentType] = "application/json";
		//로그인시 전달받은 쿠키 사용
		// .ASPXAUTH는 ASP.NET Forms Authentication에서 사용하는
		// 쿠키명이다.
		wc.Headers[HttpRequestHeader.Cookie] = ".ASPXAUTH=" + this._token;
		var result = wc.DownloadString(url);		
		txtResult.Text = result;
	} 
}

그런데 이러한 방식이 항상 통하는 것은 아니다. 만약 웹페이지를 로컬 머신에 두고 여기에서 JavaScript로 AJAX를 통해 REST API를 호출한다면 (그리고 웹브라우져를 통해 REST API를 호출하게 된다면), Forms Authentication을 사용할 수 없다. 예를 들어 Sencha Touch와 같은 Mobile Web App에서 이러한 경우가 발생할 수 있다. 이러한 문제가 발생하는 이유는 웹브라우져에서 JavaScript를 이용하여 임의로 쿠키를 추가할 수 없기 때문이다. 따라서 이러한 경우 Web API 서버상에서 해당 사용자의 토큰(보통 Encrypt됨)을 생성해서 이를 컬렉션으로 서버 캐쉬에 관리하는 방식을 사용할 수 있다. 그리고 이 토큰을 웹브라우져에 전달하고, 다시 JavaScript에서 이를 저장 차후 REST 호출에서 이 토큰을 HTTP Request 헤더에 추가하여 보내게 된다. Web API 에서는 이 Request에서 토큰 헤더를 읽어 들어 캐쉬 컬렉션에 저장된 토큰과 비교하여 인증하게 된다. 나아가 만약 Encrypt를 해쉬가 아닌 AES 같은 방식을 사용한다면 쉽게 Decrypt하여 사용자ID 같은 정보를 찾아낼 수 있다. 이러한 토큰 인증을 보다 쉽게 하기 위하여, Web API에 Built-in된 표준 Authorization Filter인 Authorize 대신에 아래와 같은 AuthorizeToken 필터를 정의할 수 있다.

public class AuthorizeTokenAttribute : AuthorizationFilterAttribute
{
	public static readonly string AuthToken = "AUTHTOKEN";        
	public override void OnAuthorization(HttpActionContext actionContext)
	{
		if (actionContext != null)
		{
			var headers = actionContext.ControllerContext.Request.Headers;
			if (headers.Contains(AuthToken))
			{
				var token = headers.GetValues(AuthToken).FirstOrDefault();
				if (token != null && VerifyToken(token) == true)
				{
					return; // success
				}
			}
		}
		// if failed, return Unauthorized
		actionContext.Response = new HttpResponseMessage(HttpStatusCode.Unauthorized)
		{
			RequestMessage = actionContext.ControllerContext.Request
		};
	}
}

그리고 이 필터는 다시 전체 ApiController, 특정 ApiController 혹은 특정 메서드 등을 대상으로 적용할 수 있다. 아래 예제는 AccountController 클래스에 AuthorizeToken을 적용한 것으로 만약 토큰이 없거나 Bad Token일 경우 메서드를 실행하지 않고 Unauthorized를 리턴하게 된다.

[AuthorizeToken]
public class AccountController : ApiController
{
   //...
}



본 웹사이트는 광고를 포함하고 있습니다. 광고 클릭에서 발생하는 수익금은 모두 웹사이트 서버의 유지 및 관리, 그리고 기술 콘텐츠 향상을 위해 쓰여집니다.