Developer: PHP
  다운로드
Oracle Database
샘플 코드
  태그
php, security, All

PHP를 이용한 아이덴티티 관리, 제 1 부


(Identity Management Using PHP, Part 1)

저자 Michael McLaughlin

PHP 기반 웹 애플리케이션에서 아이덴티티 관리 환경을 구현하는 방법을 배워 봅시다.

게시일: 2007년 5월

웹 페이지와 애플리케이션에 대한 접근 보안을 관리하는 것은 모든 개발자에게 공통적인 과제입니다. 신뢰할 수 있는 사용자에게는 데이터 접근을 제공하는 한편으로 불법적인 사용자의 접근은 차단할 수 있어야 합니다. 이러한 문제를 해결하기 위해 가장 일반적으로 사용되는 것이 아이덴티티 관리 솔루션입니다.

아이덴티티 관리는 사용자를 확인, 인증, 감사하기 위한 시스템을 설계, 개발, 관리하기 위한 프로세스를 의미합니다. 아이덴티티 관리 시스템은 사용자 크리덴셜(credential) 목록으로 구성된 액세스 컨트롤 리스트(ACL)를 포함하고 있으며, 이 ACL을 이용하여 할당된 시스템 권한을 대조합니다. 크리덴셜은 일반적으로 사용자 이름과 패스워드의 쌍으로 구성됩니다. 크리덴셜은 사용자를 시스템 권한과 연계시키는 역할을 합니다. 시스템 권한(system previlege)이란 사용자 계정이 데이터를 접근/변경하거나 서브시스템/서브루틴을 실행할 수 있는 권한을 말합니다. 여기서 계정(account)은 사용자, 그룹 또는 시스템일 수 있습니다.

본 문서에서는 PHP 기반 웹 애플리케이션을 이용하여 아이덴티티 관리 솔루션을 구현하는 방법을 설명하고 있습니다. Oracle Identity Management 데이터베이스 모델을 설계, 구현하는 방법과 브라우저 기반 애플리케이션에서 다양한 사용자 상호 작용을 계획, 관리하는 방법이 소개됩니다.

본 문서의 제 2 부에서는, 데이터베이스 접근을 위한 다양한 방법과 아이덴티티 관리 환경에서 Oracle Virtual Private Database (VPD)의 보안 정책을 활용하는 방법, 그리고 기본 내장된 DBMS_APPLICATION_INFO 패키지에 대해 배워 봅니다.

아키텍처, 인증, 암호화

아키텍처. 웹 애플리케이션이 Apache HTTP 서버의 정보를 요청할 때, 클라이언트로부터 서버로 정보가 전달됩니다. 사용자 관점에서 볼 때 이 프로세스는 URL을 입력하고 그 결과로 웹 페이지를 확인하는 과정으로 요약됩니다. 하지만 URL은 URI를 구성하는 한 요소에 불과합니다. URI는 URL 이외에도 HTTP 헤더, 쿠키 등의 정보를 포함하고 있습니다. 이 정보는 URL 내에서 인코딩된 이름/값 쌍의 형태로 전송됩니다.

쿠키(cookie)란 일반 텍스트 또는 암호화된 텍스트를 포함하는 작은 크기의 텍스트 파일을 의미합니다. 쿠키는 브라우저와 서버 애플리케이션 간의 현재 커뮤니케이션 상태 정보를 저장하고 있습니다. 과거에는 쿠키의 컨텐트를 URI에 첨부하여 트랜잭션 상태를 관리하는 방법이 자주 사용되었습니다. 현재 사용되는 싱글 세션 쿠키는 참조 숫자를 통해 서버에 안전하게 저장된 이름/값 쿠키 쌍을 참조합니다. 이 참조 숫자를 "웹 세션 ID"라 부릅니다.

웹 애플리케이션 세션을 사용할 때에는, 세션 ID와 세션 만료 정보가 별도의 세션 쿠키를 통해 전달됩니다. 클라이언트 브라우저가 쿠키를 허용하지 않는 경우, 웹 애플리케이션은 URL 리라이팅을 통해 공개되지 않은 URI로부터 공개된 URL로 세션 쿠키를 리다이렉트 처리합니다. 데이터를 서버에 저장한 후 유니크한 세션 ID를 통해 참조하는 방법은 두 가지 장점을 갖습니다. 먼저 URL의 크기를 최소화할 수 있고, 두 번째로 사용자 인터액션의 내부 상세 정보를 숨길 수 있습니다.

샘플 코드 설명

프로그램 이름 언어 설명
AddDbUser.php PHP 새로운 사용자를 인증하고 액티브 세션을 반환한 후, 새로운 사용자를 ACL에 입력하기 위한 폼을 렌더링하는 프로그램 파일
create_identity_db1.sql SQL 데이터베이스를 생성하고 웹 애플리케이션의 초기화에 필요한 시드 데이터를 입력하는 스크립트
Credentials1.inc PHP 모든 oci_connect() 함수 호출에서 사용되는 3 가지 글로벌 상수를 정의한 인클루드 파일
SignOnDB.php PHP AddDbUser.php 프로그램을 호출하기 전에 PHP 세션 ID를 설정하는 프로그램 파일
SignOnRealm.php PHP 사용자 크리덴셜을 인증하고 인증 값을 정적 웹 페이지에 렌더링하는 프로그램 파일

서버에 세션 정보를 저장하는 방법이 보안 측면에서 좀 더 바람직한 것은 사실이지만, 세션 하이재킹에 대비할 필요가 있습니다. 악성 사용자는 세션 하이재킹을 위해 (1) 세션 쿠키를 훔치거나, (2) 세션 ID를 포함하는 URL을 복사하거나, (3) "man-in-the-middle" 공격을 통해 정보를 가로챌 수 있습니다. 최신 브라우저는 이러한 공격에 대한 대비책을 제공하며, PHP는 물리적인 세션 쿠키를 파일 시스템에 기록하지 않음으로써 세션 하이재킹을 방지합니다.

