보안: ASP.NET Web API와 개별 사용자 계정

등록일시: 2014-01-24 08:00,  수정일시: 2015-07-25 16:09
조회수: 10,151
이 문서는 ASP.NET Web API 기술을 널리 알리고자 하는 개인적인 취지로 제공되는 번역문서입니다. 이 문서에 대한 모든 저작권은 마이크로소프트에 있으며 요청이 있을 경우 언제라도 게시가 중단될 수 있습니다. 번역 내용에 오역이 존재할 수 있고 주석은 번역자 개인의 의견일 뿐이며 마이크로소프트는 이에 관한 어떠한 보장도 하지 않습니다. 번역이 완료된 이후에도 대상 제품 및 기술이 개선되거나 변경됨에 따라 원문의 내용도 변경되거나 보완되었을 수 있으므로 주의하시기 바랍니다.

본문에서는 로컬 데이터베이스에서 관리되는 개별 사용자 계정으로 Web API를 인증하는 방법을 살펴봅니다. 기본적으로 사용자 프로필은, 운영 사이트의 SQL Server나 Windows Azure SQL 데이터베이스에 배포 가능한, SQL Server LocalDB 데이터베이스에 저장됩니다.

본 자습서에서는 피들러 웹 디버깅 도구를 사용해서 Web API에 HTTP 요청을 전송합니다. 이 방법을 사용하면 사용자 인증 및 권한 부여 과정에 사용되는 Raw HTTP 메시지들을 여과 없이, 있는 그대로 살펴볼 수 있습니다. 물론, 클라이언트 응용 프로그램을 작성할 때는, 적절한 HTTP 클라이언트 라이브러리를 사용해서 HTTP 요청을 생성하게 될 것입니다. 가령, 웹 클라이언트를 위한 XMLHttpRequest, .NET 클라이언트를 위한 HttpClient 등이 그것입니다.

전제조건

Web API 프로젝트 생성하기

먼저, Visual Studio 2013을 시작하고, 파일(File) 메뉴에서 새 프로젝트(New Project)를 선택합니다. 새 프로젝트(New Project) 대화 상자가 나타나면, 좌측 패인에서 웹(Web) 노드를 선택한 다음, 중앙 패인에서 ASP.NET 웹 응용 프로그램(ASP.NET Web Application) 템플릿을 선택합니다. 프로젝트 이름을 입력하고 확인(OK) 버튼을 클릭합니다.

그리고, 새 ASP.NET 프로젝트(New ASP.NET Project) 대화 상자에서 "Web API" 템플릿을 선택합니다.

그런 다음, 인증 변경(Change Authentication) 버튼을 클릭합니다. 인증 변경(Change Authentication) 대화 상자가 나타나면 개별 사용자 계정(Individual User Accounts)을 선택합니다. OK 버튼을 클릭합니다. (역주: 재미있게도 이 버튼만 한글화가 되어 있지 않습니다.)

다시, 새 ASP.NET 프로젝트(New ASP.NET Project) 대화 상자에서 확인(OK) 버튼을 클릭해서 프로젝트를 생성합니다.

이제 F5키를 눌러서 응용 프로그램의 디버깅을 시작합니다. 그러면, ASP.NET MVC 5로 구현된 응용 프로그램의 기본 홈 페이지가 나타날 것입니다.

웹 서버의 포트는 Visual Studio가 웹 프로젝트를 실행할 때, 무작위로 선택됩니다. 가령, 위의 이미지에서는 포트 번호로 49436번이 사용되고 있습니다.

Web API로 HTTP 요청 전송하기

먼저, 익명 요청을 Web API에 전송하면 무슨 일이 발생하는지부터 살펴보겠습니다. 피들러를 실행하고 Composer 탭을 선택합니다. 그런 다음, 주소 입력란에 다음 URL을 입력합니다:

http://localhost:port/api/values

이 URL에서 "port" 부분에는 Visual Studio가 선택한 실제 포트 번호를 입력해야 합니다. 그런 다음, Execute 버튼을 클릭합니다.

이렇게 Execute 버튼을 클릭하면 피들러가 지정한 URL로 GET 요청을 전송합니다. 그리고, 요청이 완료되면 좌측 패인의 HTTP 세션 목록에 응답이 나타납니다.

이 응답의 상태 코드는 401로, 인증되지 않았음을 의미합니다. 이 코드가 반환된 이유는 ValuesController[Authorize] 어트리뷰트가 지정되어 있기 때문입니다. 이 어트리뷰트가 지정된 컨트롤러는 인증된 사용자로부터 수신된 요청에만 응답합니다. 즉, 지금은 요청에 아무런 자격 증명도 제공하지 않았기 때문에 메서드가 401 코드를 반환한 것입니다.