쿠키와 세션을 이용한 아키텍처는 세션 데이터의 히스토리에 대한 마이닝이 가능하다는 이점을 제공합니다(단 데이터가 정규화된 데이터 모델에 저장되어 있어야 합니다). 이 데이터를 이용하면 고객이 웹 애플리케이션을 어떻게 이용하고 있는지 분석할 수 있습니다. 또 고객의 브라우징 패턴과 구매 경향을 확인하는 것도 가능합니다. 이러한 정보를 활용함으로써 전체가 아닌 각 개인을 대상으로 한 마케팅을 수행할 수 있습니다.

인증. 웹 애플리케이션에서 사용자를 검증하려면 두 가지 중 한 가지 인증 모델을 사용해야 합니다. 기본적인 HTTP/HTTPS 인증 또는 쿠키와 세션을 이용한 인증 모델 중 한 가지를 선택해야 합니다. 세 번째 대안으로 다이제스트(digest) HTTP 인증 방법이 현재 개발되고 있지만 향후 몇 년 동안은 완성되지 않을 것으로 보입니다.

HTTP/HTTPS 인증 모델은 웹 페이지에서 크리덴셜을 가져온 후 ACL과 대조하는 방법으로 검증을 수행합니다. 또 브라우저가 쿠키를 허용하지 않도록 설정된 환경에서도 정상적으로 실행이 가능합니다. HTTP/HTTPS 인증과 다이제스트 HTTP 인증 방식은 쿠키를 사용하지 않고 검증을 수행하지만, 세션/쿠키 인증 방식은 최소한 세션 ID 쿠키 작성을 요구한다는 차이가 있습니다.

URL의 일부로 세션 ID를 전송하는 경우에는 URL 리라이팅(rewriting)에 보안 문제가 수반될 수 있습니다. 일부 사용자들이 인스턴트 메시지를 이용해서 URL을 다른 사람에게 전송할 수 있기 때문입니다. 이렇게 URL을 입수한 다른 사용자는 하이재킹을 통해 기밀 데이터에 불법적으로 접근할 수 있습니다. URL 리라이팅의 대안으로 세션 ID를 숨겨진 필드로 웹 페이지에 렌더링하는 방법이 있습니다. 이 방법 역시 보안상 취약한 것은 마찬가지이지만, 세션을 URL에 덧붙이는 방법보다는 덜 위험합니다.

PHP 인증 스크립트는 유저네임/패스워드를 위한 이름/값 쌍을 입력으로 취합니다. 그런 다음, 스크립트는 입력된 값을 ACL에 저장된 데이터와 대조합니다. 이 프로세스는 위에서 설명한 두 가지 인증 모델에 공통적으로 적용됩니다. 서버-사이드 패스워드는 일반적으로 암호화된 형태로 저장되지만, 유저네임/패스워드 값이 일반 텍스트로 노출될 때마다 언제든 시스템 보안이 뚫릴 수 있음을 고려해야 합니다.

그러므로, 사용자에게 지나치게 복잡한 패스워드를 강요해서는 안됩니다. 사용자가 쉽게 기억하지 못할 수 있기 때문입니다. 사용자는 기억하기 어려운 패스워드를 메모지나 포스트잇에 기록해 두는 경우가 많습니다. 이렇게 기록된 패스워드는 해커들의 불법적인 접근을 위해 악용될 수 있습니다.

암호화.암호화는 웹 애플리케이션 개발, 구축, 관리 과정에서 언제나 중요하게 논의되는 주제입니다.

PHP 웹 애플리케이션은 두 가지 암호화 테크닉을 지원합니다. 첫 번째로 프로토콜을 암호화하는 방법이 있습니다. HTTPS와 같은 암호화된 프로토콜은 네트워크 분석기(패킷 스니퍼)를 이용한 "man-in-the-middle" 공격을 차단하는데 효과적입니다. 두 번째로 사용자 패스워드를 암호화하여 서버에 침입한 해커가 패스워드를 확인하지 못하도록 하는 방법이 있습니다.

패스워드 암호화는 암호화된 패스워드와 소스 코드를 분리해서 저장했을 때 그 효과가 극대화됩니다. ACL이 파일 시스템에 파일 형태로 저장되어 있다면, 단 한 번의 서버 해킹으로 암호화 소스 코드와 암호화된 패스워드가 동시에 노출될 수 있습니다. ACL을 데이터베이스에 저장하면 암호화된 패스워드의 노출 위협을 줄일 수 있습니다.

아이덴티티 관리 데이터 모델링

아이덴티티 관리 데이터 모델의 구현 작업은 매우 간단할 수도, 또는 매우 복잡할 수도 있습니다. 가장 간단한 모델은 ACL을 포함하는 단 하나의 테이블로 구성됩니다. 하지만 이 모델은 기본적인 HTTP/HTTPS 인증 방식에서만 적용이 가능하다는 한계가 있습니다.

쿠키/세션 인증 방식에서는 최소한 두 개 이상의 테이블이 필요합니다. 하나의 테이블은 ACL의 저장에, 다른 하나는 세션 데이터의 저장에 사용됩니다. 기본적인 모델이 아래 그림과 같습니다:



그림 1 기본적인 인증 데이터 모델

여기에 사용자 인터액션을 캡처/마이닝하는 기능을 추가함으로써 보다 효과적인 솔루션을 구현할 수도 있을 것입니다. 관리자는 이러한 기능을 통해 웹 페이지의 목록 또는 마우스 클릭과 같은 개별 이벤트를 분석할 수 있습니다. 그림 2는 사용자 네비게이션과 로그인 성공/실패 이벤트를 캡처하기 위한 데이터 모델을 보여 주고 있습니다. 본 문서의 예제에서, 이 데이터 모델은 create_identity_db.sql을 통해 생성됩니다(샘플 코드 참조).



그림 2 세부적인 인증 데이터 모델

이 모델을 좀 더 확장하여 정의된 모듈의 버전과 모듈에 대한 런타임 호출을 추적하는 것도 가능합니다. 매개변수들은 카탈로그 아이템(예: Amazon.com의 서적)에 매핑됩니다. 또 액세스 로그를 추가하여 단일 세션에서 사용된 다수의 연결을 동시에 추적할 수 있습니다. 이러한 모델을 구현하는 방법은 요구 사항에 따라 달라질 수 있습니다. 그림 2는 카탈로그 주문 애플리케이션의 버전 컨트롤을 지원하기 위한 모델의 예를 함께 보여 주고 있습니다. 본 문서의 샘플 코드는 로그인 실패 이벤트만을 캡처하고 있으며, ACCESS_LOG 테이블의 이름을 INVALID_SESSION 테이블로 변경하여 사용하고 있습니다.

인증 프로세스 모델

기본적인 HTTP/HTTPS 인증 방식은 브라우저의 크리덴셜을 렐름(relm)에 설정하는 방식으로 동작합니다. 브라우저는 다수의 렐름을 동시에 지원할 수 있습니다. 렐름은 XHTML 문서의 헤더에 설정됩니다. 렐름은 폼 내에 수작업으로 코딩될 수도 있고, httpd.conf 파일의 AuthName 매개변수에 설정될 수도 있습니다. 렐름은 보호 서버 영역(protected server area)의 보안 강화를 위해 활용되며, 두 개 이상의 보호 서버 영역에 동시에 적용될 수도 있습니다. 기본적인 HTTP/HTTPS 인증 방식은 로그인/로그아웃 기능을 별도로 코딩할 필요가 없다는 장점을 가집니다. 이 방식에서는 브라우저가 로그인 폼을 제공하며, 브라우저 윈도우를 닫음으로써 로그아웃이 가능합니다.

하지만 HTTP/HTTPS 인증 방식은 활성화된 렐름 인증을 종료하기 위해 모든 브라우저 윈도우를 닫아야 한다는 단점이 있습니다. 따라서 인증된 사용자가 브라우저 윈도우를 열어 놓은 상태로 자리를 뜰 경우, 다른 사용자가 신분을 도용하여 기밀 데이터에 불법적으로 접근할 수 있습니다.

쿠키/세션 인증 방식은 사용자 크리덴셜을 ACL에 대조하는 방식으로 동작합니다. ACL이 파일에 저장된 경우, 파일을 읽어 들인 후 사용자가 입력한 유저네임(일반 텍스트), 패스워드(암호화된 텍스트)와 대조하는 과정이 수행됩니다.

ACL이 데이터베이스에 저장되어 있는 경우에는, 1차 로그인 연결과 후속 연결의 두 차례에 걸쳐 작업이 수행됩니다. 1차 로그인에서는 데이터베이스에 연결하여 테이블로부터 유저네임과 암호화된 패스워드를 읽어 들인 후 그 결과를 사용자가 입력한 유저네임과 암호화된 패스워드에 대조하는 작업이 이루어집니다. 후속 웹 요청에서는, 데이터베이스에 연결하여 세션 ID를 읽어 들인 후 실제 세션 ID와 대조하는 작업이 수행됩니다. 기본적인 HTTP/HTTPS 인증 방식과 달리, 이 경우에는 웹 브라우저를 열어 놓은 상태에서도 로그아웃이 가능합니다.

세션 ID가 세션 하이재킹을 통해 악용될 수도 있지만, 몇 가지 예방 조치를 통해 리스크를 최소화하는 것이 가능합니다. 먼저 로그인 페이지를 단순한 텍스트 XHTML 폼이 아닌 PHP 스크립트로 구현함으로써 가장 심각한 보안 위협을 차단할 수 있습니다. 본 문서의 쿠키/세션 인증을 위한 샘플 코드가 이 방식을 사용하고 있습니다. 이 방식을 이용하면 공유 머신에 연결한 두 번째 사용자가 앞에서 사용된 크리덴셜에 접근하는 것을 차단할 수 있습니다. 자세한 구현 방법은 "쿠키/세션 인증 모델" 섹션에서 설명됩니다.

기본 HTTP/HTTPS 인증 모델. 기본 HTTP/HTTPS 인증 모델은 렐름에 대해 단 한 차례 인증되며, 모든 브라우저 윈도우가 닫힌 경우 로그아웃 처리됩니다. 그림 3은 브라우저 컨텍스트의 관점에서 바라본 액티비티 다이어그램을 보여 주고 있습니다. Microsoft Internet Explorer는 인증 작업을 실패 처리하기 전에 세 차례에 걸쳐 인증을 시도합니다. 사용자는 단순히 페이지를 리프레시하는 방법으로 다시 로그인 시도를 할 수 있습니다. Firefox 브라우저는 사용자가 인증 프로세스를 취소할 때까지 프롬프트를 표시합니다.



그림 3 기본 HTTP/HTTPS 인증 액티비티 다이어그램

사용자는 로그인 폼의 URL을 입력하여 웹 애플리케이션 세션을 시작합니다. 서버는 post 작업을 수행한 후 브라우저에 사용자 크리덴셜 정보를 요청하는 메시지를 전달합니다. 사용자가 SingOnDB.php 프로그램에 크리덴셜을 제출하면, 코드는 입력된 크리덴셜의 검증을 시도합니다. 검증이 성공하면, 사용자는 렐름에 포함된 모든 웹 페이지를 사용할 수 있습니다. 인증에 실패하는 경우에는 앞에서 설명한 것처럼 브라우저 로그인 폼이 다시 표시됩니다.