응답의 세부 내용을 살펴보려면 세션 목록에서 응답을 더블 클릭합니다. 응답의 세부 내용은 Inspector 탭에서 살펴볼 수 있습니다.

Raw HTTP 응답을 살펴보려면 상세 뷰에서 Raw 탭을 클릭합니다. 다음과 비슷한 내용을 확인할 수 있을 것입니다:

HTTP/1.1 401 Unauthorized
Cache-Control: no-cache
Pragma: no-cache
Content-Type: application/json; charset=utf-8
Expires: -1
Server: Microsoft-IIS/8.0
X-AspNet-Version: 4.0.30319
WWW-Authenticate: Bearer
X-Powered-By: ASP.NET
Date: Tue, 15 Oct 2013 16:31:28 GMT
Content-Length: 61

{"Message":"Authorization has been denied for this request."}

이 응답의 WWW-Authenticate 헤더 값은 "Bearer"로, 이는 클라이언트가 반드시 무기명 토큰(Bearer Token)을 사용해서 인증해야 한다는 것을 나타냅니다.

무기명 토큰은 특정 형태의 엑세스 토큰(Access Token)입니다. 엑세스 토큰은 보호되고 있는 리소스에 클라이언트가 접근할 수 있는 권한을 부여해주는 자격 증명 문자열입니다 (RFC 6749 참고). 무기명 토큰은 모든 클라이언트가 사용할 수 있는 엑세스 토큰입니다. 즉, 클라이언트가 토큰을 사용할 때, 그 토큰이 특정 클라이언트에게 발급된 것인지 확인하지 않고도 사용할 수 있습니다 (RFC 6750 참고). 따라서, 무기명 토큰은 반드시 SSL과 함께 사용해야 합니다. 만약, 무기명 토큰을 평문으로 전송한다면, 누군가가 이를 가로채서 보호된 리소스에 접근할 수도 있습니다.

사용자 등록하기

정상적으로 Web API에 접근하려면 등록된 사용자로 로그인해야 합니다. 따라서, 가장 먼저 처리해야 할 작업은 새로운 사용자를 등록하는 일입니다. 피들러의 Composer 탭에서 주소 입력란 좌측에 있는 드롭다운의 값을 "POST"로 변경합니다.

그리고, 주소 입력란에 다음 URL을 입력합니다:

http://localhost:port/api/Account/Register

계속해서, Request Headers의 끝 부분에 다음 헤더를 추가합니다:

Content-Type: application/json

마지막으로, Request Body의 끝 부분에 다음 내용을 추가합니다:

{
  "UserName": "Alice",
  "Password": "password123",
  "ConfirmPassword": "password123"
}

다음 스크린샷은 피들러에 작성된 이번 요청을 보여줍니다.

이 요청은 다음과 같은 Raw HTTP 요청을 생성하게 될 것입니다:

POST http://localhost:49436/api/Account/Register HTTP/1.1
User-Agent: Fiddler
Host: localhost:49436
Content-Type: application/json
Content-Length: 106

{
  "UserName": "Alice",
  "Password": "password123",
  "ConfirmPassword": "password123"
}

이제, Execute 버튼을 클릭해서 요청을 전송합니다. 그러면, 이번에는 사용자가 성공적으로 멤버십 데이터베이스에 추가됐음을 뜻하는 응답 상태 코드 200이 반환될 것입니다.

한편, 서버 쪽에서는 이 요청이 Controllers/AccountController.cs 파일에 정의된 AccountController.Register 메서드에 의해서 처리됩니다. 이 메서드는 ASP.NET Identity를 이용해서 데이터베이스에 사용자를 추가합니다. 다음 스크린샷은 Register 메서드에 중단점이 걸린 상황을 보여줍니다. 전송된 JSON 페이로드가 RegisterBindingModel의 인스턴스로 역직렬화된 것을 확인할 수 있습니다.

인증 및 무기명 토큰 얻기

이번에는 "Alice"로 로그인 하고 무기명 토큰을 얻어보겠습니다. 그러려면, HTTP 요청을 토큰 종점으로 전송해야 합니다.

피들러의 Composer 탭에 다음 URL을 입력합니다 ("port"를 실제 포트 번호로 변경해야 한다는 점을 기억하시기 바랍니다):

http://localhost:port/Token

이번에도 요청 메서드로 POST를 사용합니다. Content-Type 헤더를 application/x-www-form-urlencoded로 변경합니다. 그리고, 요청 본문도 다음과 같이 변경해야 합니다:

grant_type=password&username=Alice&password=password123