기본 HTTP/HTTPS 인증 방식을 구현하는 경우, PHP는 $_SERVER 어레이의 3 가지 사전 정의된 변수를 사용합니다. 기본 HTTP/HTTPS 인증은 사용자 ID와 패스워드를 각각 $_SERVER['PHP_AUTH_USER']와 $_SERVER['PHP_AUTH_PW']에 저장합니다. 세 번째 변수 HTTP_AUTHORIZATION이 설정되어 있지 않은 경우, 사용자는 이 두 가지 $_SERVER 변수 값을 인증 함수에 제출하게 됩니다. 서버-사이드 프로그램에 의해 사용자 인증이 완료되면, 서버는 $_SERVER 어레이의 HTTP_AUTHORIZATION을 읽어 들인 후 윈도우가 닫힐 때까지 렐름에 대한 브라우저 액세스를 허용합니다.

기본 HTTP/HTTPS 인증 모델의 페이지를 보호하기 위한 간단한 방법으로, Boolean 컨트롤 변수를 추가하여 인증 작업이 완료될 때까지 False 값으로 설정할 수 있습니다. 서버는 Boolean 컨트롤 변수가 True인 경우에만 컨텐트를 디스플레이합니다. 이를 위해 인증 여부를 점검하는 코드를 구현하고, 렐름에 대하여 인증되지 않은 경우 크리덴셜을 요구하는 프롬프트를 표시해야 합니다. 또 이 코드는 보안이 요구되는 모든 웹 페이지에 포함되어 있어야 합니다.

서버-사이드 스크립트의 HTTP 헤더 검증 작업은 사전 정의된 $_SERVER 변수 값의 점검 결과에 따라 트리거 됩니다. SignOnRealm.php 페이지가 요청된 경우 이 변수들의 값이 반환되는 헤더에 저장되며, 이 값을 확인한 브라우저는 인증 정보 입력을 요구하는 대화 상자를 표시합니다. 사용자가 인증 정보를 입력한 후 OK 버튼을 누르면 서버-사이드 스크립트가 다시 한 번 실행됩니다. 서버-사이드 스크립트는 입력된 크리덴셜의 검증을 시도합니다. 사용자가 인증 프로세스를 중간에 취소하는 경우, 일반적으로 서버-사이드 스크립트는 실패 메시지를 반환합니다. 일부 사이트는 3, 4 회에 걸쳐 인증이 실패한 경우 이를 해킹 시도로 판단하고 사용자 계정을 폐쇄하기도 합니다.

테스트 애플리케이션의 셋업. 데모 애플리케이션은 IDMGMT1이라는 이름의 오라클 스키마를 사용하고 있습니다. (패스워드는 스키마 네임과 동일하게 설정되어 있습니다.) 아래와 같은 방법으로 사용자와 환경을 생성합니다:

1. SYSTEM 사용자 권한으로 데이터베이스에 로그인한 후 아래 명령을 실행합니다:

SQL> CREATE USER IDMGMT1 IDENTIFIED BY IDMGMT1;
SQL> GRANT CONNECT, RESOURCE TO IDMGMT1;

2. 새로운 사용자 스키마로 연결합니다:

SQL> CONNECT IDMGMT1/IDMGMT1@XE

3. IDMGMT1 스키마에서 create_identity_db1.sql 스크립트를 실행하여 필요한 오브젝트들을 생성하고 SYSTEM_USER 테이블을 초기화합니다:

SQL> @create_identity_db1.sql

4. htdocs 디렉토리 또는 htdocs 디렉토리의 서브디렉토리에 아래 파일을 위치시킵니다:

 • SignOnRealm.php
 • SignOnDB.php
 • AddDbUser.php

이것으로 셋업에 필요한 모든 작업을 완료하였습니다. 이제 기본 HTTP/HTTPS 인증, 또는 세션/쿠키 인증 방식을 테스트해 볼 수 있습니다.

기본 HTTP/HTTPS 인증 방식의 테스트. 아래와 같은 방법으로 렐름 아이덴티티 관리 방식을 테스트해 볼 수 있습니다:

1. 아래 URL을 이용하여 렐름 인증 페이지를 엽니다.

http://localhost/SignOnRealm.php 

2. 아래의 계정을 이용하여 기본 HTTP/HTTPS 인증을 테스트해 볼 수 있습니다. 유저네임과 패스워드가 대소문자를 구분함에 주의하시기 바랍니다.

User Name Password
administrator welcome
guest guest



그림 4 기본 HTTP/HTTPS 인증 대화 상자

인증 정보를 성공적으로 입력하고 나면, 아래 페이지를 통해 유저네임(일반 텍스트)과 암호화된 패스워드가 표시됩니다.



그림 5 기본 HTTP/HTTPS 인증 정보의 검증 완료 화면

기본 HTTP/HTTPS 인증 코드의 분석. 사용자가 유저네임/패스워드를 입력하면, 스크립트는 verify_db_login() 함수를 호출합니다. 아래 PHP 코드는 렐름에 대한 사용자 인증을 수행하는 과정을 보여 주고 있습니다.

// Declare control variable.
$valid_user = false;

// Authenticate user.
if ((isset($_SERVER['PHP_AUTH_USER'])) && (isset($_SERVER['PHP_AUTH_PW'])))
  if (verify_db_login($_SERVER['PHP_AUTH_USER'],$_SERVER['PHP_AUTH_PW']))
    $valid_user = true;

위 함수는 오라클 데이터베이스에 대한 연결을 설정한 뒤 사용자가 입력한 인증 정보가 올바른지 확인합니다. verify_db_login() 함수는 인증 정보가 올바른 경우 True를, 그렇지 않은 경우 False를 반환합니다. SYSTEM_USER 테이블이 존재하지 않는 경우에는 이를 알리는 에러 메시지가 표시됩니다. 웹 페이지를 위한 메인 코드가 아래와 같습니다:

// Check for authorized account.
function verify_db_login($userid,$passwd)
{
  // Attempt connection.
  if ($c = @oci_connect(SCHEMA,PASSWD,TNS_ID))
  {
    // Return a row.
    $s = oci_parse($c,"SELECT   NULL
                       FROM     system_user
                       WHERE    system_user_name = :userid
                       AND      system_user_password = :passwd
                       AND      SYSDATE BETWEEN start_date
                                        AND NVL(end_date,SYSDATE)");

    // Encrypt password.
    $newpassword = sha1($passwd);
    // Bind variables as strings.
    oci_bind_by_name($s,":userid",$userid);
    oci_bind_by_name($s,":passwd", $newpassword));

      // Execute the query.
      if (@oci_execute($s,OCI_DEFAULT))
      {
        // Check for a validated user, also known as a fetched row.
        if (oci_fetch($s))
           return true;
        else
          return false;
      }
      else
      {
        // Print error when execution fails.
        $errorMessage = "Check for a missing SYSTEM_USER table.<br />";
        print $errorMessage;
      }

    // Close connection.
    oci_close($c);
  }
  else
  {
    $errorMessage = oci_error();
    print htmlentities($errorMessage['message'])."<br />";
  }
}

verify_db_function() 함수에서 참고해야 할 사항이 두 가지 있습니다. 먼저, 쿼리는 NULL 값을 반환하며 따라서 반환된 값을 별도로 처리할 필요가 없습니다. 두 번째로, 일반 텍스트로 입력된 암호는 sha1() 함수를 통해 암호화되어 로컬 변수에 저장됩니다.

지금까지 기본 HTTP/HTTPS 인증을 위해 기본적으로 요구되는 컴포넌트를 구현하는 방법을 설명하였습니다. 이 인증 모델을 사용할 때 주의해야 할 사항이 두 가지 있습니다. (1) 사용자는 브라우저의 크리덴셜 인증이 완료되고 나면 렐름에 대한 전폭적인 접근 권한을 가집니다. (2) 사용자가 로그아웃 하려면 모든브라우저 윈도우를 닫아야 합니다.

쿠키/세션 인증 모델. 쿠키/세션 모델은 전체 브라우저 윈도우를 닫지 않고도 웹 애플리케이션의 로그인/로그아웃이 가능하다는 이점 때문에 대중적으로 활용되고 있습니다. 이 인증 모델은 기본 HTTP/HTTPS 모델보다 복잡하며, 여러 가지 방식으로 쿠키/세션 인증을 구현할 수 있습니다.

로그인/로그아웃 작업은 활성화된 브라우저 내부에서 수행됩니다. 이 로그인/로그아웃 컨텍스트 때문에 쿠키/세션 인증 모델이 복잡한 구조를 갖게 되는 것입니다. 예를 들어, 브라우저의 뒤로 가기, 앞으로 가기, 새로 고침 버튼을 누를 때 이를 처리하기 위한 별도의 코드가 구현되어야 합니다. 사용자가 새로 고침 버튼을 누르더라도 사용자의 인증 정보가 두 차례 이상 전송되어서는 안됩니다. 또 사용자가 이미 애플리케이션에서 로그아웃한 뒤에 새로 고침 버튼을 눌렀을 때 다시 인증 시도가 이루어져서도 안됩니다. 이러한 로직은 프로그램을 통해 구현하는 방법 밖에 없습니다.

XHTML 웹 페이지 대신 로그인 PHP 스크립트를 사용하면 가장 직접적인 방법으로 웹 애플리케이션의 완전한 로그아웃을 보장할 수 있습니다. XHTML 웹 페이지와 달리, PHP 스크립트를 사용하면 세션 값의 리셋이 가능합니다. PHP 스크립트는 타겟 페이지에 새로운 인증 정보를 전달할 때 이전의 세션 값을 함께 전송하기 때문입니다.

PHP에서 글로벌 변수를 저장하기 위해 활용 가능한 몇 가지 사전 정의된 변수들이 있습니다. 그 중 하나가 $_SESSION 어레이입니다. 이 어레이를 이용하여 임의의 이름/값 쌍을 추가할 수 있으며, 여기서 이름은 실제 데이터 값에 대한 인덱스 값으로 사용됩니다. $_SESSION 어레이는 단순성과 유연성 면에서 장점을 제공하지만, 다른 라이브러리와의 충돌 가능성이 있다는 단점 또한 있습니다.

session_start() 함수는 활성화된 PHP 세션 ID 값을 반환합니다. session_regenerate_id() 함수를 호출하면 실제로는 session_destroy() 함수가 호출됩니다. 따라서 이 함수 이전에 먼저 session_start() 함수를 호출해야 합니다. session_destroy() 함수의 실행 여부는 PHP 세션 ID의 존재 여부에 따라 달라집니다. 세션 ID가 존재하지 않는 경우에는 호출 이전에 에러가 발생합니다. 실제 존재하는 세션 ID에 대해 session_regenerate_id() 함수를 실행하면 PHP 세션 ID가 리셋 처리됩니다. 브라우저 컨텍스트에 현재 세션 ID가 존재하지 않는 경우에도 session_regenerate_id() 함수로 인해 에러가 발생하지 않도록 보장하려면 런타임 에러 억제(runtime error suppression) 기능을 사용해야 합니다.

<?php
  // Start and regenerate session.
  session_start();
  @session_regenerate_id(true);
  $_SESSION['sessionid'] = session_id();
?>

위의 스크립트는 브라우저의 세션 값을 변경하고, Login 버튼을 다시 클릭했을 때 새로 인증이 수행되도록 합니다. 이를 위해 이전에 인증된 PHP 세션 ID를 새로운 세션 ID로 대체하는 작업이 수행됩니다. 새로운 PHP 세션 ID는 다음 로그인 과정에서 제출되는 URL에 포함되어 전송됩니다.