역주: 위의 요청 본문을 복사해서 붙여 넣는 경우, 문자열의 마지막에 빈 문자열 등이 추가되지 않도록 조심하기 바랍니다. 주의하지 않으면, 비밀번호가 일치하지 않는 것으로 평가되어 본문과 다른 결과가 나타나게 됩니다.

이제, Execute 버튼을 클릭해서 요청을 전송합니다. 다음은 이렇게 전송되는 Raw HTTP 요청입니다:

POST http://localhost:49436/Token HTTP/1.1
User-Agent: Fiddler
Host: localhost:49436
Content-Type: application/x-www-form-urlencoded
Content-Length: 55

grant_type=password&username=Alice&password=password123

요청이 성공하면, 다음과 같은 응답 본문이 반환될 것입니다:

{
    "access_token":"boQtj0SCGz2GFGz[...]",
    "token_type":"bearer",
    "expires_in":1209599,
    "userName":"Alice",
    ".issued":"Mon, 14 Oct 2013 06:53:32 GMT",
    ".expires":"Mon, 28 Oct 2013 06:53:32 GMT"
}

(이 결과는 읽기 편하도록, 엑세스 토큰을 자르고 들여쓰기를 적용한 상태입니다.)

이 응답은 엑세스 토큰을 담고 있는 JSON 페이로드로, 클라이언트는 이 토큰을 사용해서 Web API에 접근할 수 있습니다. 또한, 토큰 종점에 요청을 전송해서 사용자를 인증한다는 점에도 유의하시기 바랍니다. 이 엑세스 토큰은 보호되는 리소스에 클라이언트가 접근할 수 있도록 권한을 부여해주는 무기명 토큰입니다.

이 엑세스 토큰을 메모장 같은 곳에 간단하게 복사해놓습니다. 본문의 다음 절에서 이 토큰을 사용해보게 될 것입니다.

그리고, 무기명 토큰에는 만료시간이 존재한다는 점에 주의하시기 바랍니다. 이 만료시간은 Startup.Auth.cs 파일에 작성된 Startup 클래스의 정적 생성자에서 설정이 가능합니다.

static Startup()
{
    // ...
    OAuthOptions = new OAuthAuthorizationServerOptions
    {
        TokenEndpointPath = new PathString("/Token"),
        Provider = new ApplicationOAuthProvider(PublicClientId, UserManagerFactory),
        AuthorizeEndpointPath = new PathString("/api/Account/ExternalLogin"),
        AccessTokenExpireTimeSpan = TimeSpan.FromDays(14),
        AllowInsecureHttp = true
    };
}

권한을 부여받은 요청 전송하기

이제 Web API에 다시 한 번 GET 요청을 전송할 준비가 되었습니다. 이번에는 요청에 무기명 토큰을 담아서 함께 전송해 볼 것입니다.

피들러의 Composer 탭에서 요청 메서드를 POST에서 다시 GET으로 변경합니다. 요청 본문을 삭제하고 Content-Type 헤더도 삭제합니다. 그리고, 다음 헤더를 추가합니다:

Authorization: Bearer [...]

여기서 "Bearer" 뒤편의 [...] 자리에는 이전 절에서 복사해뒀던 엑세스 토큰을 입력합니다. 실제 토큰 문자열은 몇 백 글자 길이의 문자들로 구성되어 있습니다.

역주: 위의 이미지에서 볼 수 있는 것처럼, 주소 입력란에는 본문의 최초 요청과 동일한 http://localhost:port/api/values을 입력합니다:

다시, Execute 버튼을 클릭해서 요청을 전송합니다. 이번에는 요청이 성공하고 상태 코드 200이 반환될 것입니다.

HTTP/1.1 200 OK
Cache-Control: no-cache
Pragma: no-cache
Content-Type: application/json; charset=utf-8
Expires: -1
Server: Microsoft-IIS/8.0
X-AspNet-Version: 4.0.30319
X-Powered-By: ASP.NET
Date: Tue, 15 Oct 2013 17:02:40 GMT
Content-Length: 19

["value1","value2"]

이후 과정 안내

본문에서는 Raw HTTP 요청을 이용해서 개별 사용자 계정으로 인증하는 방법을 살펴봤습니다. 피들러로 클라이언트 측 테스트를 수행하는 대신, Visual Studio 2013에서 ASP.NET "Single Page Application" 템플릿을 선택해서 클라이언트 웹 응용 프로그램을 생성할 수도 있습니다.

그리고, Microsoft 계정, 트위터, 페이스북, 그리고 구글 등과 같은 외부 인증 서비스를 추가할 수도 있습니다. 이에 대한 보다 자세한 정보는 보안: 외부 인증 서비스 (C#) 문서를 참고하시기 바랍니다.