PHP 세션이 설정된 후 데이터베이스 연결을 관리하는 방법에는 두 가지 가 있습니다. 그 하나는 PHP 모듈과 데이터베이스 간에 퍼시스턴트(persistent) 연결을 사용하는 방법이고, 또 하나는 논-퍼시스턴트(non-persistent) 연결을 사용하는 방법입니다. 퍼시스턴트 연결을 이용하면 하나의 트랜잭션 스코프에서 여러 개의 웹 요청을 처리할 수 있습니다. 퍼시스턴트 연결은 데이터베이스 커밋 명령이 실행되었을 때에만 종료됩니다. 퍼시스턴트 연결의 스코프 내에서, 사용자는 SQL, PL/SQL 명령을 이용하여 로우 락킹을 수행하여 변경 작업을 보류하거나 언커밋 처리할 수 있습니다. 논-퍼시스턴트 연결에서는 단일 웹 요청을 위한 연결이 생성/종료될 때까지 SQL, PL/SQL 구문을 실행하여 데이터를 조회하거나 변경할 수 있습니다. 이처럼 단일 연결로만 제한되는 데이터 트랜잭션을 "autonomous transaction"이라 부르기도 합니다.

본 문서의 샘플 프로그램은 논-퍼시스턴트 데이터베이스 연결을 사용하여 오라클 데이터베이스의 데이터를 조회/변경하고 있습니다. 따라서 모든 데이터베이스 트랜잭션은 "autonomous transaction"으로 수행됩니다.

그림 5의 SignOnDB.php 웹 페이지는 XHML 페이지 앞에 이러한 PHP 스크립트를 구현하고 있습니다. SignOnDB.php 폼에서 사용되는 XHTML 렌더링은 AddDbUser.php 페이지의 signOnForm() 함수에서도 사용되고 있습니다.

사용자가 애플리케이션에 로그인한 이후라 하더라도, 모든 웹 페이지에 PHP 인증 로직이 적용되어야 합니다. 샘플 프로그램의 모든 인증 로직은 AddDbUser.php 페이지 내에 구현되어 있습니다(그림 6 참조). 하지만 12 개의 함수를 하나의 인증 라이브러리에 구현하는 것도 가능합니다.

이 인증 로직이 동작하려면 브라우저에서 쿠키를 허용하고 있어야 합니다. 쿠키가 비활성화된 상태에서는 로그인/로그아웃만 가능할 뿐, 페이지 간을 이동할 때 세션 값이 상실되기 때문에 인증 정보의 유지가 불가능합니다.

세션/쿠키 인증 방식을 이용한 테스트. 아래와 같은 방법으로 세션 아이덴티티 관리 방식을 테스트해 볼 수 있습니다:

1. 아래 URL을 이용하여 세션/쿠키 인증을 실행합니다. (코드는 htdocs 디렉토리 내에 위치하고 있다고 가정합니다.)

http://localhost/SignOnDB.php 

2. 앞에서 정의된 두 가지 계정 중 하나를 이용하여 로그인을 테스트할 수 있습니다.



그림 6 쿠키/세션 인증을 위한 로그인 페이지

3. 이제 New User 폼을 이용하여 새로운 사용자 정보를 입력할 수 있습니다. 여기서 유저네임은 6-10 개의 문자로 구성되며 알파벳 문자로 시작되어야 합니다.



그림 7 쿠키/세션 인증을 위한 Add New User 페이지

4. 새로운 사용자를 추가하면, AddDBUser.php 스크립트가 새로운 New User 폼을 표시합니다. 이 폼을 통해 새로운 사용자가 성공적으로 입력되었다는 메시지, 또는 입력된 항목이 위반한 규칙이 무엇인지 표시됩니다. 이제 IDMGMT1 스키마에 연결하여 아래 쿼리를 실행함으로써 새로 입력된 사용자 엔트리를 확인할 수 있습니다:

SELECT system_user_name, system_user_password FROM system_user; 

세션 인증 코드의 분석

샘플 PHP 프로그램은 브라우저의 뒤로 가기, 앞으로 가기, 새로 고침 버튼을 위한 로직을 포함하고 있습니다. 그림 7은 렌더링된 웹 페이지를 보여 주고 있습니다. 브라우저 네비게이션 버튼을 이용하여 폼을 호출하거나 새로 고침을 실행하는 경우, 프로그램은 (a) 세션이 현재 사용자에 대해 데이터베이스 모델에 등록되어 있는지, (b) 원격 주소가 인증에 사용된 클라이언트 IP 주소와 동일한지, (c) 세션이 현재 활성화된 상태인지를 점검합니다. 여기서 세션이 '활성화'되었다는 것은 해당 세션의 마지막 데이터베이스 관련 작업이 최근 5 분 안에 실행되었음을 의미합니다.

최초 로그인 폼과 사용자 추가를 위한 폼은 모두 사용자 인증 정보를 조회하고 인증을 처리하기 전에 등록된 세션을 확인/검증하는 절차를 거칩니다. 인증 정보가 정상적으로 확인되면, SignOnDB.php 스크립트는 새로운 세션을 등록한 후 AddDbUser.php 스크립트를 호출합니다. AddDbUser.php 스크립트에 대해 새로 고침 버튼을 누르는 경우 signOnForm() 함수가 호출되어 SignOn.php 폼과 동일한 기능을 수행합니다. AddDbUser.php 스크립트는 인증이 성공적인 경우addUserForm()을 이용하여 페이지를 렌더링하며, 인증이 거부된 경우에는 로그인 실패 기록을 로그에 저장하고 다시 로그인 페이지를 렌더링합니다.



그림 8 쿠키/세션 인증을 위한 액티비티 다이어그램

이 웹 페이지는 두 개의 XHTML 폼을 포함하고 있습니다. 그 중 하나는 새로운 사용자를 입력하고, 입력된 사용자 정보가 가이드라인(예: 6-10개 문자를 사용, 알파벳으로 시작)을 준수하는지 검증한 후, 데이터베이스에 인증 정보를 입력하는데 활용됩니다. 다른 폼은 SignOnDB.php 스크립트를 호출하여 애플리케이션에서 로그아웃하고, 세션을 리셋하여 사용자가 로그아웃한 뒤에도 인증된 것처럼 보이지 않도록 처리합니다.

웹 페이지는 세션을 시작한 후 $_SESSION 어레이에 할당하고, 임의의 입력된 인증 정보를 로컬 변수에 할당한 후, 현재 인증된 세션이 사용 가능한지 확인합니다. 이를 위한 코드가 아래와 같습니다.

  // Check for valid session and regenerate when session is invalid.
  if ((get_session($_SESSION['sessionid'],$userid,$passwd) == 0) ||
      (($_SESSION['userid'] != $userid) && ($userid)))
  {
    // Regenerate session ID.
    session_regenerate_id(true);
    $_SESSION['sessionid'] = session_id();
  }
  else
  {
    $authenticated = true;
  }

get_session() 함수는 데이터베이스에 연결을 생성하고 SYSTEM_USER, SYSTEM_SESSION 테이블을 조인한 결과를 쿼리합니다. 쿼리로부터 반환된 결과를 통해 세션의 인증 여부를 확인할 수 있습니다. 이러한 검증 과정에서 요청이 동일한 IP 주소로부터 전달되었는지 확인하는 작업이 진행됩니다. 인증이 완료되면, 웹 페이지 변수에 쿼리 결과값이 할당되어 페이지 요청의 처리에 이용됩니다. 이와 동시에 update_session() 함수에 의해 LAST_UPDATE_DATE 컬럼 타임스탬프가 업데이트됩니다. 인증이 실패하는 경우에는 record_session() 함수에 의해 OBSELOTE_SESSION 테이블에 로그 기록이 저장됩니다.

// Get a valid session.
  function get_session($sessionid,$userid = null,$passwd = null)
  {
    // Attempt connection and evaluate password.
    if ($c = @oci_connect(SCHEMA,PASSWD,TNS_ID))
    {
      // Assign metadata to local variable.
      $remote_address = $_SERVER['REMOTE_ADDR'];

      // Return database UID within 5 minutes of session registration.
      // The Oracle DATE data type is a timestamp where .003472222 is
      // equal to 5 minutes.
      $s = oci_parse($c,"SELECT   su.system_user_name
                         ,        ss.system_remote_address
                         ,        ss.system_session_id
                         FROM     system_user su JOIN system_session ss
                         ON       su.system_user_id = ss.system_user_id
                         WHERE    ss.system_session_number = :sessionid
                         AND     (SYSDATE - ss.last_update_date) <=
                                    .003472222");

      // Bind the variables as strings.
      oci_bind_by_name($s,":sessionid",$sessionid);

      // Execute the query, error handling should be added.
      if (@oci_execute($s,OCI_DEFAULT))
      {
        // Check for a validated user, also known as a fetched row.
        if (oci_fetch($s))
        {
           // Assign unqualified values.
          $_SESSION['userid'] = oci_result($s,'SYSTEM_USER_NAME');

          // Check for same remote address.
          if ($remote_address == oci_result($s,'SYSTEM_REMOTE_ADDRESS'))
          {
            // Refresh last update timestamp of session.
            update_session($c,$sessionid,$remote_address);
            return (int) oci_result($s,'SYSTEM_SESSION_ID');           }
           else
           {
             // Log attempted entry.
             record_session($c,$sessionid);
             return 0;
           }
        }
        else
        {
          // Record when not first login.
          if (!isset($userid) && !isset($passwd))
            record_session($c,$sessionid);

          // Return a zero.
          return 0;
        }
      else
      {
        // Print error when oci_execute() fails.
        $errorMessage = "Check for a missing SYSTEM_USER or ";
        $errorMessage .= "SYSTEM_SESSION tables.<br />";
        print $errorMessage;
        return 0;
      }

      // Close the connection.
      oci_close($c);
    }
    else
    {
      $errorMessage = oci_error();
      print htmlentities($errorMessage['message'])."<br />";
      return 0;
    }
  }

get_session() 함수로부터 호출된 함수가 기존 세션의 확인을 위해 열린 데이터베이스 세션을 공유한다는 사실을 참고하시기 바랍니다. 이를 위해 연결을 실제 매개변수의 형태로 다른 두 함수에 전달하는 작업이 수행됩니다. 단일 데이터베이스 연결 내에서 데이터베이스 트랜잭션 스코프를 관리함으로써, 다수의 SQL, PL/SQL 구문을 단일 트랜잭션의 구성요소로서 관리할 수 있습니다. 따라서 데이터베이스 추상화 계층(data abstraction layer) 내에서 함수를 관리하고 네스티드 함수(nested function)을 서브루틴처럼 취급하는 것이 가능합니다.



그림 9 쿠키/세션 인증을 위한 Add New User Bad Credential 페이지

웹 페이지의 다음 코드 섹션은 다양한 목적을 위해 활용 가능한 여러 버전의 폼을 렌더링하고 있습니다. 그림 6은 애플리케이션 최초 로그인 시에 렌더링 되는 폼을 보여 주고 있습니다. 그림 5는 null 유저네임을 입력한 경우 렌더링 되는 폼을 보여 주고 있습니다. 이 코드는 SYSTEM_USER 테이블의 ACL에 새로운 사용자를 추가하는데 필요한 create_db_user() 함수를 호출하고 있습니다:

  // Check whether the program should:
  // -----------------------------------------------------------------
  //  Action #1: Verify new credentials and start a database session.
  //  Action #2: Continue a session on refresh button.
  //  Action #3: Provide a new form after adding a user.
  //  Action #4: Provide a new form after failing to add a user.
  // -----------------------------------------------------------------
  if (($authenticated) || (authenticate($userid,$passwd)))
  {
    // Assign inputs to variables.
    $newuserid = @$_POST['newuserid'];
    $newpasswd = @$_POST['newpasswd'];

    // Set message and write new credentials.
    if ((isset($newuserid)) && (isset($newpasswd)) &&
        (($code = verify_credentials($newuserid,$newpasswd)) !== 0))
    {
      // Render empty form with error message from prior attempt.
      addUserForm(array("code"=>$code
                       ,"form"=>"AddDbUser.php"
                       ,"userid"=>$newuserid));
    }
    else
    {
      // Create new user only when authenticated.
      if (!(isset($userid)) && (isset($_SESSION['userid'])))
       create_new_db_user($_SESSION['db_userid'],$newuserid,$newpasswd);

      // Render fresh empty form.
      addUserForm(array("form"=>"AddDbUser.php"));
    }
  }
  else
  {
    // Destroy the session and force re-authentication.
    session_destroy();

    // Redirect to the login form.
    signOnForm();
  }

네스트 처리된 if 구문은 $newuserid, $newpasswd 변수가 설정되어 있는지 확인하는 방법으로 create_new_db_user() 함수에 대한 호출을 필터링합니다. 이 두 변수는 AddDbUser.php 웹 페이지에만 설정될 수 있으며, 따라서 사용자가 인증되기 전까지는 호출될 수 없습니다. Add User 버튼을 클릭하면 동일한 웹 페이지가 재귀적으로 호출되므로, AddDbUser.php 웹 페이지가 다시 호출되었을 때에도 정상적인 동작을 보장할 수 있습니다.

  // Add a new user to the authorized control list.
  function create_new_db_user($userid,$newuserid,$newpasswd)
  {
    // Attempt connection and evaluate password.
    if ($c = @oci_connect(SCHEMA,PASSWD,TNS_ID))
    {
      // Check for prior insert, possible on Web page refresh.
      if (!is_inserted($c,$newuserid))
      {
        // Encrypt password.
        $newpassword = sha1($passwd);
        
        // Return database UID.
        $s = oci_parse($c,"INSERT INTO system_user
                           ( system_user_id
                           , system_user_name
                           , system_user_password
                           , system_user_group_id
                           , system_user_type
                           , created_by
                           , creation_date
                           , last_updated_by
                           , last_update_date )
                           VALUES
                           ( system_user_s1.nextval
                           , :newuserid
                           , :newpasswd
                           , 1
                           , 1
                           , :userid1
                           , SYSDATE
                           , :userid2
                           , SYSDATE)");

        // Bind the variables as strings.
        oci_bind_by_name($s,":newuserid",$newuserid);
        oci_bind_by_name($s,":newpasswd", $newpassword);
        oci_bind_by_name($s,":userid1",$userid);
        oci_bind_by_name($s,":userid2",$userid);

        // Execute the query, error handling should be added.
        if (!@oci_execute($s,OCI_COMMIT_ON_SUCCESS))
        {
          // Print error when oci_execute() fails.
          $errorMessage = "Check for a missing SYSTEM_USER table.<br />";
          print $errorMessage;
        }
      }

      // Close the connection.
      oci_close($c);

    }
    else
    {
      $errorMessage = oci_error();
      print htmlentities($errorMessage['message'])."<br />";
    }
  }

create_db_user() 함수 내에서 is_inserted() 함수에 대한 호출이 수행됩니다. 이를 통해 사용자를 SYSTEM_USER 테이블에 삽입하기 전에 사용자가 이미 존재하고 있는지 확인하게 됩니다. 앞의 네스트 처리된 함수의 예와 마찬가지로, is_inserted() 함수는 트랜잭션 컨트롤을 위해 로컬 연결을 공유합니다. 또 sha1() 함수는 사용자 패스워드를 변수와 바인딩하기 전에 일반 텍스트 포맷의 패스워드를 암호화된 문자열로 변환합니다.

새로운 사용자 레코드를 성공적으로 삽입한 후, New User폼이 다시 렌더링 되어 새로운 사용자를 추가로 입력하거나 Log Out 버튼을 클릭할 수 있게 합니다. Log Out 버튼을 누르면 로그인 화면이 표시되고 돌아오고 세션 ID가 리셋됩니다.

결론

지금까지 아이덴티티 관리 솔루션의 동작 원리와 개념, 기본적인 구현 방법과 사용자 인증을 위한 접근법에 대해 배워 보았습니다.

지금까지 구현한 솔루션으로 모든 사용자의 인증 및 접근을 일관성 있게 관리할 수 있습니다. 하지만 모든 사용자가 동일한 접근 권한을 갖는다는 것이 문제입니다. 어떤 사용자는 무제한 접근 권한을 가져야 할 수도 있고, 또 다른 사용자들은 매우 제한된 접근 권한을 가져야 할 수도 있습니다. 본 문서의 제 2 부에서는 FGA(fine-grained access)를 위한 두 가지 테크놀로지를 아이덴티티 관리 솔루션에 접목하는 방법을 배워 보기로 합니다.

오라클 데이터베이스의 VPD는 매우 최신의 기능을 제공하고 있지만, 보다 오래된 기술인 DBMS_APPLICATION_INFO 역시 Oracle8i, Oracle9i, Oracle Database 10g Release 1에서 모두 동작합니다. 또 Oracle E-Business 11i Suite 인증을 위한 코어 유틸리티로 지원되고 있기도 합니다. 이 두 가지 테크놀로지 모두 FGA 권한 및 역할을 정의하기 위한 유용한 도구로서 활용됩니다.

제 2 부로 가기


Michael McLaughlin <Oracle Database 10g Express Edition PHP Web Programming>, 의 저자이며 <Oracle Database 10g PL/SQL Programming>,<Expert Oracle PL/SQL> 의 공저자로 아이다호주의 브리검 영 유니버시티에서 컴퓨터 정보 테크놀로지 교수로 재직 중입니다.
E-mail this page
Printer View Printer View