차례
Acegi Security는 J2EE 기반의 엔터프라이즈 소프트웨어 애플리케이션에 대한 포괄적인 보안 솔루션을 제공해 준다. 여러분은 본 참조 가이드를 활용하는 만큼 우리가 여러분에게 유용하고 고도로 설정가능한 보안 시스템을 제공하려 노력했다는 것을 알게 될 것이다.
보안은 늘 움직이는 대상이며, 그리고 포괄적이며 시스템 수준(system wide)의 접근법을 추구하는 것이 중요하다. 보안의 테두리 내에서 우리는 여러분이 "보안 계층(layers of security)"을 도입하기를 권장하는데, 그렇게 함으로써 연속된 계층들이 추가적인 보안기능들을 제공함과 동시에 각 계층들이 자체적으로도 가능한한 안전한 상태를 유지해 나갈 수 있다. 각 계층에 대한 보안이 더 엄격하면 엄격할수록 여러분의 애플리케이션도 더 강건하고 안전하게 될 것이다. 가장 하위 수준에서는 여러분이 중간자 공격(man-in-the-middle attack)을 최소화하기 위하여 전송 보안(transportation security)과 시스템 식별(system identification)과 같은 문제들을 다루어야 할 것이다. 그렇게 한 다음에는 일반적으로 방화벽을 운용하여, 가능하면 VPN이나 IP 보안을 이용하여 오직 허가된 시스템만이 연결을 시도할 수 있도록 할 것이다. 기업 환경에서는 DMZ를 배치하여 외부에 공개되어 있는 서버들을 백엔드 데이터베이스 및 애플리케이션 서버로부터 분리할 수도 있다. 또한 여러분의 운영체제 역시 중요한 부분을 수행할 것이며, 가령 허가받지 않은 사용자가 실행한 실행중인 프로세스와 파일 시스템 보안을 극대화하는 것과 같은 문제들을 다룬다. 일반적으로 운영체제에도 자체적인 방화벽이 설정되어 있다. 어쩌면 여러분은 구석구석에서 시스템에 대한 서비스 거부(denial of service)와 무차별 대입 공격(brute force attack)을 방지하고자 할 수도 있을 것이다. 또한 침입 탐지 시스템(intrusion detection system)은 모니터링 및 공격에 대한 대응에 특히 유용한데, 침입 탐지 시스템을 이용하는 시스템들은 실시간으로 공격적인 TCP/IP 주소를 차단하는 등의 방어 조치를 취할 수 있다. 좀 더 상위 계층으로 올라가 보면 아마도 여러분의 자바 가상 머신(Java Virtual Machine)은 서로 다른 자바 유형에 대해 허가된 접근권한들을 최소화도록 설정되어 있을 것이며, 게다가 여러분의 애플리케이션에도 자체적으로 문제 영역에 특화된 보안 설정을 추가할 것이다. Acegi Security는 이러한 두 번째 영역-애플리케이션 보안-을 훨씬 더 용이하게 한다.
물론 여러분은 위에서 언급했던 모든 보안 계층들을 적절히 다루어야 할 필요가 있을 것이며, 더불어 모든 계층들을 둘러싸고 있는 관리상의 요소들도 함께 고려해야 할 것이다. 그러한 관리상의 요소들을 모조리 수록하고 있는 목록에는 보안 공고 (security bulletin) 감시, 패칭(patching), 신원조회(personnel vetting), 감사 (audits), 변경 제어(change control), 엔지니어링 관리 시스템(engineering management system), 데이터 백업(data backup), 재난 복구(disaster recovery), 성능 벤치마킹(performance benchmarking), 부하 모니터링(load monitoring), 통합 로깅(centralized logging), 침해 대응 절차(incident response procedures) 등이 포함될 것이다.
여러분이 Acegi Security를 엔터프라이즈 애플리케이션 보안 계층을 보조하는 데 집중시킨다면 여러분은 업무 문제 영역이 존재하는 만큼 서로 다른 수많은 요구사항들이 있다는 것을 알게 될 것이다. 은행 업무 애플리케이션에는 전자상거래 애플리케이션과는 다른 요구사항이 있으며 전자상거래 애플리케이션에는 기업 영업 부서 자동화 도구와는 다른 요구사항이 있을 것이다. 이러한 각각의 서로 다른 요구사항들은 애플리케이션 보안을 흥미롭고 해볼만한 것으로 만들어 준다.
본 참조 가이드는 1.0.0 버전의 Acegi Security 릴리즈를 대거 재구성한 것이다. I부인 전체적인 아키텍처는 모두 빠짐없이 읽어보길 바란다. 본 참조 가이드의 나머지 부분들은 좀 더 전통적인 레퍼런스 형식으로 구성되어 있으므로 여러분이 필요할 때마다 읽어볼 수 있도록 되어 있다.
우리는 본 참조 가이드가 여러분에게 유용하길 바라며, 그리고 여러분이 제안을 통해 피드백을 보내주는 것을 환영한다.
마지막으로 Acegi Security 커뮤니티에 온 것을 환영한다.
대부분의 소프트웨어와 마찬가지로 Acegi Security에도 프레임워크 구석구석에서 공통적으로 사용되는 어떤 중심이 되는 인터페이스와 클래스, 그리고 개념적으로 추상화한 것들이 포함되어 있다. 참조 가이드의 본 파트에서는 Acegi Security 통합을 성공적으로 계획하고 수행하는데 필수적인 이러한 중심 요소들을 검토하기 전에 먼저 Acegi Security에 관해 소개할 것이다.
Acegi Security는 J2EE 기반의 엔터프라이즈 소프트웨어 애플리케이션에 대한 포괄적인 보안 서비스를 제공한다. 특히 엔터프라이즈 소프트웨어 개발을 주도하고 있는 J2EE 솔루션인 Spring 프레임워크를 이용하여 개발되는 프로젝트를 지원한다는 장점이 있다. 여러분이 엔터프라이즈 애플리케이션을 개발하는데 있어 Spring 프레임워크를 사용하고 있지 않다면 Spring 프레임워크를 좀 더 가까이 지켜볼 필요가 있다고 말하고 싶다. 여러분이 Spring 프레임워크, 특히 의존성 주입(dependency injection) 원리에 익숙하다면 여러분은 Acegi Security에 훨씬 더 쉽게 익숙해질 것이다.
사람들이 Acegi Security를 사용하는 이유는 여러가지가 있는데, 대부분은 J2EE의 서블릿 스펙이나 EJB 스펙이 일반적인 엔터프라이즈 애플리케이션 시나리오에서 필요로 하는 수준의 기능이 부족하다는 것을 깨닫고 난 이후의 프로젝트에서부터 사용한다. 이러한 것들이 표준으로 언급되고 있기는 하지만, 이러한 표준들이 WAR나 EAR 차원의 이식성을 갖추고 있지 않다는 것을 인식하는 것이 중요하다. 그러므로 만약 여러분이 서버 환경을 교체한다면 보통 여러분이 개발한 애플리케이션의 보안 관련 설정들을 새로운 대상 환경에 맞게 재구성하기 위해 해야할 일들이 많을 것이다. Acegi Security를 사용하면 이러한 문제들을 극복할 수 있으며, 게다가 갖가지 다른 유용하고 완전히 조정가능한 보안 기능들을 사용할 수 있다.
아마 여러분도 알고 있겠지만, 보안을 구성하는 주요 오퍼레이션에는 두 가지가 있다. 그 중 첫 번째는 "인증(authentication)"으로 알려져 있으며 인증 주체(principal)가 누구라는 것을 주장하는 과정이다. 인증주체(principal)는 일반적으로 여러분의 애플리케이션에서 특정한 행위를 수행할 수 있는 사용자나, 디바이스 혹은 다른 시스템을 의미한다. 권한부여(authorization)는 인증주체가 여러분의 애플리케이션에서 특정 행위를 수행하도록 허가되었는지를 결정하는 과정을 의미한다. 권한부여에 대한 결정이 필요한 지점까지 도달하려면 그 전에 이미 인증주체의 신원이 인증절차에 의해 수립되어야 한다. 이러한 개념들은 공통적으로 사용되는 것들이며, Acegi Security에만 특화되어 있는 것은 아니다.
인증 차원에서 보았을 때 Acegi Security는 광범위한 인증 모델을 지원한다. 이러한 인증 모델들의 대다수는 써드 파티에서 제공하거나, 아니면 IETF(Internet Engineering Task Force)와 같은 관련 표준 단체에 의해 개발된다. 더불어 Acegi Security는 독자적인 인증 기능을 제공하고 있는데, 구체적으로 말하자면 현재 아래에 나열되어 있는 인증 모델들을 모두 지원한다:
HTTP BASIC 인증 헤더(IETF RFC 기반 표준)
HTTP 다이제스트(Digest) 인증 헤더(IETF RFC 기반 표준)
HTTP X.509 클라이언트 인증서 교환(IETF RFC 기반 표준)
LDAP (교차 플랫폼, 특히 여러 환경에서 인증을 해야 할 필요가 있을 경우의 가장 일반적인 접근법)
폼 기반 인증(단순한 사용자 인터페이스 요구사항에 적합)
Computer Associates Siteminder
JA-SIG 통합 인증 서비스(JA-SIG Central Authentication Service) (CAS로도 알려져 있는 대중적인 오픈 소스 싱글 사인 온(Single Sign On) 시스템)
RMI(원격 메소드 호출; Remote Method Invocation) 및 HttpInvoker(Spring 원격 프로토콜)에 대한 투명한 인증 컨텍스트 전달
자동 "remember-me" 인증(미리 정의되어 있는 시간동안은 재인증을 수행할 필요가 없음)
익명 인증(Anonymous authentication) (모든 요청이 자동적으로 일정한 보안 식별성을 지니고 있다고 가정하게 함)
Run-as 인증(만약 누군가가 다른 보안 신원을 이용하여 진행해야만 하는 요청에 유용)
자바 인증 및 권한부여 서비스(JAAS; Java Authentication and Authorization Service)
JBoss, Jetty, Resin, Tomcat과의 컨테이너 통합 (원한다면 컨테이너 관리 인증(Container Manager Authentication)을 그대로 사용할 수 있음)
여러분의 독자적인 인증 시스템 (하단 참조)
수많은 독립 소프트웨어 벤더(ISV; independent software vendors)들이 Acegi Security가 제공하는 풍부한 인증 모델로 인해 Acegi Security를 도입하고 있다. 그렇게 함으로써 독립 소프트웨어 벤더들은 최종 사용자가 원하는 어떤 것도 갖가지 기술적인 작업이나 클라이언트 환경을 변경하지 않고도 재빨리 자사의 솔루션에 통합할 수 있다. 만약 위의 인증 메커니즘들 중에서 여러분의 요구사항에 적합한 것이 없더라도 Acegi Security는 개방 플랫폼이므로 여러분은 여러분의 독자적인 인증 메커니즘을 상당히 간단하게 작성할 수 있다. 수많은 Acegi Security의 기업 사용자들은 특정 보안 표준을 따르지 않는 "레거시(legacy)" 시스템과 통합해야할 필요가 있는데 Acegi Security는 그러한 시스템에 대한 작업을 매우 용이하게 할 수 있다.
간혹 단순히 인증 절차만으로는 충분하지 않을 때가 있다. 간혹 여러분은 인증 주체가 여러분의 애플리케이션과 상호작용하는 방법에 기반하여 보안을 차별화해야할 때도 있다. 예를 들면 여러분은 훔쳐보는 자(eavesdropping)으로부터 비밀번호를 보호하거나 중간자 공격(man-in-the-middle attack)으로부터 최종 사용자를 보호하기 위해 HTTPS로만 요청이 도착한다는 것을 보장해야할 수도 있다. 아니면 여러분은 실제로 사람이 요청을 만들어 내며, 로봇이나 다른 자동화된 프로스세스에 의해 요청이 만들어지지 않는다는 것을 보장하고자 할 수도 있다. 이렇게 하는 것은 특히 무차별 대입 공격(brute force attacks)로부터 비밀번호 복구 프로세스를 보호하거나 아니면 단순히 사람들이 애플리케이션의 중요 내용을 복제하는 것을 어렵게 만드는데 도움이 될 것이다. 여러분이 이러한 목적들을 달성토록 하기 위해 Acegi Security는 인간 사용자 감지(human user detection)를 위한 JCaptcha 통합과 더불어 자동화된 "채널 보안(channel security)"을 완벽하게 지원한다.
인증이 어떻게 이루어지는 것에 관계없이 Acegi Security는 심도있는 권한부여 기능을 제공한다. 권한부여의 측면에는 세 가지 주요 영역이 존재하는데, 이러한 영역에는 웹 요청에 대한 권한부여, 호출가능한 메소드에 대한 권한부여, 개별 도메인 객체 인스턴스에의 접근에 대한 권한부여가 있다. 여러분이 각각의 차이점을 이해하는 것을 돕자면, 서블릿 스펙의 웹 패턴 보안, EJB 컨테이너 관리 보안(Container Managed Security) 및 파일 시스템 보안에서 찾아볼 수 있는 권한부여로 생각하면 된다. Acegi Security는 이러한 모든 중요 영역에 있어 깊이 있는 보안 기능들을 제공해 주며, 이러한 것들은 본 참조 문서의 후반부에서 알아볼 것이다.
Acegi Security는 2003년 후반에 시작되었는데, Spring 개발자 메일링 리스트에서 Spring 기반의 보안 구현체를 탑재할 계획이 있는지를 물어보는 질문으로부터 시작되었다. 당시 Spring 커뮤니티는 상대적으로 규모가 작았는데, 실제로 Spring 프레임워크 자체만이 2003년 초반부터 소스포지 프로젝트로서 존재해 왔다. 그 질문에 대한 대답은, 보안은 중요한 영역이며, 다만 현재는 시간이 부족해서 보안 영역을 개척하지 못하고 있다는 것이었다.
그러한 점을 염두에 두고 간단한 보안 구현체가 만들어 졌지만 릴리즈되지는 않았다. 몇 주 후 Spring 커뮤니티의 다른 멤버가 보안에 관해 물어보았으며 그 때 그들에게 코드가 제공되었다. 몇 가지 다른 요청사항들이 뒤따랐으며, 그리고 2004년 1월 즈음 20명의 사람들이 그 코드를 사용하였다. 이러한 개척자적인 성향의 사용자들이 소스포지 프로젝트를 제시한 다른 사람들에 의해 참여하였고, 2004년 3월 정식으로 만들어졌다.
프로젝트 초기에는 자체적인 인증 모듈이 아무것도 없었다. 인증 절차는 Container Managed Security에 의존하였으며, Acegi Security는 권한부여에만 집중하였다. 이렇게 하는 것이 처음에는 적합했지만 사용자가 추가적인 컨테이너 지원을 요청하는 것이 많아지면 많아질수록 컨테이너에 특화된 인증 영역 인터페이스의 근본적인 한계가 드러났다. 또한 컨테이너의 클래스패스에 새로운 JAR를 추가하는 것과 관련된 문제도 있었는데, 이는 최종 사용자가 혼동하여 잘못 설정하게 되는 공통적인 이유였다.
이어서 Acegi Security에 특화된 인증 서비스가 도입되었다. 그로부터 약 1년 후 Acegi Security는 공식적인 Spring 프레임워크의 하위 프로젝트가 되었다. 1.0.0 최종 릴리즈가 2006년 5월에 출시되었으며, 2년 반 뒤에는 실제로 수많은 제품 소프트웨어 프로젝트에서 사용되고 있으며 수백가지의 개선사항과 커뮤니티 지원이 이루어지고 있다.
오늘날 Acegi Security는 강력하고 활성화된 오픈소스 커뮤니티를 만끽하고 있다. 지원 포럼에는 수 천개의 Acegi Security에 관한 메시지가 있다. 14명의 개발자가 코드 자체에 대한 작업을 하고 있으며 활성화된 커뮤니티는 주기적으로 패치를 공유하고 사용자들을 지원하고 있다.
Acegi Security의 릴리즈 번호 부여 체계를 이해하는 것이 유용할 수 있는데, 왜냐하면
여러분이 프로젝트의 차기 릴리즈로 마이그레이션하는데 수반되는 노력이 얼마나
드는지(아니면 얼마만큼의 노력이 들지 않는지) 확인하는데 도움이 되기 때문이다.
공식적으로 우리는 아파치 포터블 런타임 프로젝트 버전 부여 지침을 이용하며,
이것은 http://apr.apache.org/versioning.html에서
확인할 수 있다. 여러분의 편의를 위해 링크한 페이지에 포함되어 있는 소개 내용을
인용해 본다.
“버전은 MAJOR.MINOR.PATCH의 표준 삼단계 정수(standard triplet integer)를 이용하여 나타낸다. 기본적인 의미를 설명하면 MAJOR 버전은 API에 호환되지 않는 대규모의 업그레이드를 수행한 것이며, MINOR 버전은 이전 마이너 버전과의 소스 및 바이너리 호환성은 유지한 것이고, PATCH 수준의 변경은 상하위 호환성을 완벽하게 유지한 버전이다.”
Acegi Security는 표준 Java 1.3 런타임 환경에서 실행되도록 작성되었다. Acegi Security는 Java 5.0도 지원하며 Java 5.0에 한정된 자바 유형은 JAR 파일명이 "tiger"로 끝나는 별도의 패키지에 들어있다. Acegi Security는 자급적인(self-contained) 방식으로 작동하는 것을 목표로 하므로 별도의 설정파일을 Java 런타임 환경에 추가할 필요는 없다. 특히 별도의 JAAS(Java Authentication and Authorization Service) 정책 파일을 설정하거나 Acegi Security를 공통 클래스 패스 위치에 위치시킬 필요도 없다.
이와 유사하게 여러분은 EJB 컨테이너나 서블릿 컨테이너를 사용하고 있는 경우에도 별도의 설정 파일을 어딘가에 집어넣거나 Acegi Security를 서버 클래스로더에 포함시킬 필요가 없다.
위 설계방식은 배포시 최대의 유연성을 제공하며, 따라서 여러분은 단순히 대상 산출물(JAR, WAR, EAR가 되는)을 한 시스템에서 다른 시스템으로 복사할 수 있으며, 즉시 작동할 것이다.
Acegi Security에서 가장 중요한 공유 컴포넌트(shared component)에 관해 몇 가지 알아보기로 하자. 컴포넌트가 프레임워크의 중심에 있으면 컴포넌트들은 "공유(shared)"되어 있는 것으로 여겨지며 프레임워크는 컴포넌트 없이는 동작할 수 없다. 이러한 자바 타입들은 시스템의 나머지 부분들에 대한 빌딩 블록을 나타내며, 따라서 여러분이 컴포넌트와 직접적으로 상호작용할 필요가 없을지라도 컴포넌트가 그곳에 존재하고 있다는 것을 이해하는 것이 중요하다.
가장 기초가 되는 객체는 SecurityContextHolder이다.
SecurityContextHolder는 우리가 현재 애플리케이션의
보안 컨텍스트에 대한 세부사항을 저장하는 곳인데,
현재 애플리케이션을 사용하고 있는 인증주체에 대한 세부사항을 포함하고 있다.
기본적으로 SecurityContextHolder는
ThreadLocal을 이용하여 이러한 세부사항들을 저장하며,
이는 보안 컨텍스트를 명시적으로 동일한 실행 쓰레드내의 메소드에 인자로
전달하지 않더라도 보안 컨텍스트를 언제든지 그러한 메소드에서 사용할 수
있다는 것을 의미한다. 이러한 방식으로 ThreadLocal을 이용하는
것은 현 인증주체의 요청을 처리한 다음 쓰레드를 초기화하더도 상당히 안전한 방법이다.
물론 Acegi Security가 자동적으로 그러한 것들을 처리하므로
여러분이 신경쓸 필요는 없다.
어떤 애플리케이션은 그 애플리케이션이 쓰레드와 동작하는 특정한 방식으로 인해
ThreadLocal을 이용하는 것이 전혀 맞지 않다.
예를 들어 Swing 클라이언트는 자바 가상 머신상의 모든 쓰레드가 동일한
보안 컨텍스트를 사용하기를 원할 수도 있다. 이 경우 여러분은
SecurityContextHolder.MODE_GLOBAL을
사용할 수 있다. 다른 애플리케이션은 안전한 쓰레드에 의해 만들어진
쓰레드들 역시 동일한 보안 식별성을 갖기를 원할 수도 있다.
이는 SecurityContextHolder.MODE_INHERITABLETHREADLOCAL을
사용하여 달성할 있다. 여러분은 기본
SecurityContextHolder.MODE_THREADLOCAL의 모드를
두 가지 방법으로 변경할 수 있다. 첫 번째 방법은 시스템 프로퍼티를
설정하는 것이다. 다른 방법은 SecurityContextHolder의
정적 메소드를 호출하는 것이며 대부분의 애플리케이션에서는 기본값을
변경할 필요가 없을 것이다. 하지만 여러분이 기본값을 변경하고자 한다면
SecurityContextHolder에 대한 JavaDoc을 참고하여
더 상세한 내용에 관해 알아보도록 한다.
SecurityContextHolder 내부에는 현 애플리케이션과
상호작용하고 있는 인증주체의 세부사항이 저장된다. Acegi Security는
Authentication 객체를 이용하여 이러한 정보들을
나타낸다. 일반적으로 여러분이 직접 Authentication 객체를 생성할 필요는
없는 반면, 사용자가 Authentication 객체를 조회하는 것은
것은 상당히 일상적인 일이다. 여러분은 애플리케이션 어디에서도
아래 코드를 사용하여 Authentication 객체를 조회할 수 있다:
Object obj = SecurityContextHolder.getContext().getAuthentication().getPrincipal();
if (obj instanceof UserDetails) {
String username = ((UserDetails)obj).getUsername();
} else {
String username = obj.toString();
}위 코드는 몇 가지 흥미로운 관계와 주요 객체들을 보여주고 있다. 먼저 여러분은
SecurityContextHolder와 Authentication 사이에서
매개자 역할을 하는 객체가 있다는 것을 알 수 있을 것이다.
SecurityContextHolder.getContext() 메소드는
실질적으로는 SecurityContext를 반환한다.
Acegi Security는 몇 가지 SecurityContext 구현체를
사용하는데, 이를 테면 인증 주체에 특화되지 않은 요청에 관계된
특수한 정보를 저장해야 할 경우에 사용하는 것이 있다. 이러한
구현체의 적절한 예를 하나 들자면 JCaptcha 통합(integration)이 있는데,
JCaptcha는 현재 요청을 보낸 사용자가 사람인지 아닌지를 알아야할 필요가
있을 때 사용한다. 그러한 결정은 인증 여부를 결정하는 인증 주체와는 전혀 무관하므로
우리는 그러한 결정들을 SecurityContext에 저장한다.
위 코드에서 한 가지 더 유념해야할 점은 여러분이
Authentication 객체로부터 인증 주체를 획득할 수도
있다는 것이다. 인증주체는 단순히 하나의 Object 타입 객체일 뿐이다.
대부분 이 객체는 UserDetails 객체로 캐스팅할 수 있다.
UserDetails는 Acegi Security의 중심에 위치하는 인터페이스이다.
UserDetails는 인증 주체를 나타내지만 확장가능하며
애플리케이션에 특화된 방식으로 나타내어 진다. 여러분은 UserDetails를
사용자 데이터베이스와 SecurityContextHolder에서
Acegi Security가 요구하는 것 사이에 위치하는 어댑터로 생각하도록 한다.
UserDetails는 사용자 데이터베이스의 무언가를 표현한 것이므로
여러분은 UserDetails를 상당히 자주 여러분의 애플리케이션에서
제공하는 원래의 객체로 캐스팅할 것이며, 따라서 여러분은 비즈니스에
특징적인 메소드(가령 getMail(),
getEmployeeNumber() 등과 같은)를 호출할 수 있다.
이제 여러분은 아마도 다음과 같은 의문을 가질것이다. 그러면
UserDetails 객체를 언제 제공하는가? 그리고 어떻게 제공하는가?
필자는 여러분이 이는 선언적이라서 아무런 자바 코드를 작성할 필요가
없다라고 말하리라 생각했다-그럼 무엇을 제공해야 하는가? 짧게 답을 말하자면
UserDetailsService라 불리는 특별한 인터페이스가
있다는 것이 답이다. 이 인터페이스에 들어있는 유일한 메소드는 문자열에 기반하고 있는
사용자명을 인자로 전달받아 UserDetails 객체를 반환한다.
Acegi Security에 탑재되어 있는 대부분의 인증 제공자들은 인증 절차의 일부로서
UserDetailsService에게 위임한다.
UserDetailsService는 SecurityContextHolder에
저장되는 Authentication 객체를 만드는데 사용된다.
한 가지 좋은 소식은 우리가 여러 가지 UserDetailsService 구현체들을
제공하고 있다는 것인데, 이러한 UserDetailsService에는
인 메모리 맵(in-memory map)을 사용하는 것과 JDBC를 이용하는 것들이 포함되어 있다.
대부분의 사용자들은 독자적인 구현체를 작성하려는 경향이 있는데,
그럼에도 불구하고 그러한 구현체들은 대개 단순히 기업 애플리케이션의
직원이나 고객, 아니면 다른 사용자들을 나타내는 기존 DAO(Data Access Object)위에
얹은 것에 불과하다. 여러분의 UserDetailsService가
반환하는 그 어떤 것도 위 코드에 나타나 있는 것과 같이 언제든지 그것들을
SecurityContextHolder에서 가져올 수 있다는 것의 이점을
염두에 두도록 한다.
인증 주체와 더불어 Authentication에서 제공되는
다른 중요한 메소드는 getAuthorities()이다.
이 메소드는 GrantedAuthority 객체의 배열을 반환한다.
GrantedAuthority는 놀랄 것도 없이 인증주체에 허가된
권한을 말한다. 이러한 권한들은 보통 "역할(role)"이며 가령
ROLE_ADMINISTRATOR나 ROLE_HR_SUPERVISOR를
예로 들 수 있다. 이러한 역할들은 차후에 웹 권한부여(web authorization)나
메소드 권한부여(method authorization), 도메인 객체 권한부여(domain object
authorization)에서 설정된다. Acegi Security의 다른 부분들에서는
이러한 권한부여를 해석하고 권한부여가 표현되도록 요청한다.
일반적으로 GrantedAuthority 객체들은
UserDetailsService에서 불러들인다.
일반적으로 GrantedAuthority 객체들은 애플리케이션 범위에
걸쳐있는 권한(permission)이다. GrantedAuthority
객체들은 주어진 도메인 객체에만 특화되어 있지는 않다. 따라서 여러분은
GrantedAuthority가 54번 Employee
객체에 대한 권한만을 나타내도록 하지 않을 것인데, 왜냐하면 만약
그러한 권한들이 수 천개에 걸쳐 존재할 경우 금방 메모리가 부족해질 것이기
때문이다(아니면 적어도 애플리케이션에서 사용자를 인증하는데 걸리는 시간이
길어질 것이다). 물론 Acegi Security는 확실히 이러한 일반적인 요구사항들을
처리할 수 있도록 설계되어 있으며, 대신 여러분은 이러한 목적으로 프로젝트의
도메인 객체 보안 기능을 사용할 수 있다.
마지막으로 말하기는 하지만 매우 중요한 것으로 간혹 여러분은 HTTP 요청간에
SecurityContext를 저장해야할 필요가 있을 것이다.
그렇지 않으면 인증 주체가 모든 요청에 대하여 재인증을 받을 것이다.
하지만 대부분의 경우에는 SecurityContext가 저장될 것이다.
HttpSessionContextIntegrationFilter는 HTTP 요청간의
SecurityContext를 저장할 책임을 맡고 있다.
클래스명으로 짐작해 볼 수 있듯이, 이러한 정보를 저장하는데에는
HttpSession이 사용된다. 여러분은 보안상 결코
HttpSession와 직접 소통해서는 안된다. 그렇게 하는 것은
절대로 정당화될 수 없으며, 항상 SecurityContextHolder를
대신 사용하도록 한다.
요약해 보면 Acegi Security의 주요 빌딩 블록들은 다음과 같다:
SecurityContextHolder : SecurityContext에
대한 모든 유형의 접근을 제공
SecurityContext : Authentication을 비롯하여
가능한한 요청에 특화되어 있는 보안 정보를 보관
HttpSessionContextIntegrationFilter : 웹 요청간
SecurityContext를 HttpSession에 저장
Authentication : Acegi Security에 특화된 방식으로
인증 주체를 표현
GrantedAuthority : 인증 주체에게 허가되어 있는 애플리케이션 범위의
권한을 나타냄
UserDetails : 애플리케이션 DAO로부터
Authentication 객체를 만들어 내기 위해 필요한 정보들을 제공
UserDetailsService : String 기반의 사용자명(username)이
(혹은 인증서 ID나 다른 유사한 것들) 전달될 경우 UserDetails를 생성
이제 여러분은 이러한 반복적으로 사용되는 구성요소들을 이해하였을 것이므로, 지금부터 인증 절차에 관해 좀 더 자세히 알아보기로 하자.
참조 가이드의 초반부에서 언급했던 것처럼 Acegi Security는 여러 가지 서로 다른 인증 환경에 관여할 수 있다. 우리는 Acegi Security를 인증에 사용할 경우 기존의 컨테이너가 관리하는 인증(Container Managed Authentication)과는 통합하지 않을 것을 권장하지만, 이러한 통합도 물론 지원된다(현재는 여러분의 독자적인 인증 시스템과도 통합하는 것도 지원하고 있다). 그럼 먼저 가장 복잡하면서도 가장 일반적인 상황을 보여주는, Acegi Security가 완전히 독자적으로 웹 보안을 관리한다는 관점에서 인증을 살펴보기로 하자.
일반적인 웹 애플리케이션의 인증 절차를 생각해 보면 다음과 같다:
여러분이 홈페이지에 방문하여 링크를 클릭한다.
서버로 요청이 전해지고, 서버는 여러분이 보호되고 있는 자원을 요청했는지를 판단한다.
지금은 여러분이 인증을 거치지 않았므으로 서버는 여러분이 인증해야 한다는 것을 나타내는 응답을 되돌려 준다. 응답은 HTTP 응답 코드이거나 특정 웹 페이지로 재지정(redirect)하게 할 것이다.
인증 메커니즘에 따라 여러분의 브라우저는 폼을 채울 수 있도록 특정 웹 페이지로 재지정을 하거나 아니면 어떻게 해서든 여러분의 ID를 받아낼 것이다 (예를 들면 기본 인증(BSIC authentication) 대화상자나 X509 인증서 등을 통해).
브라우저는 응답을 서버로 되돌려 보낼 것이다. 응답은 여러분이 입력한 폼의 내용을 포함하는 HTTP POST이거나 여러분의 인증 세부사항을 포함하는 HTTP 헤더가 될 것이다.
다음으로 서버는 여러분이 제시한 신원정보(credential)가 유효한지 판단할 것이다. 만약 신원정보가 유효하면 다음 단계가 일어날 것이고, 유효하지 않다면 대개 브라우저는 인증을 재시도하기 위해 신원정보를 재요청할 것이다(그러므로 여러분은 위의 두 번째 과정으로 되돌아간다).
여러분이 보냈던 원래 요청은 인증 절차가 재시도 되도록 할 것이다. 바라건대 여러분은 보호되고 있는 자원에 접근할 수 있는 충분한 허가 권한을 가지고 인증이 되었을지도 모른다. 만약 충분한 접근권한을 가졌다면 요청은 성공할 것이며, 그렇지 않으면 여러분은 "접근금지(forbidden)"을 의미하는 403 HTTP 오류 코드를 받게될 것이다.
Acegi Security에는 위에서 논의한 대부분의 단계들을 책임지는 명확히 구별되는
클래스들이 포함되어 있다. 이것과 관계되는 주요 클래스들(사용되어지는 순서대로)로는
ExceptionTranslationFilter,
AuthenticationEntryPoint 및 특정 인증 메커니즘
(authentication mechanisms)과
AuthenticationProvider가 있다.
ExceptionTranslationFilter는 던져지는 모든
Acegi Security 예외를 탐지할 책임을 맡고 있는 Acegi Security 필터이다.
보통 이러한 예외들은 AbstractSecurityInterceptor에
의해 던져지는데, AbstractSecurityInterceptor는
인증 서비스의 주요 제공자(provider)이다.
AbstractSecurityInterceptor는 다음 섹션에서 알아볼 것이며
지금은 단지 Java 예외를 만들어 낸다는 것만 알아두고,
HTTP나 인증 주체를 어떻게 인증하는지에 관해서는 알아야할 필요가 없다는 것만 알아두도록 한다.
대신 ExceptionTranslationFilter는 이러한 서비스를 제공하는 동시에,
403 오류 코드를 반환하거나(인증 주체가 인증을 받았으나 단순히 충분한 접근 권한만이
부족할 경우 - 위의 7번째 단계) AuthenticationEntryPoint를 실행하는
것과 같은 특정한 책임도 맡고 있다(인증 주체가 인증되지 않아 3번째 단계를
시작해야 하는 경우).
AuthenticationEntryPoint는 위 목록의 3번째 단계를
책임지고 있다. 여러분도 상상할 수 있듯이 각각의 웹 애플리케이션들은
기본적인 인증 전략(이러한 인증 전략은 Acegi Security 외에도 거의 모두
가지고 있을 것이나 지금은 단순하게 생각하기로 한다)을 가질 것이다.
각각의 주요 인증 시스템들은 독자적인 AuthenticationEntryPoint
구현체들을 가질 것이며 그러한 구현체들은 위의 3번째 단계에서 설명했던 것과
같은 동작을 취할 것이다.
여러분의 브라우저가 여러분의 인증 신원정보(HTTP 폼 전송이나 HTTP 헤더로서)를
전송하기로 결정하고 나면 서버상에서는 이러한 인증에 관한 세부사항들을
"수집(collect)"할 무언가가 필요하다. 현재 우리는 위 목록의 6번째 단계에 와 있다.
Acegi Security에는 사용자 에이전트(User-Agent)로부터 인증에 관한 세부사항을 수집하는 기능에
대한 특별한 이름이 있는데, 바로 "인증 메커니즘(authentication mechanism)"이다.
인증 세부사항을 사용자 에이전트로부터 수집하고 나면 "인증 요청(Authentication
request)" 객체가 만들어지고 AuthenticationProvider에게 제시된다.
Acegi Security 인증 과정의 마지막 역할은
AuthenticationProvider가 수행한다.
극도로 단순하게 생각하면 AuthenticationProvider는
Authentication 요청 객체를 받아 그것이 유효한지 여부를
판단할 책임이 있다. 제공자는 예외를 던지거나 아니면
완전한 내용이 담긴 Authentication 객체를 반환할 것이다.
우리의 좋은 친구들인 UserDetails와
UserDetailsService를 기억하는가? 만약 머릿속에 떠오르지
않는다면 이전 섹션으로 돌아가서 여러분의 기억을 더듬어 보도록 한다.
대부분의 AuthenticationProvider들은
UserDetailsService에게 요청하여
UserDetails 객체를 제공할 것이다. 앞에서 언급했던 것처럼
대부분의 애플리케이션은 독자적인 UserDetailsService를 제공하며,
어떤 것들은 Acegi Security에 탑재되어 있는 JDBC나 인 메모리(in-memory) 구현체를
이용할 수도 있을 것이다. 결과적으로 반환되는 UserDetails 객체 - 그리고
특히 UserDetails 객체에 포함되어 있는 GrantedAuthority[]-는
완전한 내용이 담긴 Authentication 객체를 만들 때 사용될 것이다.
인증 메커니즘이 완전한 내용이 들어 있는(fully-populated)
Authentication 객체를 되돌려 받고 나면 인증 메커니즘은
요청을 유효한 것으로 간주하여 Authentication을
SecurityContextHolder에 집어넣어 원래의 요청이 재시도
되도록 할 것이다(위 목록의 7번째 과정). 한편 AuthenticationProvider가
요청을 거부할 경우 인증 메커니즘은 사용자 에이전트에게 재시도하도록
요청할 것이다(위 목록의 2번째 과정).
이러한 내용들은 일반적인 인증 작업흐름(workflow)을 설명해 주는데,
한 가지 좋은 소식은 Authentication을
SecurityContextHolder에 집어넣는 방법에 관해서는
Acegi Security가 아무런 신경을 쓰지 않는다는 것이다. 오직 한 가지 중요한
요구사항은 SecurityContextHolder가
AbstractSecurityInterceptor가 요청을 인증해야 할
필요가 있기 전에 인증 주체를 나타내는 Authentication을
포함하고 있어야 한다는 것이다.
여러분(그리고 많은 사용자들은)은 직접 독자적인 필터나 MVC 컨트롤러를 작성하여 Acegi Security에 기반하지 않는 인증 시스템을 이용하여 상호운용성(interoperability)를 제공할 수 있다. 예를 들면 여러분은 ThreadLocal이나 JNDI 로케이션으로부터 현재 사용자에 관한 정보를 사용할 수 있도록 해주는 컨테이너 관리 인증을 이용하고 있을 수도 있다. 아니면 여러분은 전사적인 "표준"이라서 여러분이 제어할 수 있는 여지가 적은 레거시 전용 인증 시스템을 갖고 있는 회사에서 일하고 있을 수도 있다. 그러한 상황에서는 여전히 인증 기능을 제공하면서 Acegi Security를 작동하게 하는 것은 어렵지 않다. 오직 여러분이 해야 할 일은 특정 위치에서 써드 파티 사용자 정보를 읽어들이고, Acegi Security에 특화된 Authentication 객체를 만들고, 그것을 SecurityContextHolder에 집어넣을 필터(혹은 필터에 상응하는)를 하나 작성하는 것 뿐이다. 이렇게 하는 것은 상당히 쉽고 또 완전히 지원되는 통합 접근법이다.
만약 여러분이 AOP에 익숙하다면 여러분은 어드바이스(advice) 타입이 before, after, throws와 around로 여러 가지가 있다는 것을 알고 있을 것이다. around 어드바이스는 매우 유용한데, 왜냐하면 어드바이저(advisor)가 메소드 호출의 진행 여부, 응답의 변경 여부, 그리고 예외를 던질지를 결정할 수 있기 때문이다. Acegi Security는 메소드 호출 뿐만 아니라 웹 요청에 대해서도 around 어드바이스를 제공해 준다. 우리는 AOP 연합을 이용하여 메소드 호출에 대한 around 어드바이스를 적용하였으며 웹 요청에 대해서는 표준 필터를 이용하여 around 어드바이스를 적용하였다.
AOP에 익숙하지 않은 사용자가 이해하는데 있어 중요한 점은 Acegi Security가 메소드 호출 뿐만 아니라 웹 요청을 보호하는 데에도 이바지할 수 있다는 것이다. 대부분의 사람들은 서비스 계층상의 메소드 호출을 보호하는데 관심이 있다. 이렇게 되는 이유는 현 세대의 J2EE 애플리케이션에는 대부분의 비즈니스 로직들이 서비스 계층에 존재하기 때문이다(정확히 말하면 프로그래머들은 이러한 설계에 찬성하지 않으며 대신 DTO와 어셈블리(assembly), 퍼사드(facade), 및 투명한 퍼시스턴스 패턴(transparent persistence patterns)와 더불어 적절히 캡슐화된 도메인 객체를 지지한다. 그러나 빈약한(anemic) 도메인 객체가 현재의 주류 접근법이므로 여기에서는 빈약한 도메인 객체에 관해 논의하겠다). 여러분이 단지 서비스 계층에 대한 메소드 호출만을 보호할 할 필요가 있다면 Spring의 표준 AOP 플랫폼(아니면 AOP 연합으로 알려져 있는)을 이용하는 것이 적당할 것이다. 그렇지 않고 도메인 객체를 직접적으로 안전하게 하고자 할 필요가 있다면 여러분은 AspectJ가 더 적절할지에 관해 고려해 보아야 할 것이다.
여러분은 AspectJ나 AOP 연합(AOP Alliance)을 이용하여 메소드 인증을 수행하거나 결정할 수 있으며, 아니면 필터를 이용하여 웹 요청에 대한 인증을 수행하도록 결정할 수 있다. 여러분은 이러한 접근방법들을 함께 사용하지 않거나 하나 혹은 둘, 아니면 세 가지를 함께 사용할 수도 있다. 주로 사용되는 방법은 몇 가지 웹 요청 인증을 수행하며, 서비스 계층상에서는 몇몇 AOP 연합의 메소드 호출 인증을 묶는 것이다.
Acegi Security는 보안을 적용할 수 있는 모든 객체에 대하여
"안전 객체(secure object)"라는 용어를 사용하고 있다. Acegi Security에서
지원하는 각각의 안전 객체들은 전용 클래스를 가지고 있는데, 이러한
클래스들은 AbstractSecurityInterceptor의 하위 클래스이다.
중요한 것은 AbstractSecurityInterceptor가 실행되기 전까지는
인증주체가 인증되었을 경우 SecurityContextHolder가 유효한
Authentication을 포함할 것이라는 점이다.
AbstractSecurityInterceptor는 안전 객체 요청을 처리하는데 있어
일관성있는 작업흐름을 제공한다. 이러한 작업흐름에는 현재 요청과 연관되어 있는
"설정 속성(configuration attributes)"를 찾아보는 것이 포함된다.
"설정 속성"은 일종의 AbstractSecurityInterceptor에 의해 사용되는
클래스에 대해 특별한 의미를 지닌 문자열(String)로 생각할 수 있다. 설정 속성들은
일반적으로 XML을 이용하여 여러분의 AbstractSecurityInterceptor에
설정된다. 아무튼 AbstractSecurityInterceptor는
AccessDecisionManager에게 "여기에 설정속성이 있습니다,
여기에 현재 Authentication 객체가 있습니다,
그리고 여기에 현재 요청의 세부사항이 있습니다 - 다음의 특정 인증주체가 다음의
특정한 오퍼레이션을 수행하도록 허가되었습니까?"라고 물어볼 것이다.
AccessDecisionManager가 요청을 허가하도록 결정한다고 가정하면
AbstractSecurityInterceptor는 정상적으로 그러한 요청을
처리하기만 할 것이다. 앞서 얘기했던 것과 같이 흔치는 않지만 사용자들이
SecurityContext내의 Authentication을 다른
Authentication로 교체하기를 원할 수도 있는데 이는
AccessDecisionManager가 RunAsManager을
요청함으로써 처리된다. 이렇게 하는 것은 꽤 보기드문 상황에서 유용한데, 가령 서비스
계층의 메소드가 원격 시스템을 호출하여 다른 id를 제시해야 하는 경우가 그러하다.
왜냐하면 Acegi Security가 자동적으로 한 서버에서 다른 서버로 보안 id를
전달하므로(여러분이 적절히 구성된 RMI나 HttpInvoker 원격
프로토콜 클라이언트를 사용하고 있다고 가정한다), 이렇게 하는 것은 상당히 편리하다.
안전 객체를 처리하여 반환하고 나면 - 메소드 호출 완료나 필터 체인
처리를 의미함 - AbstractSecurityInterceptor는
호출을 처리하는 마지막 한 번의 기회를 얻게 된다. 이 단계에서
AbstractSecurityInterceptor는 가능한한 반환 객체를
변경하는 것에 관심이 있다. 우리는 이렇게 되기를 원하는데, 왜냐하면
안전 객체 호출 과정상에서는 인증 결정이 내려질 수
없기 때문이다.
AbstractSecurityInterceptor가 고도로 플러거블(pluggable)하므로 필요에 따라 제어가
AfterInvocationManager으로 넘어가 실제로는
AfterInvocationManager가 객체를 변경할 것이다.
심지어 이 클래스는 객체를 완전히 교체하거나, 예외를 던지거나,
아무런 변경도 하지 않을 수도 있다.
AbstractSecurityInterceptor가 템플릿 클래스의 중앙에
자리잡고 있으므로 첫번째 그림은 AbstractSecurityInterceptor에
초점을 맞추고 있다는 것이 적절해 보인다.

그림 1: 주요 "안전객체" 모델
요청을 가로채어 인증하는데 있어 완전히 새로운 방법을 생각해 내는
개발자들만이 안전 객체를 직접적으로 사용해야 할 것이다. 예를 들면
메시징 시스템을 안전하게 호출하기 위한 새로운 안전 객체를 만들어
낼 수도 있다. 보안을 필요로 하고 또 호출을 가로채는 방법(AOP의
around 어드바이스와 같은 맥락에서)을 제공하는 그 어떤 것도 안전 객체로
만들어질 수 있다. 이미 언급했듯이 대부분의 Spring 애플리케이션에서는 단순히
완전한 투명성을 갖춘, 현재 지원되고 있는 세 가지 타입의 안전 객체
(AOP 연합 MethodInvocation,
AspectJ JoinPoint 및
웹 요청 FilterInterceptor)를 사용하기만 하면 될 것이다.
이 장에서는 Acegi Security에서 사용되고 있으며 Acegi Security의 기능들을 보충하고 보조해 주는 몇 가지 지원 인프라스트럭처들을 소개할 것이다. 여기에서 소개하는 기능들이 보안에 직접적으로 관련되어 있지는 않더라도, 이미 Acegi Security 프로젝트에 포함되어 있기 때문에 이 장에서 논의할 것이다.
Acegi Security는 최종 사용자가 보게 될 예외 메시지에 대한 지역화를 지원한다. 여러분의 애플리케이션이 영어 문화권 사용자에 맞춰져 있다면 기본적으로 모든 Acegi Security 메시지는 영어이므로 여러분이 해야할 일은 없다. 만일 여러분이 다른 로케일을 지원해야 하는 경우라면, 본 섹션에 여러분이 알아야 할 것들은 모두 들어있다.
모든 예외 메시지들은 지역화가 가능하며, 여기에는 인증 실패와 접근 거부(권한부여 실패)와 관계된 메시지들이 포함된다. 개발자나 시스템 배포자 등에 초점을 맞추고 있는 예외와 로깅(잘못된 속성, 인터페이스 계약 위반, 잘못된 생성자 사용, 시작시간 유효성 검증, 디버그 차원의 로깅을 포함)은 지역화되지 않으며 대신 Acegi Security 코드에 영어로 하드코딩 되어 있다.
여러분은 acegi-security-xx.jar 파일에서
messages.properties 파일을 포함하고 있는
org.acegisecurity 패키지를 발견할 수 있을 것이다.
이 파일은 ApplicationContext에서 참조되어야 하는데,
왜냐하면 Acegi Security 클래스들이 Spring의 MessageSourceAware
인터페이스를 구현하고 있으며 애플리케이션 컨텍스트 구동시 메시지
리졸버(message resolver)에 대한 의존성 주입이 이루어지기 때문이다.
일반적으로 여러분이 해야할 일은 애플리케이션 컨텍스트내의 빈이 메시지를
가리키도록 등록하는 것 뿐이다. 아래에 그러한 예가 나타나 있다:
<bean id="messageSource" class="org.springframework.context.support.ReloadableResourceBundleMessageSource"> <property name="basename"><value>org/acegisecurity/messages</value></property> </bean>
messages.properties는 표준 리소스 번들에 따라
지어진 이름이며 Acegi Security 메시지에 의해 지원되는 기본 언어를
표현한다. 이러한 기본 파일은 영어로 작성되어 있다. 여러분이 메시지
소스를 등록하지 않더라도 Acegi Security는 적절히 작동할 것이며
영어로 하드코딩되어 있는 버전의 메시지로 대체(fallback)할 것이다.
만약 여러분이 messages.properties 파일을
커스터마이즈하려 하거나 아니면 다른 언어를 지원하고자 한다면 먼저
파일을 복사한 다음 그 파일의 이름을 알맞게 변경하여 위의 빈 정의에
등록해야 한다. 이 파일 안에는 상당히 많은 양의 메시지 키가 들어 있으므로
지역화를 주 시작점으로 삼아서는 안된다. 여러분이 이 파일에 대한 지역화를
수행할 경우 JIRA 태스크에 로깅하고, 여러분이 알맞게 이름을 부여한 버전의
지역화 messages.properties 파일을 첨부하여 여러분의
작업 결과물을 커뮤니티상에서 협업하는 것을 고려해 보길 바란다.
지역화에 대한 논의는
org.springframework.context.i18n.LocaleContextHolder로
알려져 있는 Spring의 ThreadLocal로 마무리 하기로 하자.
여러분은 LocaleContextHolder가 각 사용자가 선호하는
Locale을 나타내도록 설정해야 한다. Acegi Security는
ThreadLocal로부터 획득한 Locale을 이용하여
메시지 소스로부터 메시지를 가져오려 할 것이다.
LocaleContextHolder 사용법과 자동으로
Locale을 설정할 수 있도록 해주는
헬퍼 클래스(예,
AcceptHeaderLocaleResolver,
CookieLocaleResolver,
FixedLocaleResolver,
SessionLocaleResolver 등)에 관해 자세히 알아보려면
Spring 문서를 참조하도록 한다.
Acegi Security는 여러 가지 필터를 사용하는데, 필터에 관해서는 본 참조 가이드의 나머지
부분에 걸쳐 설명할 것이다. 여러분은 이러한 필터들이 웹 애플리케이션에
추가되는 방법을 선택할 여지가 있는데, 여러분은
FilterToBeanProxy나 FilterChainProxy를
사용할 수 있다. 두 가지 모두 아래에서 알아볼 것이다.
대부분의 필터들은 FilterToBeanProxy를 이용하여 설정한다.
다음은 web.xml의 설정 예이다:
<filter>
<filter-name>Acegi HTTP Request Security Filter</filter-name>
<filter-class>org.acegisecurity.util.FilterToBeanProxy</filter-class>
<init-param>
<param-name>targetClass</param-name>
<param-value>org.acegisecurity.ClassThatImplementsFilter</param-value>
</init-param>
</filter>web.xml에 들어 있는 필터는 실제로는 FilterToBeanProxy이며 실질적으로
필터의 로직을 구현할 필터는 아니다. FilterToBeanProxy가 하는 일은
Spring 애플리케이션 컨텍스트로부터 획득한 빈에 필터의 메소드를
위임하는 것이다. 이렇게 하면 빈이 Spring 애플리케이션 컨텍스트 라이프 사이클 지원과
설정의 유연함으로부터 오는 이점을 얻을 수 있다.
빈은 javax.servlet.Filter를 구현해야 한다.
FilterToBeanProxy는 오직 하나의 초기화 매개변수만을 필요로 하는데,
초기화 매개변수는 targetClass나 targetBean이다.
targetBean이 빈의 이름으로 객체를 정하는 반면,
targetClass 매개변수는 애플리케이션 컨텍스트의 첫 번째 객체를
지정된 클래스에 위치시킨다. 보통의 Spring 웹 애플리케이션들과 같이
FilterToBeanProxy는
WebApplicationContextUtils.getWebApplicationContext(ServletContext)를 통해
애플리케이션 컨텍스트에 접근하므로 여러분은 web.xml에
ContextLoaderListener를 설정해야만 한다.
서블릿 컨테이너 대신 IoC 컨테이너상에서 Filter를
작동하게 하는 경우 고려해야할 라이프 사이클에 관련된 문제가 하나 있다.
구체적으로 말하자면 어느 컨테이너가 Filter의
"구동" 및"종료" 메소드를 호출할 책임이 있느냐이다. 필터의 초기화 및 소멸 순서는
서블릿 컨테이너마다 매우 다양할 수 있는데, 따라서 이렇게 될 경우 한
Filter가 앞서 초기화된 Filter에 의해
수립된 구성 설정에 의존하는 경우 문제가 발생할 수 있는 것으로 알려져 있다.
반면 Spring IoC 컨테이너는 잘 알려진 인터페이스 계약,
예상 가능한 메소드 호출 순서, 빈 자동묶기(autowiring) 지원, 그리고
Spring 인터페이스 구현을 생략할 수 있는 선택권(예, Spring XML의
destroy-method 메소드) 뿐만 아니라 좀 더 포괄적인
라이프 사이클/IoC 인터페이스(InitializingBean,
DisposableBean, BeanNameAware,
ApplicationContextAware 등과 같은)를 가진다.
이러한 연유로 가능하면 서블릿 컨테이너 라이프 사이클 서비스 보다는 Spring의
라이프 사이클 서비스를 사용할 것을 권장한다. 기본적으로
FilterToBeanProxy는 프록시화된 빈에
init(FilterConfig)과 destroy() 메소드를
위임하지 않을 것이다. 여러분이 이러한 메소드 호출이 위임되기를 원한다면
lifecycle 초기화 매개변수를
servlet-container-managed로 설정한다.
FilterToBeanProxy를 사용하기 보다는
FilterChainProxy를 사용하길 강력히 권장한다.
FilterToBeanProxy가 매우 유용한 클래스이긴 하지만
문제는 몇 가지 필터를 더 사용하게 될 경우 web.xml 파일 안의
<filter>와 <filter-mapping>에
입력해야할 코드 라인이 폭발적으로 증가한다는 점이다. 이러한 문제를 극복하기 위해
Acegi Security는 FilterChainProxy 클래스를 제공하고 있다.
FilterChainProxy는 FilterToBeanProxy를
이용하여 묶이기는 하나(바로 위 예제에서 처럼), 대상 클래스가
org.acegisecurity.util.FilterChainProxy이다.
필터 체인은 아래와 같은 코드를 이용하여 애플리케이션 컨텍스트내에 선언한다:
<bean id="filterChainProxy" class="org.acegisecurity.util.FilterChainProxy">
<property name="filterInvocationDefinitionSource">
<value>
CONVERT_URL_TO_LOWERCASE_BEFORE_COMPARISON
PATTERN_TYPE_APACHE_ANT
/webServices/**=httpSessionContextIntegrationFilterWithASCFalse,basicProcessingFilter,exceptionTranslationFilter,filterSecurityInterceptor
/**=httpSessionContextIntegrationFilterWithASCTrue,authenticationProcessingFilter,exceptionTranslationFilter,filterSecurityInterceptor
</value>
</property>
</bean> 여러분이 FilterSecurityInterceptor를 선언하는 방식과
비슷하다는 것을 알아챌 지도 모르겠다. 둘 모두 정규 표현식과 Ant 스타일의 Path를
지원하며 가장 구체적인 URI가 먼저 나타난다. 런타임시에는
FilterChainProxy가 현재 요청되는 웹 요청과 일치하는
첫 번째 URL 패턴을 정할 것이다. 각 해당 설정 속성들은 애플리케이션
컨텍스트내에 정의되어 있는 빈의 이름을 나타낸다. 그런 다음 필터들은 정해진
FilterChain 행위에 따라 지정된 순서대로 호출될
것이다(Filter가 처리를 끝내고자 할 경우에는
체인에 대한 처리를 진행하지 않도록 결정할 수도 있다).
여러분도 알 수 있겠지만 FilterChainProxy는 서로 다른 요청 패턴에 대한
중복된 필터 이름을 필요로 한다(위 예제에서는 exceptionTranslationFilter와
filterSecurityInterceptor가 중복되어 있다).
설계상 이렇게 하도록 결정함으로써 FilterChainProxy는
서로 다른 URI 패턴에 대해 서로 다른 Filter 호출 순서를
지정할 수 있고 표현력을 향상시킬 수 있으며(정규 표현식, Ant 스타일의 Path,
커스텀 FilterInvocationDefinitionSource 구현 관점에서),
그리고 어떠한 Filter들이 호출되어야 하는지를 명확하게 할 수 있다.
여러분이 필터 체인 안에 두 개의 HttpSessionContextIntegrationFilter가
선언되어 있다는 것을 알고 있을지도 모르겠다(ASC는 allowSessionCreation의 줄임말이며,
HttpSessionContextIntegrationFilter의 프로퍼티이다). 웹 서비스는 차후에
이루어지는 요청에는 jsessionid를 제시하지 않을 것이므로 사용자 에이전트에 대한
HttpSession를 생성하는 것은 낭비일 수 있다. 만약 여러분이 최대의
확장성(scalability)를 요하는 대형(high-volume) 애플리케이션을 개발하고 있다면
위에 나타나 있는 접근법을 취할 것을 권장한다. 규모가 더 작은 애플리케이션에는
단일 HttpSessionContextIntegrationFilter
만으로도(기본 allowSessionCreation은
true로 지정) 충분할 것이다.
라이프 사이클 문제와 관련하여, FilterChainProxy에 대해
init(FilterConfig)와 destroy()
메소드가 호출되었다면, FilterChainProxy는
항상 그러한 메소드들을 기저의 Filter에 위임할 것이다.
이 경우 FilterChainProxy는
FilterInvocationDefinitionSource에 의해 몇 번이나 선언되었는지와는
관계없이 각 Filter에 대한 초기화와 종료가 한 번만
이루어 지도록 보장해 준다. 여러분은 FilterChainProxy를
프록시화하는 FilterToBeanProxy에 들어있는
lifecycle 초기화 매개변수를 통해 이러한 메소드들의
호출 여부에 대한 총괄적인 선택권을 제어한다. 위에서 논의한 바와 같이
기본적으로는 어떠한 서블릿 컨테이너 라이프사이클 호출도
FilterChainProxy로 위임되지는 않는다.
또한 여러분은 <URI Pattern> = <Filter Chain>
표현식 우측편에 #NONE# 토큰을 사용하여 필터 체인에서
URI 패턴을 생략할 수도 있다. 예를 들어 위 예제의 경우에는 여러분이
/webservices 위치를 완전히 생략하고자 한다면,
여러분은 빈 설정에서 해당 라인을 아래와 같이 변경할 수 있다:
/webServices/**=#NONE#
한 가지 알아둘 것은 이 패턴과 일치하는 것은 모두 아무런 인증이나 권한부여 서비스가 적용되지 않으므로 누구나 이곳에 자유로이 접근할 수 있을 것이라는 점이다.
web.xml에 필터가 정의되는 순서는 매우 중요하다.
실제로 여러분이 어떠한 필터를 사용하고 있는지와는 관계없이
<filter-mapping>의 순서는 아래와 같아야 한다:
ChannelProcessingFilter :
다른 프로토콜로 재지정(redirect)해야할 수도 있기 때문임
ConcurrentSessionFilter :
SecurityContextHolder의 기능을 사용하지는 않지만,
SessionRegistry를 갱신하여 현재 인증주체로부터 진행되고
있는 요청을 나타낼 필요가 있기 때문임
HttpSessionContextIntegrationFilter :
SecurityContext가 웹 요청 시작시
SecurityContextHolder내에 설정될 수 있으며,
웹 요청이 종료되는 경우(다음 웹 요청 사용을 준비) SecurityContext에
대한 모든 변경사항들이 HttpSession에 복사될 수 있음
인증 처리 메커니즘(Authentication processing mechanisms) :
AuthenticationProcessingFilter,
CasProcessingFilter,
BasicProcessingFilter, HttpRequestIntegrationFilter, JbossIntegrationFilter 등을
지정하여 SecurityContextHolder에
유효한 Authentication 요청 토큰이 포함되도록 변경할 수 있음.
SecurityContextHolderAwareRequestFilter :
여러분이 SecurityContextHolderAwareRequestFilter을 이용하여
서블릿 컨테이너에 Acegi Security를 인식하는
HttpServletRequestWrapper을 설치하는 경우
RememberMeProcessingFilter :
이전에 SecurityContextHolder을 갱신하는
인증 처리 메커니즘이 없었으며, 요청이 remember-me 서비스를 발생시키는
쿠키를 제시하여 적절히 기억된 Authentication 객체가
놓여지는 경우
AnonymousProcessingFilter :
이전에 SecurityContextHolder를 갱신하는
인증 처리 메커니즘이 없었으며, 익명 Authentication
객체가 놓여지는 경우
ExceptionTranslationFilter :
Acegi Security 예외를 잡아내어 HTTP 오류 응답이 반환되거나
적절한 AuthenticationEntryPoint가
실행될 수 있도록 하기 위함
FilterSecurityInterceptor : 웹 URI를 보호하기 위함
위의 필터들은 모두 FilterToBeanProxy이거나
FilterChainProxy이다.
각 애플리케이션에 대한 하나의 FilterChainProxy를
통해 하나의 FilterToBeanProxy만을 프록시화할 것을 권장하며,
그러한 FilterChainProxy가 모든 Acegi Security
Filter들을 정의하도록 한다.
만약 여러분이 SiteMesh를 사용하고 있다면 SiteMesh 필터가 호출되기 전에
Acegi Security 필터가 실행되도록 한다. 이렇게 하면 SiteMesh 데코레이터에
의해 사용되는 적절한 시점에 SecurityContextHolder에
정보들이 설정되도록 할 수 있다.
애플리케이션의 인증 및 권한부여 요구사항을 조정하는 것에 더불어
Acegi Security는 인증되지 않은 웹 요청이 특정한 속성을 갖게끔 할 수 있다.
이러한 속성들은 특정 전송 타입이 되는 것을 포함할 수도 있으며,
HttpSession의 상세한 속성 집합 등과 같은 것들을 포함한다.
대부분의 일반적인 요구사항은 여러분의 웹 요청이 특정한 전송 프로토콜, 가령
HTTPS와 같은 전송 프로토콜을 이용하여 수신하도록 한다.
전송 보안에서 고려해야할 한 가지 중요한 문제는 세션 하이재킹(session hijacking)이다.
여러분의 웹 컨테이너는 쿠키나 URL 재작성(URI rewriting)을 통해 사용자 에이전트로
전송되는 jsessionid를 참조하여 HttpSession을 관리한다.
만약 jsessionid가 HTTP를 통해 전송된적이 없다면
사용자가 인증 절차를 완료하고 난 다음 세션 식별자가 중간에 가로채여
사용자를 가장할 가능성이 있다. 이는 대부분의 웹 컨테이너가 주어진 사용자에 대해
동일한 세션 식별자를 유지하기 때문인데, 심지어 HTTP에서 HTTPS 페이지로
교체한 후에도 그러하다.
여러분의 개별 애플리케이션에서 세션 하이재킹이 매우 중대한 문제로 받아들여 진다면,
유일하게 선택할 수 있는 것은 모든 요청에 대해 HTTPS를 이용하는 것 뿐이다.
이는 안전하지 않은 채널로는 결코 jsessionid가 전송되지 않을 것을 의미한다.
여러분은 web.xml에 정의되어 있는 <welcome-file>이
HTTPS 위치를 가리키도록 할 필요가 있는데, 그렇게 하면 애플리케이션은 절대로
사용자를 HTTP 위치로 재지정하지는 않을 것이다. Acegi Security는 후자에 관한
문제를 다루는데 도움되는 솔루션을 제공하고 있다.
Acegi Security의 채널 보안 서비스를 이용하려면 다음 라인들을
web.xml에 추가한다:
<filter>
<filter-name>Acegi Channel Processing Filter</filter-name>
<filter-class>org.acegisecurity.util.FilterToBeanProxy</filter-class>
<init-param>
<param-name>targetClass</param-name>
<param-value>org.acegisecurity.securechannel.ChannelProcessingFilter</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>Acegi Channel Processing Filter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
평소대로 FilterToBeanProxy를 실행하는 경우와 같이 여러분은
애플리케이션 컨텍스트에 필터를 구성할 필요가 있을 것이다:
<bean id="channelProcessingFilter" class="org.acegisecurity.securechannel.ChannelProcessingFilter">
<property name="channelDecisionManager"><ref bean="channelDecisionManager"/></property>
<property name="filterInvocationDefinitionSource">
<value>
CONVERT_URL_TO_LOWERCASE_BEFORE_COMPARISON
\A/secure/.*\Z=REQUIRES_SECURE_CHANNEL
\A/acegilogin.jsp.*\Z=REQUIRES_SECURE_CHANNEL
\A/j_acegi_security_check.*\Z=REQUIRES_SECURE_CHANNEL
\A.*\Z=REQUIRES_INSECURE_CHANNEL
</value>
</property>
</bean>
<bean id="channelDecisionManager" class="org.acegisecurity.securechannel.ChannelDecisionManagerImpl">
<property name="channelProcessors">
<list>
<ref bean="secureChannelProcessor"/>
<ref bean="insecureChannelProcessor"/>
</list>
</property>
</bean>
<bean id="secureChannelProcessor" class="org.acegisecurity.securechannel.SecureChannelProcessor"/>
<bean id="insecureChannelProcessor" class="org.acegisecurity.securechannel.InsecureChannelProcessor"/>
FilterSecurityInterceptor와 같이 아파치 Ant 형식의 패스도
ChannelProcessingFilter에서 지원한다.
ChannelProcessingFilter는 모든 웹 요청을 필터링하고,
적용되고 있는 설정 속성을 결정함으로써 작동된다. 그런 다음
ChannelDecisionManager에 위임한다. 기본 구현체인
ChannelDecisionManagerImpl은 대부분의 경우를 처리하기에 충분하다.
ChannelDecisionManagerImpl은 단순히 설정되어 있는
ChannelProcessor 인스턴스 목록에 위임한다.
ChannelProcessor는 요청을 검토하여 요청이 적절하지 않을
경우(예를 들면, 적절하지 않은 전송 프로토콜을 통해 수신받는 등) 재지정을 수행한 다음,
예외를 던지거나 다른 적절한 조치를 취할 것이다.
Acegi Security에는 두 개의 구상 ChannelProcessor 구현체가 포함되어 있다.
SecureChannelProcessor는 REQUIRES_SECURE_CHANNEL 설정 속성이
포함된 요청을, InsecureChannelProcessor는 REQUIRES_INSECURE_CHANNEL 설정 속성이
포함된 요청을 각각 HTTP상으로 전달받았는지를 확인한다.
두 구현체 모두 필요한 전송 프로토콜이 사용되지 않았을 경우
ChannelEntryPoint로 위임한다. Acegi Security에 포함되어 있는 두 개의
ChannelEntryPoint 구현체는 단순히 요청을 적절한 HTTP와 HTTPS로 재지정하기만 한다.
ChannelProcessor에는 적절한 기본값들이 할당되어 있는데,
그러한 기본값들은 ChannelProcessor가 위임하는
ChannelEntryPoint에 작용하는 설정 속성 키워드들이며,
여러분은 이러한 기본값들을 애플리케이션 컨텍스트를 이용하여 재정의할 수도 있다.
한 가지 주의할 점은 재지정은 절대주소로 해야 하며(예, http://www.company.com:8080/app/page),
상대주소(예, /app/page)로 하면 안된다는 것이다. 테스트하는 동안
사용하는 포트도 함께 변경하는 재지정 명령이 인터넷 익스플로러 6 서비스 팩 1에서 적절하지 않게
응답하는 버그를 발견하였다. 따라서 기본적으로 수 많은 Acegi Security 빈과 묶이는
PortResolverImpl에서는 버그 탐지 로직과 관련하여 절대 URL을 사용하고 있다.
PortResolverImpl에 관한 자세한 사항은 JavaDoc을 참조하길 바란다.
여러분은 로그인이 이루어지는 동안 사용자명과 비밀번호를 보호하기 위해서는
안전 채널(secure channel)을 이용할 것을 권장하고 있음을 알아두도록 한다.
만약 여러분이 폼 기반 로그인에 ChannelProcessingFilter를
사용하기로 했다면 로그인 페이지가 REQUIRES_SECURE_CHANNEL로
지정되어 있는지와 AuthenticationProcessingFilterEntryPoint.forceHttps 속성이
true로 지정되어 있는지 확인하도록 한다.
일단 한번 설정해 두기만 하면 채널 보안 필터를 사용하는 것은 매우 쉽다.
단순히 프로토콜(HTTP나 HTTPS)나 포트(80, 8080, 443, 8443 등)에 관계없이
페이지를 요청하기만 하면 된다. 확실히 여러분은 최초 요청을 어떻게 하는지
알아야 할 필요가 있을 것이며(아마 web.xml
<welcome-file>이나
잘 알려진 기본 페이지 URL을 통해), 일단 필터에 최초 요청이 이루어 지고 나면
애플리케이션 컨텍스트에 정의되어 있는 대로 재지정을 수행할 것이다.
게다가 여러분은 독자적인 ChannelProcessor 구현체를
ChannelDecisionManagerImpl에 추가할 수도 있다.
예를 들어 여러분은 HttpSession 속성을 지정하여
"다음 그림의 내용을 입력하시오"와 같은 과정을 통해
사용자가 사람인지를 알아낼 수도 있다. 여러분이 작성한
ChannelProcessor는 REQUIRES_HUMAN_USER 설정 속성에
반응하여 HttpSession 속성이 현재 설정되어 있지 않을 경우
사용자가 사람인지를 확인하는 유효성 검증 절차를 시작하기 위해 적절한
시작지점으로 재지정할 수 있다.
ChannelProcessor 나 AccessDecisionVoter 중
어느 곳에 보안 체크를 둘지 결정하기 위해서는 전자는 인증되지 않은 요청을 처리하기 위한 것이고,
후자는 인증된 요청을 처리하도록 설계된 것임을 떠올리도록 한다. 따라서 후자는
인증된 인증주체의 허가된 권한에 접근한다. 추가적으로
AccessDecisionVoter에 의해 탐지된 문제는 궁극적으로
AccessDeniedException을 발생시키는데
반해(관장하는 AccessDecisionManager에 따라),
ChannelProcessor에 의해 탐지된 문제는 일반적으로
HTTP/HTTPS 재지정을 하도록 함으로써 주어진 요구사항을 만족시킨다.
Acegi Security에는 JSP 코드를 쉽게 작성할 수 있도록 해주는 몇몇
JSP 태그 라이브러리가 포함되어 있다. 이러한 태그 라이브러리들은
authz로 알려져 있으며 다양한 각종 서비스들을 제공해 준다.
태그 라이브러리 클래스들은 모두 중심이 되는 acegi-security-xx.jar
파일에 포함되어 있으며 JAR 파일의 META-INF 디렉터리에 들어있는
authz.tld에 들어있다. 이는 JSP 1.2+ 웹 컨테이너에서는
단순히 JAR를 WAR의WEB-INF/lib 디렉터리에 집어넣기만 해도
사용할 수 있게 된다는 것을 의미한다. 만약 여러분이 JSP 1.1 컨테이너를 사용하고 있는
경우에는 authz.tld 파일을 WEB-INF/lib
디렉터리에 집어넣은 다음 여러분의 web.xml 파일에 JSP 태그 라이브러리를
선언할 필요가 있다. 다음의 설정을 web.xml에 추가하도록 한다:
<taglib> <taglib-uri>http://acegisecurity.org/authz</taglib-uri> <taglib-location>/WEB-INF/authz.tld</taglib-location> </taglib>
본 파트에서는 개별 인증 메커니즘들과, 그러한 인증 메커니즘들에 대응되는
AuthenticationProvider에 관해 알아볼 것이다.
또한 각 메커니즘들의 설정을 좀 더 일반화할 수 있는 방법에 관해서도 알아볼 것이다.
여러분이 Acegi Security에서 제공되는 인증 접근방법을 이용하고 있다면,
일반적으로 AuthenticationProvider와
AuthenticationEntryPoint와 더불어 웹 필터를 설정해야 할
필요가 있을 것이다. 이 섹션에서는 폼 기반 인증(예를 들어 멋진 HTML 페이지를
보여주어 사용자가 로그인할 수 있도록 하는)에 추가하여
BASIC 인증(예를 들어 웹 서비스나 비슷한 무언가가 보호되어 있는 자원에
접근할 수 있도록 해주는)을 지원해야할 필요가 있는 예제 애플리케이션을 살펴볼 것이다.
web.xml에는 애플리케이션이
FilterChainProxy를 사용하기 위한 하나의
Acegi Security 필터를 필요로 할 것이다. 거의 모든 Acegi Security 애플리케이션은
그러한 입력사항들을 가질 것이며, 아마 다음과 같을 것이다:
<filter>
<filter-name>Acegi Filter Chain Proxy</filter-name>
<filter-class>org.acegisecurity.util.FilterToBeanProxy</filter-class>
<init-param>
<param-name>targetClass</param-name>
<param-value>org.acegisecurity.util.FilterChainProxy</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>Acegi Filter Chain Proxy</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>위 선언은 모든 웹 요청이 Acegi Security의 FilterChainProxy로
전달되도록 할 것이다. 필터 섹션에서 설명한 대로
FilterChainProxy는 웹 요청이 서로 다른 URL 패턴에 근거하여
서로 다른 필터로 전달될 수 있도록 해주는 일반목적용으로 사용할 수 있는 클래스이다.
그러한 위임된 필터들은 애플리케이션 컨텍스트 내에서 관리되며, 따라서 의존성 주입의
이점을 누릴 수 있다. FilterChainProxy 빈 정의가 애플리케이션
컨텍스트 내에서 어떠한 모습을 띠고 있는지 살펴보도록 하자:
<bean id="filterChainProxy" class="org.acegisecurity.util.FilterChainProxy">
<property name="filterInvocationDefinitionSource">
<value>
CONVERT_URL_TO_LOWERCASE_BEFORE_COMPARISON
PATTERN_TYPE_APACHE_ANT
/**=httpSessionContextIntegrationFilter,logoutFilter,authenticationProcessingFilter,basicProcessingFilter,securityContextHolderAwareRequestFilter,rememberMeProcessingFilter,anonymousProcessingFilter,exceptionTranslationFilter,filterInvocationInterceptor,switchUserProcessingFilter
</value>
</property>
</bean>내부적으로 Acegi Security는 PropertyEditor를 이용하여 위 XML 코드에
나타나 있는 문자열들을 FilterInvocationDefinitionSource 객체로 변환할 것이다.
이 단계에서 중요한 점은 필터들이 선언되어 있는 순서대로 연속적으로 실행될 것이라는
것이며, 각각의 필터들은 실질적으로는 애플리케이션 컨텍스트 내의 다른 빈의
<bean id>이다. 따라서 위 경우 몇 가지 추가적인 빈들 또한
애플리케이션 컨텍스트에 나타날 것이며, 그리고 그것들은
httpSessionContextIntegrationFilter이나
logoutFilter 등과 같은 이름으로 불려질 것이다.
필터들이 나타나야 하는 순서는 필터 섹션에서 논의할 것이며, 위 예제에 있는 것들은
올바른 순서를 따르고 있다.
예제에서는 AuthenticationProcessingFilter와
BasicProcessingFilter가 사용될 것이다.
이러한 필터들은 각각 폼 기반의 인증과 BASIC HTTP 헤더 기반의
인증에 대응되는 "인증 메커니즘"이다(인증 메커니즘의
역할에 대해서는 참조 가이드의 앞 부분에서 알아보았다). 여러분이 폼 기반의 인증이나
BASIC 인증을 사용하고 있지 않았다면 이러한 빈들 또한 정의되어 있지 않을 것이다.
대신 DigestProcessingFilter나
CasProcessingFilter와 같은
여러분이 원하는 인증 환경에 필터 애플리케이션을 정의해야 할 것이다.
각각의 그러한 인증 메커니즘들을 설정하는 방법에 관해서는 참조 가이드의
각 메커니즘들에 관한 개별 챕터들을 참고하도록 한다.
HttpSessionContextIntegrationFilter가 HTTP 세션내의 각 호출 사이에서
SecurityContext의 내용을 유지한다는 점을 떠올려 보자. 이는 인증 주체가
인증을 시도하는 초기에 오직 한 번만 인증 메커니즘들이 사용된다는 것을 의미한다.
그 이후로는 인증 메커니즘이 정보를 필터에 유지하며 조용히 요청을
필터 체인상의 다음 필터로 전달하기만 할 뿐이다. 이렇게 하는 것이 각각의 모든 호출에 대한 신원정보를
제시하는 인증 접근방법이 거의 없다는(그 중에서 BASIC 인증은 예외지만)
사실에 비추어 보면 실용적인 요구사항이긴 하지만, 만약 인증주체의 계정이 말소 혹은
사용 중지 아니면 초기 인증 과정이 지난 후에 변경되었다면
(예를 들면 GrantedAuthority[]가 늘어나거나 줄어드는 경우) 어떻게 될 것인가?
지금부터 그러한 상황을 어떻게 처리하는지에 관해 살펴보기로 하자.
안전 객체를 사용하기 위한 주요 인증 제공자로서
AbstractSecurityInterceptor를 앞에서 소개하였다.
이 클래스는 AuthenticationManager에 접근할 것을 필요로 한다.
뿐만 아니라 이 클래스는 Authentication 객체가 안전 객체 호출에
대해 재인증되어야 할 것인지를 나타내는 구성 설정을 갖고 있기도 하다.
기본적으로 AbstractSecurityInterceptor는
Authentication.isAuthenticated() 메소드가 true를 리턴할 경우
SecurityContextHolder 안에 들어있는 인증된 모든
Authentication을 그대로 받아들인다. 이렇게 할 경우
성능상으로는 이점이 있지만 만약 여러분이 즉각적인 인증 유효성을 보장하고자 할 경우에는
이렇게 하는 것이 이상적인 방법이 아니다. 그러한 경우 여러분은 아마도
AbstractSecurityInterceptor.alwaysReauthenticate 프로퍼티를
true로 설정할 필요가 있을 것이다.
여러분은 자기 자신에게 "AuthenticationManager는 무엇인가?"라고
자문할지도 모르겠다. 아직 AuthenticationManager에
관해서는 알아보지는 않았으며, 앞에서는 AuthenticationProvider의
개념에 대해서만 알아보았다. 매우 간단하게 말하자면
AuthenticationManager는 요청을
AuthenticationProvider 체인에 전달해야 할 책임이 있다는 것이다.
이것은 우리가 앞서 알아보았던 필터 체인과 약간 유사하지만 몇 가지 차이점이 있다.
Acegi Security에는 오직 하나의 AuthenticationManager
구현체만이 포함되어 있으므로 이 장에서 사용하고 있는 예제에 맞게
AuthenticationManager를 설정하는 방법을 알아보기로 하자:
<bean id="authenticationManager" class="org.acegisecurity.providers.ProviderManager">
<property name="providers">
<list>
<ref local="daoAuthenticationProvider"/>
<ref local="anonymousAuthenticationProvider"/>
<ref local="rememberMeAuthenticationProvider"/>
</list>
</property>
</bean>아마도 이 시점에서 여러분의 인증 메커니즘(일반적으로 필터인)도
AuthenticationManager에 객체 참조로 주입되고 있다는 것을 언급할 필요가 있다.
그러므로 인증 메커니즘 뿐만 아니라 AbstractSecurityInterceptor도
모두 위의 ProviderManager를 사용하여
AuthenticationProvider의 목록을 폴링할 것이다.
위 예제에는 세 개의 제공자가 포함되어 있다. 그것들은 나타나 있는 순서대로
인증을 시도하며(Set이 아닌 List를
사용한다는 의미이다), 각 제공자는 인증을 시도하거나 아니면 단순히
null을 반환하여 인증을 생략할 수 있다.
만약 구현체들이 모두 null을 반환하면
ProviderManager는 적절한 예외를 던진다.
여러분이 제공자 연결(chaining)에 관해 관심이 있다면
ProviderManager에 관한 JavaDoc을 참조하길 바란다.
사용할 제공자는 때때로 인증 메커니즘으로 상호 교체할 수 있지만,
언젠가 그것들은 특정 인증 메커니즘에 의존하게 될 것이다. 예를 들어
DaoAuthenticationProvider는 단순히 문자열에 기반하는 사용자명과
비밀번호만을 필요로 한다. 다양한 인증 메커니즘들은 BASIC 및 폼 인증을
포함한(BASIC 및 폼 인증에 제한되지 않은) 문자열 기반의 사용자 이름과
비밀번호의 집합이 될 것이다. 동일하게 몇몇 인증 메커니즘들은 오직 한 가지
유형의 AuthenticationProvider에 의해서만 가로채어질(intercept) 수 있는 인증 요청
객체를 생성한다. 이러한 일 대 일 맵핑의 예는 JA-SIG CAS이며, JA-SIG CAS는
서비스 티켓의 개념을 이용하기 때문에 CasAuthenticationProvider에 의해서만
인증될 수가 있다. 그 이상의 일 대 일 맵핑 예는 LDAP 인증 메커니즘이며,
LDAP 인증 메커니즘은 LdapAuthenticationProvider에 의해서만 처리될 수 있다.
그러한 관계들의 세부적인 내용은 각 클래스의 JavaDoc에 자세하게 기술되어 있으며,
인증 접근법에 특화되어 있는 내용들도 포함되어 있다. 여러분은 이러한
구현체의 세부사항에 관해서는 크게 신경쓸 필요가 없는데, 왜냐하면 여러분이 적절한
제공자를 등록하는 것을 잊어버렸더라도 여러분이 인증을 시도할 때 단순히
ProviderNotFoundException을 받게 될 것이기 때문이다.
적절한 인증 메커니즘을 FilterChainProxy에 설정하고, 대응되는
AuthenticationProvider가 ProviderManager에 등록되었는지를 확인하고 난
후 여러분이 해야할 마지막 단계는 AuthenticationEntryPoint를 설정하는 것이다.
앞서 논의했던 ExceptionTranslationFilter의 역할을 떠올려 보면
ExceptionTranslationFilter는 인증을 시작하기 위해 HTTP 헤더나 HTTP 재지정을
받아야 하는 HTTP 기반 요청을 사용한다.
앞의 예제에 계속 이어서 알아보도록 하자:
<bean id="exceptionTranslationFilter" class="org.acegisecurity.ui.ExceptionTranslationFilter">
<property name="authenticationEntryPoint"><ref local="authenticationProcessingFilterEntryPoint"/></property>
<property name="accessDeniedHandler">
<bean class="org.acegisecurity.ui.AccessDeniedHandlerImpl">
<property name="errorPage" value="/accessDenied.jsp"/>
</bean>
</property>
</bean>
<bean id="authenticationProcessingFilterEntryPoint" class="org.acegisecurity.ui.webapp.AuthenticationProcessingFilterEntryPoint">
<property name="loginFormUrl"><value>/acegilogin.jsp</value></property>
<property name="forceHttps"><value>false</value></property>
</bean>주의할 점은 ExceptionTranslationFilter가 두 개의 협력자를 필요로 한다는
것이다. 첫 번째 협력자인 AccessDeniedHandlerImpl은
RequestDispatcher의 포워드(forward)를 이용하여 지정된 접근 거부 오류 페이지를
보여준다. 우리는 포워드를 이용하여 SecurityContextHolder가 여전히
인증주체의 세부사항을 포함하고 있도록 하는데, 이렇게 하면
사용자에게 무언가를 보여주는데 도움이 된다(이전 버전의
Acegi Security에서는 서블릿 컨테이너가 403 오류 메시지를
처리하도록 하였는데, 이렇게 할 경우 문맥적인 정보가 부족했다).
또한 AccessDeniedHandlerImpl은 HTTP 헤더를 403으로 지정하는데,
이는 접근 거부를 의미하는 공식 오류 코드이다. AuthentionEntryPoint의
경우, 여기에서는 인증되지 않은 인증주체가 보호되어 있는 오퍼레이션을 수행하려 할 때
어떠한 동작을 취할 것인가를 지정하고 있다. 예제에서는 폼 기반의 인증을
사용할 것이므로 AuthenticationProcessinFilterEntryPoint와 로그인
페이지의 URL을 지정하였다. 일반적으로 여러분의 애플리케이션은 오직 하나의
진입점만을 가질 것이므로 대부분의 인증 접근법은 독자적인
AuthenticationEntryPoint를 정의한다. 각 인증 접근법에 대해 어떤 진입점을
사용할 것인가에 관한 세부 내용은 인증 접근법 관련 챕터에서 알아볼 것이다.
첫 번째 파트에서 언급했던 것과 같이 대부분의 인증 제공자는
UserDetails와 UserDetailsService 인터페이스를 이용한다.
UserDetailsService 인터페이스에 대한 계약(contract)은
하나의 메소드로 구성되어 있다:
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException, DataAccessException;
반환된 UserDetails 인터페이스는 접근자들을 제공하고 있는데,
이러한 접근자들은 사용자명(username), 비밀번호(password), 허가권한(granted authorities) 및
사용자의 활성/비활성 여부와 같은 기초적인 인증 정보들이 null을 갖지 않도록
보장한다. 대부분의 인증 제공자는 심지어 사용자명과 비밀번호가 실질적으로는
인증 의사결정에서 부분적으로 사용되지 않을지라도 UserDetailsService를
사용할 것이다. 일반적으로 그러한 제공자들은 반환된 UserDetails 객체를
단순히 자체적인 GrantedAuthority[]정보로만 사용할 것인데, 왜냐하면 몇몇
다른 시스템(LDAP이나 X509, 혹은 CAS 등과 같은)에 실제적인 신원정보(credential)의
유효성을 검사할 책임이 있기 때문이다.
Acegi Security에서는 User 클래스라는 단 하나의
UserDetails의 구상 구현체를 제공하고 있다. Acegi Security 사용자들은 UserDetailsService를 작성할 때
어떠한 구상 UserDetails 클래스를 반환할지 결정해야 할 것이다.
비록 특수한 상황(객체 관계 맵퍼(object relational mapper)와 같은)에서
사용자가 자체적인 UserDetails 구현체를 작성해야 할지라도
대부분의 경우에는 User를 직접 사용하거나 서브클래싱하여 사용하게 될 것이다.
이렇게 하는 것이 흔치 않은 상황은 아니며, 사용자들은 단순히 시스템상의 사용자를
나타내는 보통의 도메인 객체를 반환하는 것을 주저해서는 안된다.
이는 특히 주어진 UserDetails를 종종 인증주체와 관련된
부가 속성(예를 들면 인증주체의 전화번호나 전자메일 주소와 같은)를 저장하기 위한
용도로 사용할 경우에 흔히 나타나는데, 그렇게 함으로써 그와 같은 정보들을 웹 상에서
손쉽게 나타낼 수 있다.
주어진 UserDetailsService를 구현하는 것은 상당히 간단하며,
UserDetailsService는 사용자가 선택한
영속화 전략(persistence strategy)을 이용하여 인증 정보를 검색하는데 용이해야 한다.
앞서 언급했듯이 Acegi Security는 두 가지 유용한 기본 구현체를 포함하고 있으며
아래에서 알아볼 것이다.
선택한 영속화 엔진으로부터 정보를 추출하는 사용자 정의
UserDetailsService 구현체를 작성하는 것이
쉽기는 하지만 대부분의 애플리케이션들은 그렇게까지 복잡할 필요는 없다.
이는 여러분이 빠르게 프로토타입을 만들어야 하는 상황이거나,
단순히 Acegi Security와의 통합작업을 시작하여 데이터베이스를 설정하거나
UserDetailsService의 구현체를 작성하는데
정말로 시간을 소모하고 싶지 않을 때 특히 그러하다.
이러한 상황에서 가장 가볍게 선택할 수 있는 것이 바로
InMemoryDaoImpl 구현체를 설정하는 것이다:
<bean id="inMemoryDaoImpl" class="org.acegisecurity.userdetails.memory.InMemoryDaoImpl">
<property name="userMap">
<value>
marissa=koala,ROLE_TELLER,ROLE_SUPERVISOR
dianne=emu,ROLE_TELLER
scott=wombat,ROLE_TELLER
peter=opal,disabled,ROLE_TELLER
</value>
</property>
</bean> 위 예제에서 userMap 프로퍼티는 각각의 사용자명과 비밀번호, 허가된
권한 목록과 선택적으로 활성/비활성 여부를 나타내는 키워드를 포함하고 있다.
각 토큰들을 분리하는 데에는 콤마(,)를 사용한다. 사용자명은 등호의 왼쪽에
나타나야 하며 비밀번호는 등호의 오른쪽에 가장 먼저 나타나야 한다.
enabled 및 disabled 키워드(대소문자 구분)는
두 번째, 혹은 이어지는 토큰 어디에도 나타날 수 있다. 나머지 토큰들은
허가된 권한들로 간주되며 GrantedAuthorityImpl 객체로서
생성된다(이는 여러분의 경우에만 그렇게 되는 것이고, 대부분의 애플리케이션들은
GrantedAuthority 구현체를 필요로 하지 않으므로
이러한 방식으로 기본 구현체를 사용하는 것도 무방하다). 만약 사용자에게
비밀번호와(혹은) 허가된 권한이 없다면 사용자는 인메모리 인증
저장소(in-memory authentication repository)에 생성되지 않는다는 것을
염두에 둔다.
또한 InMemoryDaoImpl은
setUserProperties(Properties) 메소드를 제공하는데,
이 메소드는 여러분이 java.util.Properties를 다른
Spring 설정이 적용되어 있는 빈이나 외부 properties 파일로
외부화(externalize)할 수 있도록 해준다. 여러분은 Spring의
PropertiesFactoryBean을 사용하기를 선호할 수도 있는데,
PropertiesFactoryBean은 그러한 외부 프로퍼티 파일을
불러오는데 유용하다. 이 설정자 메소드는 대규모의 사용자층을 갖고 있는 단순한
애플리케이션이나 배치시 설정 변경(deployment-time configuration change)에서
유용할 수도 있으나, 인증 세부사항을 처리하는데 있어 완전한 데이터베이스를
사용하길 바라지는 않았으면 한다.
Acegi Security에는 인증 정보를 JDBC 데이터 소스로부터 획득할 수 있는
UserDetailsService도 포함되어 있다. 내부적으로는
Spring JDBC가 사용되므로 단순히 사용자의 세부사항을 저장하는 데에는
완전한 기능집합을 갖춘 객체 관계 매퍼(ORM)의 복잡성을 피할 수 있다.
여러분의 애플리케이션에 ORM 툴을 사용하고 있다면, 여러분은 사용자 정의
UserDetailsService를 작성하여 이미 이전에 작성해 놓은
맵핑 파일을 재사용하고자 할 수도 있다.
JdbcDaoImpl로 돌아가 보면 아래에 설정 예가 나타나 있다:
<bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource"> <property name="driverClassName"><value>org.hsqldb.jdbcDriver</value></property> <property name="url"><value>jdbc:hsqldb:hsql://localhost:9001</value></property> <property name="username"><value>sa</value></property> <property name="password"><value></value></property> </bean> <bean id="jdbcDaoImpl" class="org.acegisecurity.userdetails.jdbc.JdbcDaoImpl"> <property name="dataSource"><ref bean="dataSource"/></property> </bean>
여러분은 위에서 보여지는 DriverManagerDataSource을 변경하여
여러 가지 관계형 데이터베이스 관리 시스템을 사용할 수 있다.
또한 여러분은 일반적으로 Spring에서 선택할 수 있는 것처럼
JNDI로부터 획득한 전역 데이터 소스(global data source)를 이용할
수도 있다. 사용된 데이터베이스와 DataSource를 획득하는 방법과는 상관없이
여러분은 dbinit.txt에서 나타나 있는 것처럼 표준 스키마를 사용해야만 한다.
여러분은 이 파일을 Acegi Security 웹 사이트에서 다운로드할 수 있다.
만약 기본 스키마가 여러분의 요구사항에 적합하지 않다면 JdbcDaoImpl은
SQL 문장을 조정할 수 있도록 해주는 두 개의 프로퍼티를 제공해 준다.
또한 좀더 커스터마이징이 필요할 경우 여러분은 JdbcDaoImpl을 서브클래싱할
수도 있다. JdbcDaoImpl 클래스가 복잡한 사용자 정의 하위클래스를
의도하지는 않는 것을 염두에 두고, 좀 더 세부적인 내용을 보려면
JavaDoc을 참조하길 바란다. 여러분의 요구사항이 복잡하다면(특수한 스키마나
특정 UserDetails 구현체가 반환되기를 바라는 경우와 같은),
여러분이 직접 독자적인 UserDetailsService를
작성하는 것이 더 낫다. Acegi Security에서 제공되는 기본 구현체는 일반적인
상황에서 사용되는 것을 의도하고 있으며 무한한 설정상의 유연함을 제공하지는 않는다.
Acegi Security는 인증주체가 동일한 애플리케이션에서 동시에 인증되어 지정된 횟수 이상으로 인증되는 것을 방지할 수 있다. 여러 ISV들은 이러한 라이센싱을 강제하는 기능을 이용하고 있는데, 네트워크 관리자는 이 기능이 사람들이 로그인명을 공유하는 것을 방지하는데 도움되기 때문에 선호한다. 예를 들면 여러분은 "Batman"이라는 사용자가 두 개의 서로 다른 세션으로부터 하나의 웹 애플리케이션에 로그인하는 것을 방지할 수 있다.
동시 세션 지원기능을 사용하려면 다음의 설정내용을
web.xml에 추가하도록 한다:
<listener> <listener-class>org.acegisecurity.ui.session.HttpSessionEventPublisher</listener-class> </listener>
더불어 여러분은 FilterChainProxy에
org.acegisecurity.concurrent.ConcurrentSessionFilter를 추가할 필요가 있을 것이다.
ConcurrentSessionFilter는
두 개의 프로퍼티를 필요로 하는데, 일반적으로 SessionRegistryImpl의
인스턴스를 가리키는 sessionRegistry와 세션이 만료되었을 때 보여줄
페이지를 가리키는 expiredUrl이다.
web.xml안에 들어있는 HttpSessionEventPublisher는
ApplicationEvent가 매번 HttpSession이
시작되거나 중지될 때마다 Spring ApplicationContext로
공표(publish)되도록 한다. 이렇게 하는 것이 매우 중요한 이유는 세션이
종료될 경우 SessionRegistryImpl이 통지받도록 하기 때문이다.
또한 여러분은 ConcurrentSessionControllerImpl과 묶어
여러분의 ProviderManager 빈에서 그것을 참조하게 할 필요가 있다:
<bean id="authenticationManager" class="org.acegisecurity.providers.ProviderManager">
<property name="providers">
<!-- your providers go here -->
</property>
<property name="sessionController"><ref bean="concurrentSessionController"/></property>
</bean>
<bean id="concurrentSessionController" class="org.acegisecurity.concurrent.ConcurrentSessionControllerImpl">
<property name="maximumSessions"><value>1</value></property>
<property name="sessionRegistry"><ref local="sessionRegistry"/></property>
</bean>
<bean id="sessionRegistry" class="org.acegisecurity.concurrent.SessionRegistryImpl"/>AuthenticationTag는 단순히 현 인증 주체의
Authentication.getPrincipal() 객체의 프로퍼티를
웹 페이지로 출력하는데 사용한다.
다음 JSP 코드는 AuthenticationTag의 사용법을 보여준다:
<authz:authentication operation="username"/>
이 태그는 인증주체의 이름을 출력하게 할 것이다. 여기서 우리는
Authentication.getPrincipal()을
UserDetails 객체로 가정할 수 있으며,
일반적으로 DaoAuthenticationProvider를 사용하는 경우이다.
Acegi Security에는 DaoAuthenticationProvider라는 이름의
제품 수준의 품질을 갖춘 AuthenticationProvider 구현체가
포함되어 있다. 이 인증 제공자는 UsernamePasswordAuthenticationToken을
생성하는 모든 인증 메커니즘과 호환 가능하며, 아마도 프레임워크에서 가장 널리
사용되는 제공자일 것이다. 다른 대다수의 인증 제공자와 유사하게 DaoAuthenticationProvider는
사용자명과 비밀번호, GrantedAuthority[]들을 탐색하기 위해
UserDetailsService를 이용한다.
UserDetailsService를 이용하는 다른 대부분의 인증 제공자와는 다르게
DaoAuthenticationProvider는 실제로 비밀번호가 제공되기를 요구하며, 인증 요청
객체에 제공된 비밀번호의 유효성 등을 실제로 평가할 것이다.
DaoAuthenticationProvider를 여러분의 ProviderManager 목록에 추가하는 것
(참조 가이드의 시작부분에서 논의했던)과 적절한 인증 메커니즘이
UsernamePasswordAuthenticationToken을 제공하도록 설정하는 것을
별도의 문제로 생각하면 DaoAuthenticationProvider 자체를 설정하는
것은 상대적으로 간단하다:
<bean id="daoAuthenticationProvider" class="org.acegisecurity.providers.dao.DaoAuthenticationProvider"> <property name="userDetailsService"><ref bean="inMemoryDaoImpl"/></property> <property name="saltSource"><ref bean="saltSource"/></property> <property name="passwordEncoder"><ref bean="passwordEncoder"/></property> </bean>
PasswordEncoder와 SaltSource는 선택사항이다.
PasswordEncoder는 설정된 UserDetailsService로부터
반환된 UserDetails 객체에 들어있는 비밀번호의 인코딩과 디코딩을 제공한다.
SaltSource는 비밀번호에 "양념(salt)"을 뿌릴 수 있도록 하는데,
이러한 salt는 인증 저장소에 들어있는 비밀번호의 보안을 강화해준다.
Acegi Security에서 제공되는 PasswordEncoder 구현체로는 MD5, SHA,
클리어텍스트(cleartext) 인코딩들이 있다. Acegi Security에는 두 개의
SaltSource 구현체가 제공되는데, 모든 비밀번호에 대하여 동일한 salt로
인코딩을 수행하는 SystemWideSaltSource와, 반환된
UserDetails 객체의 프로퍼티를 검사하여 salt를 획득하는
ReflectionSaltSource가 있다. 선택적인 기능들에 관해 자세히 알아보려면
JavaDoc을 참조하길 바란다.
위 프로퍼티에 추가하여 DaoAuthenticationProvider는
UserDetails 객체에 대한 선택적인 캐싱을 지원한다.
UserCache 인터페이스는 DaoAuthenticationProvider가
UserDetails 객체를 캐시에 위치할 수 있도록 해주어 동일한 사용자명으로
이어지는 인증 시도에 대해 캐시로부터 검색할 수 있도록 한다. 기본적으로
DaoAuthenticationProvider는 NullUserCache를 사용하는데,
NullUserCache는 아무런 캐싱도 수행하지 않는다.
사용하기에 편리한 캐싱 구현체로는 EhCacheBasedUserCache가 있으며
다음과 같이 설정한다:
<bean id="daoAuthenticationProvider" class="org.acegisecurity.providers.dao.DaoAuthenticationProvider">
<property name="userDetailsService"><ref bean="userDetailsService"/></property>
<property name="userCache"><ref bean="userCache"/></property>
</bean>
<bean id="cacheManager" class="org.springframework.cache.ehcache.EhCacheManagerFactoryBean">
<property name="configLocation">
<value>classpath:/ehcache-failsafe.xml</value>
</property>
</bean>
<bean id="userCacheBackend" class="org.springframework.cache.ehcache.EhCacheFactoryBean">
<property name="cacheManager">
<ref local="cacheManager"/>
</property>
<property name="cacheName">
<value>userCache</value>
</property>
</bean>
<bean id="userCache" class="org.acegisecurity.providers.dao.cache.EhCacheBasedUserCache">
<property name="cache"><ref local="userCacheBackend"/></property>
</bean> Acegi Security EH-CACHE의 구현체(EhCacheBasedUserCache 포함)들은
모두 EH-CACHE Cache 객체를 필요로 한다. Cache 객체는
여러분이 원하는 어디에서도 획득할 수 있지만 위 설정에서 볼 수 있는 것과 같이
Spring의 팩토리 클래스를 사용할 것을 권장한다. 만약 Spring의 팩토리 클래스를
사용할 경우 캐시 저장소 위치, 메모리 사용, 추방 정책, 시간 제한 등의 최적화 방법에 관한 자세한 내용은
Spring 문서를 참조하길 바란다.
DaoAuthenticationProvider에서는 설계상 계정 잠금(account locking)을 지원하지 않기로
결정하였는데, 계정 잠금을 지원할 경우 UserDetailsService 인터페이스의 복잡성이
증가하였을 것이기 때문이다.즉, 예를 들자면 실패한 인증 시도의 횟수를 증가시키는
메소드도 필요하게 될 수 있다. 그러한 기능은 아래에서 설명할 애플리케이션의
공표(publishing) 기능을 이용하여 손쉽게 제공할 수 있다.
DaoAuthenticationProvider는 인증주체의 프로퍼티 집합을 포함하는
Authentication 객체를 반환한다. 인증주체는
String(본질적으로는 사용자명)이나
UserDetails 객체(UserDetailsService로부터
탐색되는)가 될 것이다. 기본적으로는 UserDetails가 반환되며,
따라서 애플리케이션에서는 잠재적으로 애플리케이션에서 사용할 사용자의 전체 이름,
전자메일 주소 등의 추가적인 프로퍼티를 추가할 수 있다. 컨테이너 어댑터(container adapter)를
사용하고 있거나, 여러분의 애플리케이션이 String(Acegi Security 0.6 이전 버전의 경우)으로
동작하도록 작성되어 있을 경우 애플리케이션 컨텍스트에서
DaoAuthenticationProvider.forcePrincipalAsString 프로퍼티를
true로 지정해야 한다.
Acegi Security는 인증 요청을 JAAS(Java 인증 및 권한부여 서비스; Java Authentication and Authorization Service)에 위임할 수 있는 패키지를 제공한다. 이 패키지는 아래에서 자세히 알아볼 것이다.
JAAS가 동작하는 중심에는 로그인 설정 파일이 있다. JAAS 로그인 설정 파일에 관해 자세히 알아보려면 Sun Microssystems에서 찾아볼 수 있는 JAAS 참조문서를 참고하도록 한다. 우리는 여러분이 이 섹션의 내용을 이해하기 위해 JAAS와 JAAS의 로그인 설정 파일 문법에 대해 기본적인 이해를 하고 있다고 가정할 것이다.
JaasAuthenticationProvider는 JAAS를 통해
사용자의 인증주체와 신원정보에 대한 인증을 시도한다.
다음 내용을 포함하고 있는 JAAS 로그인 설정파일이 /WEB-INF/login.conf에
들어있다고 가정해 보자:
JAASTest {
sample.SampleLoginModule required;
};모든 Acegi Security 빈들 처럼 JaasAuthenticationProvider도 애플리케이션
컨텍스트를 통해 설정된다. 다음의 빈 정의는 위 JAAS 로그인 설정 파일과 대응된다:
<bean id="jaasAuthenticationProvider" class="org.acegisecurity.providers.jaas.JaasAuthenticationProvider">
<property name="loginConfig">
<value>/WEB-INF/login.conf</value>
</property>
<property name="loginContextName">
<value>JAASTest</value>
</property>
<property name="callbackHandlers">
<list>
<bean class="org.acegisecurity.providers.jaas.JaasNameCallbackHandler"/>
<bean class="org.acegisecurity.providers.jaas.JaasPasswordCallbackHandler"/>
</list>
</property>
<property name="authorityGranters">
<list>
<bean class="org.acegisecurity.providers.jaas.TestAuthorityGranter"/>
</list>
</property>
</bean>
CallbackHandler와 AuthorityGranter는
아래에서 알아볼 것이다.
대부분의 JAAS LoginModule은 일종의 콜백을 필요로 하며,
일반적으로 이러한 콜백들을 사용하여 사용자명과 비밀번호를 획득한다.
Acegi Security를 배치하는 경우에는 Acegi Security가 이러한 사용자
상호작용(인증 메커니즘을 통한)을 책임진다. 따라서 인증 요청이 JAAS를 통해
위임되기 전까지는 Acegi Security의 인증 메커니즘에는 이미
JAAS LoginModule에서 필요로 하는 모든 정보들을 포함하고 있는
Authentication 객체가 있을 것이다.
따라서 Acegi Security의 JAAS 패키지는 두 개의 기본적인 콜백 핸들러를
제공하는데, 각각 JaasNameCallbackHandler와
JaasPasswordCallbackHandler이며, 이러한 각 콜백 핸들러들은
JaasAuthenticationCallbackHandler를 구현한다.
대부분의 경우 이러한 콜백 핸들러들은 내부 메커니즘을 전혀 이해해야할 필요없이
사용될 수 있다.
콜백이 작동하는 방식을 완전하게 제어할 필요가 있을 경우, 내부적으로
JaasAutheticationProvider가 이러한
JaasAuthenticationCallbackHandler를
InternalCallbackHandler로 래핑한다.
InternalCallbackHandler는 실제로는
JAAS의 표준 CallbackHandler 인터페이스를 구현하는 클래스이다.
JAAS LoginModule이 사용될 때마다
InternalCallbackHandler는
InternalCallbackHandler가 설정되어 있는 애플리케이션
컨텍스트의 목록을 전달한다. 만약 LoginModule이
InternalCallbackHandler에 대해 콜백을 요청하면 이번에는 콜백이
JaasAuthenticationCallbackHandler이
래핑된 곳으로 전달된다.
JAAS는 심지어 "역할(role)"이 JAAS에서 인증주체로서 나타내어 지더라도 인증주체와 함께 동작한다.
반면 Acegi Security는 Authentication 객체와
함께 동작한다. 각각의 Authentication 객체는 하나의 인증주체와 여러 개의
GrantedAuthority를 가진다. 이러한 서로 다른 개념들간의 맵핑을 돕기 위해
Acegi Security의 JAAS 패키지에는 AuthorityGranter 인터페이스가
포함되어 있다.
AuthorityGranter는 JAAS 인증주체를 검사하고 String을
반환할 책임이 있다. 그리고 나서
JaasAuthenticationProvider는 AuthorityGranter가
반환하는 String과 AuthorityGranter가
전달된 JAAS 인증주체를 모두 포함하는
JaasGrantedAuthority(Acegi Security의 GrantedAuthority 인터페이스를 구현하고
있는)를 생성한다. JaasAuthenticationProvider는
JAAS LoginModule을 이용하여 사용자의 신원정보를 획득하는데, 즉 최초에 성공적으로 인증한 다음
JAAS LoginModule이 반환하는 LoginContext에 접근하여
JAAS 인증 주체를 획득한다. LoginContext.getSubject().getPrincipals()을 호출하여
반환된 인증주체들은 JaasAuthenticationProvider.setAuthorityGranters(List)에
프로퍼티로 정의되어 있는 각 AuthorityGranter로 전달된다.
Acegi Security에는 구현에 특화되어 있는 수단을 포함한
JAAS 인증주체가 모두 주어진 어떠한 AuthorityGranter도 존재하지 않는다.
하지만 간단한 AuthorityGranter 구현을 시험하는 단위 테스트에는
TestAuthorityGranter가 포함되어 있다.
Siteminder는 Computer Assosicates의 상용 싱글 사인 온(single sign on) 솔루션이다.
Acegi Security는 SiteminderAuthenticationProcessingFilter라는 이름의
필터와 Siteminder에 의해 이미 인증된 적이 있는 요청을 처리하는데 사용할 수 있는
SiteminderAuthenticationProvider라는 이름의 제공자를 제공한다.
이 필터는 여러분이 인증에는 Siteminder, 권한 부여에는 Acegi Security를
사용하고 있다고 가정한다. 권한 부여에 Siteminder를 사용하는 것은 아직까지는
Acegi Security에서 직접적으로 지원하고 있지 않다.
Siteminder를 사용하면 특정 에이전트가 여러분의 웹 서버에 설정되어
인증주체가 애플리케이션으로 보내는 최초 요청을 가로챈다. 에이전트는 웹 요청을
싱글 사인 온 로그인 페이지로 재지정하며, 일단 인증이 이루어진 다음에야 여러분의
애플리케이션이 요청을 받게 된다. HTTP 요청의 내부에는 SM_USER와
같은 헤더가 있는데, 이러한 헤더는 인증된 인증주체를 확인하는데
사용된다(특정한 설정에 들어있는 헤더의 자세한 내용을 알아보려면
여러분이 속해 있는 조직의 "싱글 사인 온(single sign-on)" 관련 부서에 문의하길 바란다).
Acegi Security에서 Siteminder를 지원하기 위해 설정해야 하는 첫 번째 단계는
앞서 논의했던 HTTP 헤더를 검사할 인증 메커니즘을 정의하는 것이다.
인증 메커니즘은 차후에 SiteminderAuthenticationProvider로 전송될
UsernamePasswordAuthenticationToken을 생성하는 책임을 가질 것이다.
예제를 살펴보기로 하자:
<bean id="authenticationProcessingFilter" class="org.acegisecurity.ui.webapp.SiteminderAuthenticationProcessingFilter"> <property name="authenticationManager"><ref bean="authenticationManager"/></property> <property name="authenticationFailureUrl"><value>/login.jsp?login_error=1</value></property> <property name="defaultTargetUrl"><value>/security.do?method=getMainMenu</value></property> <property name="filterProcessesUrl"><value>/j_acegi_security_check</value></property> <property name="siteminderUsernameHeaderKey"><value>SM_USER</value></property> <property name="formUsernameParameterKey"><value>j_username</value></property> </bean>
위 예제에서는 빈에 AuthenticationManager가 제공되며,
이렇게 하는 것은 일반적으로 인증 메커니즘에서 AuthenticationManager를
필요로 하기 때문이다. 예제에는 몇 개의 URL도 지정되어 있는데, URL의 값은
스스로 설명이 가능하다. Acegi Security가 검사해야 하는 HTTP 헤더가 지정되어 있다는
것이 중요하다. 여러분이 추가적으로 폼 기반의 인증을 지원하고자
한다면(예를 들어 Siteminder가 설치되어 있지 않은 개발환경일 경우),
폼의 사용자명 파라미터도 지정하도록 하며, 제품으로 출시할 때에는
그렇지 하지 않도록 한다!
주의할 점은 여러분이 Siteminder 인증 메커니즘을 사용하기 위해서는
ProviderManager에 SiteminderAuthenticationProvider를
설정할 필요가 있다는 것이다. 보통 AuthenticationProvider는
비밀번호 프로퍼티가 UserDetailsSource에서 받는 것과 일치할 것으로 예상하지만,
이 경우에는 인증이 이미 Siteminder에 의해 처리되므로 비밀번호 프로퍼티가 그에 상응하지 않을 수도
있다. 이렇게 되는 것이 보안상 약점으로 들릴 수도 있긴 하나 여러분의 애플리케이션이
요청을 받기 훨씬 전에 사용자가 Siteminder를 이용하여 인증해야 한다는 것을 떠올려야 하며,
따라서 여러분이 직접 정의한 UserDetailsService의 목적은 단순히
Authentication 객체(예를 적절한 GrantedAuthority[]를 가진)를
완성하는 데 있다.
고급 팁 및 경구(word to the wise): 여러분의 개발환경(일반적으로 Siteminder가 설치되어 있지 않은)에 추가적으로 폼 기반의 인증도 지원하고자 한다면 폼의 username 파라미터도 지정한다. 출시하는 애플리케이션에는 그렇게 하지 않도록 한다!
AbstractSecurityInterceptor는 안전 객체의 콜백 단계 동안
SecurityContext와 SecurityContextHolder내의
Authentication 객체를 임시로 대체할 수 있다.
이는 오직 원래의 Authentication 객체가
AuthenticationManager와 AccessDecisionManager에 의해
성공적으로 처리될 경우에만 일어난다. RunAsManager는 대체할
Authentication 객체를 가리킬 것이며,
만약 Authentication 객체가 존재한다면, 그 객체는
SecurityInterceptorCallback상에서 사용되어야 한다.
안전 객체의 콜백 단계 동안 일시적으로 Authentication 객체를
대체함으로써 안전한 호출을 수행할 때 다른 인증 및 권한부여 신원정보를 필요로 하는
다른 객체를 요청하는 것이 가능해질 것이다. 뿐만 아니라 특정
GrantedAuthority 객체에 대한 내부 보안 검사를 수행하는 것도
가능해질 것이다. Acegi Security에는 SecurityContextHolder의
내용에 근거하여 원격 프로토콜을 자동적으로 설정하는 수 많은 헬퍼 클래스들이 제공되므로
이러한 run-as 대체는 원격 웹 서비스를 호출할 경우에 특히 유용하다.
Acegi Security에서 제공되는 RunAsManager 인터페이스는
다음과 같다:
public Authentication buildRunAs(Authentication authentication, Object object, ConfigAttributeDefinition config); public boolean supports(ConfigAttribute attribute); public boolean supports(Class clazz);
첫 번째 메소드는 메소드 호출이 이루어지는 동안 기존 Authentication 객체를
대체할 Authentication 객체를 반환한다.
메소드가 null을 반환하면 아무런 대체도 이루어지지 않아야 함을
의미한다. 두 번째 메소드는 설정 속성에 대한 초기 유효성 검증의 일부로서
AbstractSecurityInterceptor에서 사용된다.
supports(Class) 메소드는 보안 인터셉터 구현체에 의해 호출되어
설정된 RunAsManager가 보안 인터셉터가 제공할 안전 객체의 타입을
지원하는지를 확인해준다.
Acegi Security에는 하나의 RunAsManager에 대한 구상 구현체를 제공하고 있다.
RunAsManagerImpl 클래스는 ConfigAttribute가
RUN_AS_로 시작할 경우 교환 RunAsUserToken을
반환한다. 만약 그러한 ConfigAttribute가 발견되면 교환
RunAsUserToken은 새로운 각각의
RUN_AS_ ConfigAttribute에 대한 새로운
GrantedAuthorityImpl 뿐만 아니라 동일한 인증주체와 신원정보,
허가된 권한들을 원래의 Authentication 객체로서 포함할 것이다.
각각의 새로운 GrantedAuthorityImpl은 ROLE_이라는 접두사를 가지며
RUN_AS_ ConfigAttribute가 따라 붙는다.
예를 들면 RUN_AS_SERVER는
ROLE_RUN_AS_SERVER라는 허가 권한을 포함하는 대체
RunAsUserToken을 만들어낼 것이다.
대체 RunAsUserToken은 다른 Authentication 객체와
매우 유사하다. 교환 RunAsUserToken은 아마 적절한 AuthenticationProvider에
위임된 객체를 통해 AuthenticationManager에 의해 인증될 필요가 있다.
RunAsImplAuthenticationProvider는 그러한 인증을 수행한다.
RunAsImplAuthenticationProvider는 단순히 제시된
RunAsUserToken이 유효하다면 어떤 것이든 받아들인다.
악성코드가 RunAsUserToken를 생성하지 않으며
RunAsImplAuthenticationProvider에 보장된 상태로 받아들여지지 않도록
보장하기 위해서는 키의 해시가 생성된 모든 토큰에 저장되도록 한다.
RunAsManagerImpl과
RunAsImplAuthenticationProvider는 동일한 키를 가지고
빈 컨텍스트내에 생성된다:
<bean id="runAsManager" class="org.acegisecurity.runas.RunAsManagerImpl">
<property name="key"><value>my_run_as_password</value></property>
</bean>
<bean id="runAsAuthenticationProvider" class="org.acegisecurity.runas.RunAsImplAuthenticationProvider">
<property name="key"><value>my_run_as_password</value></property>
</bean>
동일한 키를 사용함으로써 각 RunAsUserToken들은 승인된
RunAsManagerImpl에 의해 생성되었는지에 대한 유효성 검사를
받을 수 있다. RunAsUserToken은 보안상의 이유로 생성된
후에는 변경되지 않는다.
HTTP 폼 인증은 AuthenticationProcessingFilter를 이용하여
로그인 폼을 처리하는 것을 수반한다. HTTP 폼 인증은 애플리케이션에서 가장 널리
사용되는 최종 사용자에 대한 인증 방법이다. 폼 기반의 인증은 전적으로 DAO 및 JAAS 인증 제공자와
함께 사용될 수 있다.
로그인 폼은 단순히 j_username과
j_password 입력 필드를 포함하며,
그리고 필터에 의해 모니터링되고 있는 URL로 게시(post)한다(기본값은
j_acegi_security_check이다). web.xml에는 다음과 같이
FilterToBeanProxy를 정의한다:
<filter>
<filter-name>Acegi Authentication Processing Filter</filter-name>
<filter-class>org.acegisecurity.util.FilterToBeanProxy</filter-class>
<init-param>
<param-name>targetClass</param-name>
<param-value>org.acegisecurity.ui.webapp.AuthenticationProcessingFilter</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>Acegi Authentication Processing Filter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>FilterToBeanProxy에 관해 자세히 알아보려면
Filters 섹션을 참고하길 바란다. 애플리케이션 컨텍스트에는
AuthenticationProcessingFilter를
정의할 필요가 있을 것이다:
<bean id="authenticationProcessingFilter" class="org.acegisecurity.ui.webapp.AuthenticationProcessingFilter"> <property name="authenticationManager"><ref bean="authenticationManager"/></property> <property name="authenticationFailureUrl"><value>/acegilogin.jsp?login_error=1</value></property> <property name="defaultTargetUrl"><value>/</value></property> <property name="filterProcessesUrl"><value>/j_acegi_security_check</value></property> </bean>
설정된 AuthenticationManager는 각각의 인증 요청을 처리한다.
인증이 실패하면 브라우저는 authenticationFailureUrl로 이동할 것이다.
AbstractProcessingFilter.ACEGI_SECURITY_LAST_EXCEPTION_KEY로 알 수 있는
AuthenticationException이 HttpSession 속성에
위치할 것이며, 사용자는 오류 페이지에서 오류가 발생한 원인에 관해 알 수 있을 것이다.
인증이 성공하면 결과 Authentication 객체는
SecurityContextHolder에 위치할 것이다.
일단 SecurityContextHolder가 갱신되면,
브라우저는 대상 URL로 이동할 필요가 있을 것이다. 대상 URL은 보통
AbstractProcessingFilter.ACEGI_SECURITY_TARGET_URL_KEY로
설정되어 있는 HttpSession 속성으로 알 수 있다. 이 속성은
AuthenticationException이 발생할 경우
ExceptionTranslationFilter에 의해 자동적으로 설정되며,
따라서 로그인이 성공한 후 사용자는 사용자가 접근을 시도했던 곳으로 돌아갈 수 있다.
몇 가지 이유로 HttpSession이 대상 URL을 가리키지 않을 경우
브라우저는 defaultTargetUrl 프로퍼티로 이동할 것이다.
Acegi Security에는 HTTP 헤더에 제공되는 기본적인 인증 신원정보를 처리할 수 있는
BasicProcessingFilter가 제공된다.
BasicProcessingFilter는 일반적인 사용자 에이전트(인터넷 익스플로러나
넷스케이프 네비게이터) 뿐만 아니라 Spring 원격 프로토콜(Hessian과 Burlap과 같은)에 의해
만들어진 요청에 대한 인증을 수행하는데 사용될 수 있다. HTTP Basic 인증을 제어하는 표준은
RFC 1945의 11장에 정의되어 있으며, BasicProcessingFilter는 이 RFC를
준수한다. Basic 인증은 인증에 있어 매력적인 접근법인데, Basic 인증이 사용자 에이전트에
매우 널리 배포되어 있으며 극도로 단순하기 때문이다(단순히 사용자명:비밀번호를
Base64로 인코딩하여 HTTP 헤더에 지정하기만 하면 된다).
HTTP Basic 인증을 구현하려면 BasicProcessingFilter를 필터 체인에
정의할 필요가 있다. 애플리케이션 컨텍스트에 BasicProcessingFilter와
필요한 협력자들을 정의한다:
<bean id="basicProcessingFilter" class="org.acegisecurity.ui.basicauth.BasicProcessingFilter">
<property name="authenticationManager"><ref bean="authenticationManager"/></property>
<property name="authenticationEntryPoint"><ref bean="authenticationEntryPoint"/></property>
</bean>
<bean id="authenticationEntryPoint" class="org.acegisecurity.ui.basicauth.BasicProcessingFilterEntryPoint">
<property name="realmName"><value>Name Of Your Realm</value></property>
</bean>
설정된 AuthenticationManager는 각 인증 요청들을 처리한다.
인증이 실패하면 설정된 AuthenticationEntryPoint를 이용하여
인증 절차를 재시도할 것이다. 일반적으로 여러분은
BasicProcessingFilterEntryPoint를 사용할 것이며, 이것은
HTTP Basic 인증을 재시도하기 위해 적절한 헤더를 포함하고 있는 401 응답을 반환한다.
인증이 성공하면 반환되는 Authentication 객체가
SecurityContextHolder에 위치할 것이다.
인증 시도가 성공하거나 HTTP 헤더가 지원되는 인증 요청을 포함하고 있지 않아
인증이 시도되지 않을 경우 필터 체인은 보통의 경우처럼 계속 진행할 것이다.
필터 체인이 중단되는 유일한 순간은 인증이 실패하고
AuthenticationEntryPoint가 요청되는 경우이며,
이는 앞 문단에서 논의하였다.
Acegi Security에는 HTTP 헤더 안에 들어있는 다이제스트 인증 신원정보를 처리할 수 있는
DigestProcessingFilter를 제공한다. 다이제스트 인증(Digest Authentication)은
구체적으로 말해 신원정보가 유선을 통해 클리어 텍스트(clear text)로
전달된 적이 없는지를 확인함으로써 Basic 인증이 가진 여러 약점들을 해결하려 한다.
다이제스트 인증을 지원하는 여러 사용자 에이전트에는 파이어폭스(FireFox)와
인터넷 익스플로러(Internet Explorer)가 포함되어 있다. HTTP 다이제스트 인증을
관장하는 표준은 RFC 2617에 정의되어 있는데, RFC 2617은 RFC 2069에 이미 기술되어 있는
다이제스트 인증 표준의 구 버전을 갱신한 것이다. 대부분의 사용자 에이전트들은
RFC 2617을 구현하고 있다. Acegi Security의 DigestProcessingFilter는 RFC 2617에
이미 기술되어 있는 "인증("auth")"에 대한
보호 품질(QOP; Quality of Protection)과 호환되며,
뿐만 아니라 RFC 2069와의 하위 호환성도 갖추고 있다. 다이제스트 인증은 여러분이
암호화되지 않은 HTTP(예를 들면, TLS/HTTPS가 아닌)를 사용하고 있으며, 인증 과정상의
보안을 최대화하고자 할 경우에 선택할 수 있는 상당히 매력적인 대안이다. 실제로
다이제스트 인증은 WebDAV 프로토콜에서는 필수적으로 지원되어야 하는 요구사항이며
RFC 2518 섹션 17.1에 지정되어 있으므로 배포되는 다이제스트 인증이 점차 늘어나
Basic 인증을 대체하는 것을 볼 수 있기를 기대한다.
비록 보안기능을 추가하는 것이 사용자 에이전트의 구현을 더 복잡하게 만드는 것을 의미하기는 하지만 폼 인증, Basic 인증, 다이제스트 인증 중에서는 말할 것도 없이 다이제스트 인증이 가장 안전한 선택이다. 다이제스트 인증의 중심에는 "비표(nonce)"가 있다. 비표는 서버가 만들어내는 값이며 Acegi Security의 비표는 다음의 형식을 도입하고 있다:
base64(expirationTime + ":" + md5Hex(expirationTime + ":" + key)) expirationTime: 비표가 폐기되는 날짜/시간이며 밀리세컨드 단위로 표현된다. key: 비표 토큰이 변경되는 것을 방지하는 비밀 키
DigestProcessingFilterEntryPoint는 비표 토큰의 폐기시간(기본값은 300이며
5분과 같다)을 결정하는데 사용되는 nonceValiditySeconds 프로퍼티와 함께
비표 토큰을 생성하는데 사용되는 key를 지정하는 프로퍼티를 포함하고 있다.
비표가 유효하더라도 다이제스트는 사용자명, 비밀번호, 비표, 요청 URI,
클라이언트가 만들어내는 비표(각 요청에 대해 사용자 에이전트가 만들어내는
임의의 값), 영역명(realm name) 등을 포함하는 다양한 문자열을 연결한 다음,
MD5 해싱을 수행하여 계산된다. 서버와 사용자 에이전트 모두 이러한
다이제스트 연산을 수행하므로 만약 포함되어 있는 값(예, 비밀번호)에 대해
서버와 사용자 에이전트가 서로 불일치할 경우 서로 다른 해시 코드가 만들어지게 된다.
Acegi Security 구현체에서는 만약 서버가 만들어낸 비표가 폐기되면
(그러나 그와 달리 다이제스트는 유효할 경우), DigestProcessingFilterEntryPoint가
"stale=true" 헤더를 전송할 것이다. 이렇게 하는 것이 사용자 에이전트가 사용자를
방해할 필요는 없으므로(비밀번호와 사용자명 등이 적절하지 않기 때문)
단순히 새로운 비표를 이용하여 재시도하도록 알린다.
DigestProcessingFilterEntryPoint의
nonceValiditySeconds 매개변수로
어떠한 값이 적절한지는 여러분의 애플리케이션에 달려있을 것이다. 주의할 점은
극도로 안전해야 하는 애플리케이션의 경우 가로채어진 인증 헤더가
expirationTime이 담긴 비표가 도착할 때까지 인증 주체를
사칭하는 데 사용될 수 있다는 것이다.
이것은 적절한 설정값을 선택하는데 있어 중요한 원칙이나 우선적으로
TLS/HTTPS상으로 실행되지는 않을 굉장히 안전한 애플리케이션에서는
일반적인 설정이 아닐 수도 있다.
다이제스트 인증이 좀 더 복잡한 구현체이기 때문에 종종 사용자 에이전트와 관련된
문제가 발생하곤 한다. 예를 들면 인터넷 익스플로러는 동일한 세션상의 연속적인
요청에 대한 "불투명한(opaque)" 토큰을 제시하지 못한다.
따라서 Acegi Security 필터는 대신 모든 상태 정보를 "비표(nonce)"
토큰에 캡슐화한다. 우리가 테스트했을 때 Acegi Security 구현체는 비표 시간 제한 등을
적절히 처리하는 등 파이어폭스와 인터넷 익스플로러에서 안정적으로 동작하였다.
이제 이론을 살펴보았으므로 어떻게 사용하는지 알아보기로 하자.
HTTP 다이제스트 인증을 구현하려면 DigestProcessingFilter를 필터 체인에
정의할 필요가 있다. 애플리케이션 컨텍스트에 DigestProcessingFilter와
필요한 협력자들을 정의할 필요가 있을 것이다:
<bean id="digestProcessingFilter" class="org.acegisecurity.ui.digestauth.DigestProcessingFilter">
<property name="userDetailsService"><ref local="jdbcDaoImpl"/></property>
<property name="authenticationEntryPoint"><ref local="digestProcessingFilterEntryPoint"/></property>
<property name="userCache"><ref local="userCache"/></property>
</bean>
<bean id="digestProcessingFilterEntryPoint" class="org.acegisecurity.ui.digestauth.DigestProcessingFilterEntryPoint">
<property name="realmName"><value>Contacts Realm via Digest Authentication</value></property>
<property name="key"><value>acegi</value></property>
<property name="nonceValiditySeconds"><value>10</value></property>
</bean>
설정된 UserDetailsService는
DigestProcessingFilter가 사용자의 클리어 텍스트 비밀번호에
직접 접근해야 하므로 필요하다. 다이제스트 인증은 여러분이
DAO에 들어있는 인코딩된 비밀번호를 사용하고 있을 경우 작동하지 않을 것이다.
UserCache와 더불어 DAO 협력자는 일반적으로
DaoAuthenticationProvider와 직접적으로 공유된다.
authenticationEntryPoint 프로퍼티는
DigestProcessingFilterEntryPoint이어야 하며,
따라서 DigestProcessingFilter는 다이제스트 연산에 필요한
realmName과 key를 획득할 수 있다.
BasicAuthenticationFilter에서 처럼 인증이 성공할 경우
Authentication 요청 토큰이
SecurityContextHolder에 위치할 것이다. 만약 인증 시도가
성공하거나 HTTP 헤더가 다이제스트 인증 요청을 포함하고 있지 않아 인증이
시도되지 않았을 경우 필터 체인은 평상시대로 진행할 것이다. 필터 체인이
중단되는 유일한 시점은 앞 단락에서 논의했던것 처럼 인증이 실패하거나
AuthenticationEntryPoint가 호출되는 경우이다.
다이제스트 인증의 RFC는 보안성을 증대시키기 위한 광범한 추가 기능들을 제공한다. 예를 들어, 비표는 모든 요청마다 변경될 수 있다. 이렇게 함에도 불구하고 Acegi Security 구현체는 구현체의 복잡성을 최소화하고(그리고 발생할 수 있는 사용자 에이전트의 호환성 없음은 의심할 것도 없이), 서버측 상태를 저장할 필요성을 되도록이면 줄이도록 설계되었다. 만약 여러분이 이러한 기능들을 좀 더 자세히 알아보길 원한다면 RFC 2617을 검토해 보길 바란다. 우리가 아는 한 Acegi Security 구현체는 RFC 2617의 최소한의 표준만을 준수하고 있다.
특히 웹 요청 URI 보안의 경우, 간혹 모든 가능한 안전 객체 호출에 대해
설정 속성을 할당하는 것이 더 편리할 때가 있다. 달리 말하자면,
간혹 기본적으로 ROLE_SOMETHING을 필요로 하는데,
이 규칙에는 로그인, 로그아웃 그리고 애플리케이션의 기본 페이지와 같은
특정 예외만을 허용하는 것이 더 나을 때가 있다는 것이다. 게다가 가령 감사
인터셉터(auditing interceptor)가 SecurityContextHolder로
하여금 특정 인증주체가 주어진 오퍼레이션에 대한 책임을 갖고 있는지 확인하기 위해
조회하는 경우와 같이 익명 인증(anonymous authentication)이 요구되는 다른 상황도
있다. 그러한 클래스들은 SecurityContextHolder가 항상
Authentication 객체를 포함하고 있고 결코 null이
아님을 알고 있을 경우 좀 더 강건하게 만들어질 수 있다.
Acegi Security에는 익명 인증 기능을 제공하기 위해 서로 결합되는
세 개의 클래스들을 제공하고 있다. AnonymousAuthenticationToken은
Authentication의 구현체이며 익명 인증 주체에 해당되는
GrantedAuthority[]를 저장한다. 대응되는
AnonymousAuthenticationProvider는
ProviderManager에 연결되어 있으므로
AnonymousAuthenticationTokens가
받아들여진다. 마지막으로 AnonymousProcessingFilter가 있는데,
AnonymousProcessingFilter는 일반적인 인증 메커니즘에 이어서 연결되며,
SecurityContextHolder에 이미 존재하는
Authentication이 들어있지 않을 경우
자동적으로 AnonymousAuthenticationToken을
SecurityContextHolder에 추가한다.
필터와 인증 제공자에 대한 정의는 다음과 같다:
<bean id="anonymousProcessingFilter" class="org.acegisecurity.providers.anonymous.AnonymousProcessingFilter">
<property name="key"><value>foobar</value></property>
<property name="userAttribute"><value>anonymousUser,ROLE_ANONYMOUS</value></property>
</bean>
<bean id="anonymousAuthenticationProvider" class="org.acegisecurity.providers.anonymous.AnonymousAuthenticationProvider">
<property name="key"><value>foobar</value></property>
</bean>
key는 필터와 인증 제공자 사이에서 공유되므로 필터가
만들어 내는 토큰은 제공자에게 받아들여 진다. userAttribute는
usernameInTheAuthenticationToken,grantedAuthority[,grantedAuthority]의
형태로 표현된다. 이는 InMemoryDaoImpl의 userMap 프로퍼티의
등호 기호 뒤에서 사용되는 것과 동일한 문법이다.
앞서 설명했던 것과 같이 익명 인증의 이점은 모든 URI 패턴에 보안을 적용할 수 있다는 것이다. 아래는 그러한 예를 보여준다:
<bean id="filterInvocationInterceptor" class="org.acegisecurity.intercept.web.FilterSecurityInterceptor">
<property name="authenticationManager"><ref bean="authenticationManager"/></property>
<property name="accessDecisionManager"><ref local="httpRequestAccessDecisionManager"/></property>
<property name="objectDefinitionSource">
<value>
CONVERT_URL_TO_LOWERCASE_BEFORE_COMPARISON
PATTERN_TYPE_APACHE_ANT
/index.jsp=ROLE_ANONYMOUS,ROLE_USER
/hello.htm=ROLE_ANONYMOUS,ROLE_USER
/logoff.jsp=ROLE_ANONYMOUS,ROLE_USER
/acegilogin.jsp*=ROLE_ANONYMOUS,ROLE_USER
/**=ROLE_USER
</value>
</property>
</bean>
익명 인증에 관한 논의는 AuthenticationTrustResolver 인터페이스와
이 인터페이스에 대응되는 AuthenticationTrustResolverImpl 구현체에 관한
것으로 마무리 짓도록 하자. AuthenticationTrustResolver 인터페이스는
isAnonymous(Authentication) 메소드를 제공하는데, 이 메소드는 관심있는
클래스가 이러한 특별한 유형의 인증 상태를 고려하도록 해준다.
ExceptionTranslationFilter는 AccessDeniedException을 처리하며
이 인터페이스를 이용한다. 만약 AccessDeniedException이 던져지고,
그리고 인증이 익명 타입이면, 403(접근금지) 응답 대신 필터는
AuthenticationEntryPoint를 시작하여 인증주체가 적절히 인증을 받을 수
있도록 할 것이다. 이는 구별될 필요가 있는며, 그렇지 않을 경우 인증 주체가 항상
"인증된" 것으로 여겨져 폼이나 BASIC, 아니면 일반적인 인증 메커니즘을
통해 로그인할 수 있는 기회가 주어지지 않을 것이기 때문이다.
Remeber-me 인증은 웹 사이트가 세션간 인증주체의 식별성(identity)을 기억할 수 있는 것을 말한다. 이것은 일반적으로 브라우저에 쿠키를 전달함으로써 이루어지는데, 쿠키는 나중에 만들어지는 세션에서 발견되며, 자동화된 로그인이 발생할 수 있도록 한다. Acegi Security에서는 필요한 훅을 제공하여 그러한 오퍼레이션들이 일어날 수 있도록 할 뿐만 아니라 쿠키 기반의 토큰에 대한 보안을 보존하기 위해 해싱을 사용하는 구상 구현체를 제공하고 있다.
Remember-me 인증은 BASIC 인증에는 사용되지 않는데, BASIC 인증은
종종 HttpSession을 이용하지 않기 때문이다.
Remember-me는 AuthenticationProcessingFilter와 함께
사용되며, 그리고 AbstractProcessingFilter 상위 클래스에
들어있는 훅을 통해 구현된다. 훅은 적절한 시기에 구상
RememberMeServices를 호출할 것이다.
인터페이스는 다음과 같다:
public Authentication autoLogin(HttpServletRequest request, HttpServletResponse response); public void loginFail(HttpServletRequest request, HttpServletResponse response); public void loginSuccess(HttpServletRequest request, HttpServletResponse response, Authentication successfulAuthentication);
위 메소드가 어떠한 동작을 하는지에 대한 자세한 사항은 JavaDoc을 참조하길 바라며,
이 단계에서는 AbstractProcessingFilter가 오직
loginFail()과 loginSuccess() 메소드만을
호출한다는 것만을 기억해 두기 바란다. autoLogin() 메소드는
SecurityContextHolder가 Authentication을
포함하고 있지 않을 때마다 RememberMeProcessingFilter에 의해 호출된다.
그러므로 이 인터페이스는 인증과 관련된 이벤트에 대해 충분히 통지를 하는,
기저의 remember-me 구현체를 제공하고 있으며, 후보(candidate) 웹 요청이
쿠키를 포함하고 있고 기억되기를 희망할 때마다 그 구현체에 위임한다.
이러한 설계 덕분에 다양한 remember-me 구현 전략을 구사할 수 있다.
단순함과 더불어 기록 및 생성 메소드를 지정하는 DAO 구현체가 필요 없도록 하기 위해
Acegi Security는 오직 구상 구현체인 TokenBasedRememberMeServices를 제공하며
이 구현체는 해싱을 이용하여 유용한 remember-me 전략을 완수한다. 본질적으로는
성공적으로 상호간의 인증을 이루기 위해 쿠키가 브라우저로 전달되며,
그 쿠키는 다음과 같이 구성되어 있다:
base64(username + ":" + expirationTime + ":" + md5Hex(username + ":" + expirationTime + ":" password + ":" + key)) username: TokenBasedRememberMeServices.getUserDetailsService()로 식별가능한 것 password: TokenBasedRememberMeServices.getUserDetailsService()로 전달받은 UserDetails에 대응되는 것과 일치하는 것 expirationTime: remember-me 토큰이 폐기될 때의 날짜 및 시간. 밀리세컨드 단위로 표현됨. key: remember-me 토큰의 변경을 방지하기 위한 비밀키
이러한 remember-me 토큰은 지정된 기간에 대해서만 유효하며, 사용자명과 비밀번호 및 키는 변경되지 않은 채로 제공된다. 특히 이는 가로채어진 remember-me 토큰이 특정 사용자 에이전트가 토큰이 폐기되는 시점까지 사용될 수 있다는 점에서 잠재적인 보안 문제를 내포하고 있다. 이것은 다이제스트 인증에 있는 것과 동일한 문제이다. 만약 인증 주체가 토큰이 가로채어졌다는 것을 인식할 경우, 인증 주체는 손쉽게 비밀번호를 변경하고 곧바로 발행된 모든 remember-me 토큰을 무효화(invalidate)할 수 있다. 하지만 좀더 중요한 보안이 필요할 경우 rolling token approach가 사용되어야 하거나(데이터베이스가 필요함) 아니면 단순히 remember-me 서비스를 사용하지 말아야 한다.
TokenBasedRememberMeServices는
RememberMeAuthenticationToken을 생성하며,
RememberMeAuthenticationToken은
RememberMeAuthenticationProvider에 의해 처리된다.
이러한 인증 제공자와 TokenBasedRememberMeServices간에는
key가 공유된다. 더불어
TokenBasedRememberMeServices는
UserDetailsService를 필요로 하는데,
이러한 UserDetailsService에서 서명을 비교할 목적으로
사용자명과 비밀번호를 검색할 수 있으며,
RememberMeAuthenticationToken을 생성하여 올바른
GrantedAuthority[]를 담는다. 사용자 요청에 의해
쿠키를 무효화하는 애플리케이션(일반적으로 JSP를 통해)에 의해 일종의
로그아웃 명령이 제공되어야 한다. 예제를 보려면 연락처(Contacts) 예제 애플리케이션의
logout.jsp을 참조하도록 한다.
remember-me 서비스를 활성화하기 위해 애플리케이션 컨텍스트에서 필요로 하는 빈은 다음과 같다:
<bean id="rememberMeProcessingFilter" class="org.acegisecurity.ui.rememberme.RememberMeProcessingFilter">
<property name="rememberMeServices"><ref local="rememberMeServices"/></property>
</bean>
<bean id="rememberMeServices" class="org.acegisecurity.ui.rememberme.TokenBasedRememberMeServices">
<property name="userDetailsService"><ref local="jdbcDaoImpl"/></property>
<property name="key"><value>springRocks</value></property>
</bean>
<bean id="rememberMeAuthenticationProvider" class="org.acegisecurity.providers.rememberme.RememberMeAuthenticationProvider">
<property name="key"><value>springRocks</value></property>
</bean>
여러분의
AuthenticationProcessingFilter.setRememberMeServices()
프로퍼티에 RememberMeServices 구현체를 추가하는 것과
AuthenticationManager.setProviders() 목록에
RememberMeAuthenticationProvider를 포함하는 것, 그리고
FilterChainProxy(일반적으로 여러분의
AuthenticationProcessingFilter 바로 다음에)에
RememberMeProcessingFilter에 대한 요청을
추가하는 것을 잊지 않도록 한다.
X509 인증서 인증을 사용하는 가장 일반적인 경우는 브라우저에서 HTTPS를 사용하여 SSL을 이용하는 서버의 신원을 검증하는 경우이다. 브라우저는 자동적으로 서버에서 유지하고 있는 신뢰할 수 있는 인증기관(certificate authority)에서 제시한 인증서의 발행 여부(예, 전자서명 여부) 확인할 것이다.
또한 여러분은 상호 인증(mutual authentication)을 통해 SSL을 사용할 수 도 있는데, 그럴 경우 서버는 클라이언트로부터 SSL의 핸드쉐이크(handshake)의 일부로서 유효한 인증서를 요청할 것이다. 서버는 인증서가 수락가능한 인증기관(acceptable authority)에 의해 서명되었는지를 확인하여 클라이언트를 인증할 것이다. 만약 유효한 인증서가 제공되었을 경우 서버는 애플리케이션내의 서블릿 API를 통해 유효한 인증서를 획득할 수 있다. Acegi Security X509 모듈은 필터를 이용하여 인증서를 추출한 다음 그것을 설정된 X509 인증 제공자에게 전달하여 추가적인 애플리케이션에 특화되어 있는 확인 절차가 이루어지도록 한다. 또한 Acegi Security X509는 인증서를 애플리케이션 사용자와 맵핑하여 표준 Acegi Security 기반구조를 사용할 수 있는 허가된 권한을 불러들이기도 한다.
여러분은 Acegi Security를 이용하려 시도하기 전에 서블릿 컨테이너에 대한 인증서를 사용하고 클라이언트 인증을 설정하는데 익숙해져야 한다. 대부분의 작업은 적절한 인증서와 키를 설치하는 것이다. 예를 들면 여러분이 톰캣을 사용하고 있다면 http://jakarta.apache.org/tomcat/tomcat-5.0-doc/ssl-howto.html을 참조하도록 한다. 중요한 것은 여러분이 Acegi Security를 이용하여 X509 인증을 시도 해보기 전에 위 문서를 이용하여 직접 해보는 것이다.
X509 인증을 이용할 경우, 명시적으로 아무런 로그인 절차가 없으므로 구현은 상대적으로 간단해지며, 사용자와 상호작용하기 위해 요청을 재지정할 필요가 없다. 그 결과 몇몇 클래스는 다른 패키지에 들어있는 것들과는 약간 다르게 동작한다. 예를 들어, 일반적으로 인증절차를 시작하는 책임을 가진 기본 진입점 클래스는 인증서가 거부되었을 경우에만 호출되며 그리고 항상 오류를 사용자에게 반환한다. 적절한 빈 설정을 이용하면 일반적인 이벤트 흐름은 다음과 같다.
X509ProcessingFilter가 요청으로부터
인증서를 추출하고 그것을 인증 요청에 대한 신원정보로서 사용한다.
생성된 인증 요청은 X509AuthenticationToken이며
요청은 인증 관리자에게 전달된다.
X509AuthenticationProvider가 토큰을 받는다.
X509AuthenticationProvider의
주요 관심사는 인증서와 일치하는 사용자 정보(특히 사용자의 허가된 권한)를
획득하는 것이다. X509AuthoritiesPopulator는
이러한 책임을 X509AuthoritiesPopulator에게 위임한다.
populator의 메소드 중의 하나인
getUserDetails(X509Certificate userCertificate)가 호출된다.
구현체는 사용자에 대한 GrantedAuthority 객체의 배열을 담고 있는
UserDetails 인스턴스를 반환해야 한다. 또한 이 메소드는 인증서를 거부하기를
선택할 수도 있다(예를 들어 일치하는 사용자명을 포함하고 있지 않는 경우).
이 경우 populator는 BadCredentialsException을 던져야 한다.
DAO 기반의 구현체인 DaoX509AuthoritiesPopulator는 사용자명을 인증서내의
“common name”(CN) 항목으로부터 추출하여 제공한다.
또한 DaoX509AuthoritiesPopulator는 여러분만의 정규 표현식을 지정하여
제목에서 구별되어진 이름의 각기 다른 부분이 일치하도록 할 수도 있다.
사용자 정보는 UserDetailsService를 이용하여 불러온다.
만약 모든 것이 매끄럽게 진행된다면 유효한 Authentication
객체만이 안전 컨텍스트(secure context)에 남겨질 것이며 호출은 원래대로 진행될 것이다.
인증서가 아무것도 발견되지 않거나 인증서가 거부되었을 경우
ExceptionTranslationFilter는 사용자에게
403 오류(접근금지)를 반환하는
X509ProcessingFilterEntryPoint를 호출할 것이다.
참조 가이드에는 X509를 사용하는 여러 버전의 연락처 예제 애플리케이션이 포함되어 있다. 여기에서는 빈 설정 및 필터 설정을 여러분만의 애플리케이션 설정에 대한 시작지점으로서 복사한다. 일련의 예제 인증서들 또한 포함되어 있으므로 여러분의 서버를 설정하는데 그것들을 사용할 수 있다. 그러한 예제 인증서에는 다음과 같은 것들이 포함된다.
marissa.p12: PKCS12 형식의 파일로서 클라이언트 키와 인증서를
포함하고 있다. 이것들은 여러분이 브라우저에 설치되어 있어야 한다.
이 인증서는 애플리케이션의 “marissa” 사용자에 맵핑된다.
server.p12: 서버 인증서 및 HTTPS 연결에 대한 키
ca.jks: 자바 키 저장소(keystore)로서 marissa의 인증서를
발행한 인증기관에 대한 인증서를 포함하고 있다. 이 인증서는 컨테이너에 의해
클라이언트 인증서의 유효성을 검증하는데 사용될 것이다.
JBoss 3.2.7(그리고 톰캣 5.0)의 경우 SSL 설정은
server.xml 파일에 다음과 같이 들어있을 것이다.
<!-- SSL/TLS Connector configuration -->
<Connector port="8443" address="${jboss.bind.address}"
maxThreads="100" minSpareThreads="5" maxSpareThreads="15"
scheme="https" secure="true"
sslProtocol = "TLS"
clientAuth="true" keystoreFile="${jboss.server.home.dir}/conf/server.p12"
keystoreType="PKCS12" keystorePass="password"
truststoreFile="${jboss.server.home.dir}/conf/ca.jks"
truststoreType="JKS" truststorePass="password"
/>
만약 클라이언트에서 인증서를 제공하지 않는 경우라도 SSL 연결을
이어지게 하고 싶을 경우에는 clientAuth를
true로 설정할 수 있다.
확실한 것은 이러한 클라이언트들은 Acegi Security에 의해 보호받는 어떠한 객체에도
접근할 수 없게 되리라는 것이다(여러분이 비X509 인증 메커니즘을 사용하지 않고,
BASIC 인증을 이용하여 사용자를 인증할 경우).
LDAP은 사용자 정보와 인증 서비스에 대한 중앙 저장소로서 종종 조직에서 사용된다. 뿐만 아니라 LDAP은 애플리케이션 사용자에 대한 역할 정보를 저장하는데 사용되기도 한다.
LDAP 서버를 구성하여 Acegi LDAP 제공자를 완전히 설정하는 데에는 여러 가지 시나리오가 있을 수 있다. Acegi LDAP 제공자는 인증과 역할 검색에 대한 별도의 전략 인터페이스를 사용하며 다양한 상황을 처리하도록 설정할 수 있는 기본 구현체를 제공하고 있다.
여러분은 Acegi에서 Acegi LDAP 제공자를 사용해보기 전에 LDAP에 익숙해질 필요가 있다. 다음 링크는 무료 LDAP 서버인 OpenLDAP를 사용하는데 있어 적절한 개념 소개와 디렉터리를 구성하는 것에 관한 지침을 제공해 준다: http://www.zytrax.com/books/ldap/. 자바에서 LDAP에 접근하는데 사용되는 JNDI API에 익숙할 경우 많은 도움이 될 것이다. 우리는 LDAP 제공자에 어떠한 써드파티 LDAP 라이브러리(모질라/네스케이프, JLDAP 등)도 사용하고 있지 않다.
주요 LDAP 제공자 클래스는
org.acegisecurity.providers.ldap.LdapAuthenticationProvider이다.
이 빈은 실제로 이 빈의 상위 클래스인 AbstractUserDetailsAuthenticationProvider에서
구현하기를 요구하는 retrieveUser 메소드를 구현하는 것 말고는
크게 많은 일을 하지는 않는다. LdapAuthenticationProvider는
그 일을 다른 빈인 LdapAuthenticator와
LdapAuthoritiesPopulator에게 위임하며 이러한 빈들은
상대적으로 사용자를 인증하고 사용자의 GrantedAuthority의 집합을 검색하는 책임을 맡고 있다.
인증자(authenticator)는 필요한 사용자 속성을 검색하는 책임을 맡고 있기도 하다. 이는 그러한 속성들에 대한 접근 권한이 사용되고 있는 인증 유형에 의존할 수도 있기 때문이다. 예를 들면 사용자로 바인딩할 경우 사용자의 고유 접근권한을 읽는데 인증자가 필요할 수도 있다.
현재 Acegi Security에서는 두 가지의 인증 전략이 존재한다:
LDAP 서버에 직접적으로 연결된 Authentication("바인딩" 인증).
비밀번호 비교는 사용자가 제공한 비밀번호를 저장소에 있는 것과 비교하는 것이다. 이는 비밀번호 속성의 값을 가져와 그것을 로컬에 있는 것과 비교하거나, 아니면 제공되는 비밀번호를 비교하기 위해 서버에 전달하며 실제 비밀번호의 값은 검색되지 않는 LDAP의 "compare" 연산을 수행함으로써 이루어질 수 있다.
사용자를 인증할 수 있기 전에(두 전략에 의해), 애플리케이션에 공급되는
로그인명으로부터 DN(distinguished name)을 획득해야 한다. 이는 단순한
패턴 매칭(setUserDnPatterns 배열 프로퍼티를 지정함으로써)이나
userSearch 프로퍼티를 설정함으로써 이루어질 수 있다.
DN 패턴 매칭 접근법의 경우 표준 자바 패턴 형식이 사용되며, 그리고 로그인명은
{0} 파라미터로 대체될 것이다. 이 패턴은 설정된
InitialDirContextFactory가 바인딩 될 DN에 대응되어야
한다(이에 대한 자세한 내용은 LDAP 서버에 연결하기에
관한 섹션을 참고하도록 한다). 예를 들어 여러분이
ldap://monkeymachine.co.uk/dc=acegisecurity,dc=org와 같은
URL에 의해 지정된 LDAP 서버를 사용하고 있고, uid={0},ou=greatapes 패턴을
갖고 있으면 "gorilla"의 로그인명은
uid=gorilla,ou=greatapes,dc=acegisecurity,dc=org라는 DN에
맵핑될 것이다. 각각의 설정된 DN 패턴은 일치하는 것이 발견될때까지 시도될 것이다.
검색을 사용하는 것에 관한 정보를 보려면 아래의 객체 검색에 관한 섹션을 참고하도록 한다.
두 접근법을 조합해서 사용할 수도 있는데, 먼저 패턴을 검사하여 일치하는
DN이 발견되지 않으면 검색이 사용될 것이다.
org.acegisecurity.providers.ldap.authenticator.BindAuthenticator 클래스는
바인딩 인증 전략을 구현한다. BindAuthenticator 클래스는 단순히
사용자로서 바인딩을 시도한다.
org.acegisecurity.providers.ldap.authenticator.PasswordComparisonAuthenticator
클래스는 비밀번호 인증 전략을 구현한다.
위에서 언급한 빈은 서버에 연결될 수 있어야 한다. 위 빈들은 모두
InitialDirContextFactory 인스턴스에서
제공되어야 한다. 만약 여러분이 특수한 요구사항을 갖고 있지 않다면 일반적으로
DefaultInitialDirContextFactory 빈이 될 것이며,
DefaultInitialDirContextFactory는
여러분의 LDAP 서버에 대한 URL과 선택적으로 서버에 바인딩될
때(익명으로 바인딩되는 대신) 기본값으로 사용될 "관리자(manager)" 사용자의 사용자명과
비밀번호가 설정될 수 있다. 현재는 "단순 LDAP 인증"만을
지원하고 있다.
DefaultInitialDirContextFactory는 기본적으로 썬의 JNDI LDAP
구현체를 사용한다(JDK에 탑재되어 있음). 또한 DefaultInitialDirContextFactory는
썬(Sun)에서 제공하는 내장 커넥션 풀링을 지원하기도 한다.
익명이나 "관리자" 사용자의 신원으로 획득된 연결은 자동적으로 풀링될 것이다.
반면 특정 사용자의 신원으로 획득된 연결은 풀링되지 않을 것이다.
커넥션 풀링은 useConnectionPool 프로퍼티를 false로
지정함으로써 완전히 비활성화할 수 있다.
이 빈을 비롯한 프로퍼티에 관해 상세한 정보를 보려면 클래스에 대한 JavaDoc과 소스코드를 참조하도록 한다.
단순한 DN-매칭 보다 훨씬 더 복잡한 전략은 사용자 내역이
디렉터리에 위치하기를 요구한다. 이는 인증자(authenticator) 구현에
제공될 수 있는 LdapUserSearch 인스턴스에 캡슐화될 수 있는데,
예를 들면 인증자 구현체가 사용자를 위치할 수 있도록 해주는 것을 들 수 있으며,
제공된 구현체는 FilterBasedLdapUserSearch이다.
이 빈은 LDAP 필터를 이용하여 디렉터리 내의 사용자 객체를 일치시킨다.
이러한 과정은
JDK DirContext 클래스상의 해당 검색 메소드에 관한 Javadoc에 설명되어 있다.
그곳에 설명되어 있는 것처럼 검색 필터를 매개변수로 제공할 수 있다.
이러한 클래스에 대한 유효한 매개변수는 사용자의 로그인명으로 교체될
{0} 뿐이다.
Acegi Security에는 LDAP을 이용하는 연락처 예제 애플리케이션을 제공하고 있다. 여러분은 이 예제에서 빈과 필터 설정을 복사하여 여러분만의 애플리케이션을 설정하는 시작점으로 활용할 수 있다.
위에서 언급한 빈을 사용하는 일반적인 설정은 아래와 같을 것이다:
<bean id="initialDirContextFactory"
class="org.acegisecurity.ldap.DefaultInitialDirContextFactory">
<constructor-arg value="ldap://monkeymachine:389/dc=acegisecurity,dc=org"/>
<property name="managerDn"><value>cn=manager,dc=acegisecurity,dc=org</value></property>
<property name="managerPassword"><value>password</value></property>
</bean>
<bean id="userSearch"
class="org.acegisecurity.ldap.search.FilterBasedLdapUserSearch">
<constructor-arg index="0">
<value></value>
</constructor-arg>
<constructor-arg index="1">
<value>(uid={0})</value>
</constructor-arg>
<constructor-arg index="2">
<ref local="initialDirContextFactory" />
</constructor-arg>
<property name="searchSubtree">
<value>true</value>
</property>
</bean>
<bean id="ldapAuthProvider"
class="org.acegisecurity.providers.ldap.LdapAuthenticationProvider">
<constructor-arg>
<bean class="org.acegisecurity.providers.ldap.authenticator.BindAuthenticator">
<constructor-arg><ref local="initialDirContextFactory"/></constructor-arg>
<property name="userDnPatterns"><list><value>uid={0},ou=people</value></list></property>
</bean>
</constructor-arg>
<constructor-arg>
<bean class="org.acegisecurity.providers.ldap.populator.DefaultLdapAuthoritiesPopulator">
<constructor-arg><ref local="initialDirContextFactory"/></constructor-arg>
<constructor-arg><value>ou=groups</value></constructor-arg>
<property name="groupRoleAttribute"><value>ou</value></property>
</bean>
</constructor-arg>
</bean>
위 설정은 제공자를 설정하여
ldap://monkeymachine:389/dc=acegisecurity,dc=org라는
URL을 통해 LDAP 서버에 연결하도록 한다. 인증은
uid=<user-login-name>,ou=people,dc=acegisecurity,dc=org라는
DN을 통해 바인딩을 시도함으로써 수행될 것이다. 인증이 성공적으로 완료된 후 역할들은
기본 필터(member=<user's-DN>)를 이용하는
ou=groups,dc=acegisecurity,dc=org라는 DN들을 검색함으로써 사용자에게 할당될 것이다.
역할의 이름은 각각의 일치하는 것들의 “ou”로부터 획득될 것이다.
또한 위 설정에는 사용자 검색 객체에 대한 설정도
포함되어 있는데, 사용자 검색 객체는 필터(uid=<user-login-name>)를
사용하고 있다. 이는 DN-패턴 대신 사용될 수 있으며(아니면 그것에 추가하거나),
인증자의 userSearch 프로퍼티를 설정함으로써 가능하다.
그리고 나서 인증자는 다음 사용자로 바인딩을 시도하기 전에 검색 객체를 호출하여
적절한 사용자의 DN을 획득한다.
JA-SIG는 CAS로 알려져 있는 엔터프라이급 싱글 사인 온(single sign on) 시스템을 제공한다. 다른 이니셔티브와는 달리 JA-SIG의 중앙 인증 서비스 (Central Authentication Service)는 오픈 소스이며, 널리 사용되며, 간단하게 이해할 수 있으며, 플랫폼 독립적이고, 그리고 프록시 기능을 지원한다. Acegi Security는 CAS를 완벽하게 지원하고 있으며, 그리고 Acegi Security에 대한 단일 애플리케이션 배포(single-application deployment)를 엔터프라이즈급 CAS 서버에 의해 보호받는 다중 애플리케이션 배포(multiple-application deployment)로 마이그레이션할 수 있는 수단을 제공한다.
여러분은 http://www.ja-sig.org/products/cas/에서 CAS에 관해
좀 더 배울 수 있다. 여러분은 CAS Server 파일을 다운로드할 수 있는 URL을
방문해볼 필요가 있을 것이다. Acegi Security에 "-with-dependencies" ZIP 파일에
두 개의 CAS 라이브러리가 포함되어 있긴 하지만 여러분은 여전히 CAS 서버를
커스터마이즈하고 배치하기 위해 CAS 자바 서버 페이지와
web.xml 파일이 필요할 것이다.
위의 CAS 웹 사이트에는 CAS 아키텍처를 상세하게 설명하고 있는 두 개의 문서를 포함하고 있으며 여기에서는 Acegi Security 관점에서 일반적인 개요만을 살펴볼 것이다. 다음은 CAS 2.0(Yale에서 만들어진)과 CAS 3.0(JA-SIG)를 설명하고 있으며 CAS 3.0은 Acegi Security를 지원하는 CAS 버전이다.
여러분은 기업 어딘가에 CAS 서버를 설정할 필요가 있을 것이다. CAS 서버는 단순히 평범한 WAR 파일이므로 여러분의 서버에 설치하는 데 그다지 어려움은 없을 것이다. 여러분은 WAR 파일에 들어있는 사용자에게 보여질 로그인 및 기타 싱글 사인 온 페이지를 조정할 필요가 있을 것이다.
여러분이 CAS 2.0을 배치하고 있다면 여러분은 web.xml에
PasswordHandler를 지정할 필요도 있을 것이다.
PasswordHandler에는 주어진 사용자명과 비밀번호의 유효성 여부를
나타내는 boolean 값을 반환하는 간단한 메소드가 하나 포함되어 있다.
여러분의 PasswordHandler 구현체는 LDAP 서버나 데이터베이스와 같은
특정 타입의 백엔드 인증 저장소에 연결할 필요가 있을 것이다.
여러분이 이미 기존 CAS 2.0 서버 인스턴스를 실행하고 있다면 여러분은 이미
수립된 PasswordHandler를 갖고 있는 셈이다. 여러분이 이전에
PasswordHandler를 갖고 있지 않았다면
Acegi Security의 CasPasswordHandler 클래스를 사용하고자
할 수도 있을 것이다. 이 클래스는 Acegi Security의
AuthenticationManager에 위임하여 벌써 갖고 있을지도
모를 보안 설정을 사용할 수 있도록 할 것이다. 원치 않을 경우 여러분은
CAS 서버에 CasPasswordHandler를 사용해야 할 필요는 없다.
Acegi Security는 여러분이 CAS 서버에 사용할 목적으로 어떠한
CasPasswordHandler를 선택했는지와는 관계없이
CAS 클라이언트로서 성공적으로 작동할 것이다.
만약 여러분이 CAS 3.0을 배치하고 있다면 여러분은 CAS에 포함된
deployerConfigContext.xml에
AuthenticationHandler를 지정할 필요도 있을 것이다.
AuthenticationHandler에 주어진 Credential 집합의
유효 여부를 나타내는 boolean값을 반환하는 간단한 메소드가 하나 포함되어 있다.
여러분의 AuthenticationHandler 구현체는 LDAP 서버나 데이터베이스와 같은
특정 유형의 백엔드 인증 저장소로 연결될 필요가 있을 것이다.
이를 보조하기 위해 CAS는 자체적으로 상당수의
AuthenticationHandler를 포함하고 있다.
만약 여러분이 이미 CAS 3.0 서버 인스턴스를 실행하고 있다면, 여러분은 이미
확립된 AuthenticationHandler를 갖고 있는 셈이다. 만약 여러분이 앞서
AuthenticationHandler를 갖고 있지 않았다면 여러분은 아마도 Acegi Security의
CasAuthenticationHandler 클래스를 사용하는 것을 선호할 수도 있다. 이 클래스는
Acegi Security의 AuthenticationManager에게 위임하여, 여러분이 이미 만들어 놓았을
보안 설정을 사용할 수 있도록 해준다. 여러분은 원한다면 여러분의 CAS 서버에
CasAuthenticationHandler 클래스를 사용할 필요는 없다. Acegi Security는 여러분이
CAS 서버에 사용할 목적으로 어떠한 AuthenticationHandler를 선택했는지와는 관계없이
CAS 클라이언트로서 성공적으로 작동할 것이다.
CAS 서버 자체에서 물러나 보면, 또 다른 주인공은 당연히 여러분 기업에 배치되어 있는 안전한 웹 애플리케이션이다. 이러한 웹 애플리케이션들은 "서비스(service)"로 알려져 있다. 서비스에는 두 가지 유형이 있는데, 각각 표준 서비스와 프록시 서비스이다. 프록시 서비스는 사용자의 행위에 근거하여 다른 서비스로부터 자원을 요청할 수 있다. 이는 차후에 상세히 설명할 것이다.
서비스는 매우 다양한 언어로 개발할 수 있는데, 이는 CAS 2.0의 매우 가벼운 XML 기반 프로토콜에 기인한다. JA-SIG CAS 홈페이지에는 Java, ASP, 펄, 파이썬 등의 CAS 클라이언트를 보여주는 클라이언트 저장소가 포함되어 있다. 원래부터 자바로 작성된 CAS 서버는 자바를 매우 강력하게 지원하고 있다. 여러분은 Acegi Security에 의해 보호받는 애플리케이션에는 어떠한 CAS 클라이언트 클래스도 사용할 필요가 없다. 이는 여러분을 위해 투명하게 처리된다.
Spring이 보호하는 서비스에 대한 웹 브라우저와 CAS 서버, 그리고 Acegi Security간의 기본적인 상호작용은 다음과 같다:
웹 사용자가 서비스의 공개 페이지를 탐색한다. CAS나 Acegi Security는 관여하지 않는다.
마침내 사용자가 보호받고 있거나 사용하고 있는 빈 중의 하나가 보호받고 있는
페이지를 요청한다. Acegi Security의 ExceptionTranslationFilter가
AuthenticationException을 감지할 것이다.
사용자의 Authentication 객체가
AuthenticationException을 초래하므로
ExceptionTranslationFilter는 설정된
AuthenticationEntryPoint를 호출할 것이다.
만약 CAS를 사용하고 있다면 이는 CasProcessingFilterEntryPoint
클래스가 될 것이다.
CasProcessingFilterEntry는 사용자의 브라우저를 CAS 서버로 재지정할 것이다.
또한 CasProcessingFilterEntry는 서비스 매개변수를 나타내기도 하는데,
이것은 Acegi Security 서비스에 대한 콜백 URL이다. 예를 들어, 브라우저를 재지정한 URL은
https://my.company.com/cas/login?service=https%3A%2F%2Fserver3.company.com%2Fwebapp%2Fj_acegi_cas_security_check이
될 것이다.
사용자의 브라우저가 CAS로 재지정되고 나면 사용자명과 비밀번호를 물어볼 것이다.
사용자가 이미 로그인했었다는 것을 나타내는 세션 쿠키를 제시할 경우 재차 로그인하기 위해
물어보지는 않을 것이다(이러한 절차에 있어 한 가지 예외가 있는데, 차후에 다룰 것이다).
CAS는 위에서 논의한 PasswordHandler(아니면 CAS 3.0을 사용하고 있다면
AuthenticationHandler)를 이용하여 사용자명과 비밀번호가
유효한지 판단할 것이다.
성공적으로 로그인이 이루어지면 CAS는 사용자의 브라우저를 다시 원래의
서비스로 재지정할 것이다. 또한 CAS는 ticket 매개변수도
하나 포함하고 있는데, 이 ticket 매개변수는 "서비스 티켓(service ticket)"을
나타내는 불투명(opaque) 문자열이다. 이전 예제를 계속해 보자면 브라우저가 재지정되는 URL은
https://server3.company.com/webapp/j_acegi_cas_security_check?ticket=ST-0-ER94xMJmn6pha35CQRoZ가
될 것이다.
서비스 웹 애플리케이션으로 돌아가 보면 CasProcessingFilter는 항상
/j_acegi_cas_security_check(이것은 설정 가능하며, 본 소개에서는
기본값을 사용할 것이다)에 대한 요청을 기다린다.
프로세싱 필터는 서비스 티켓을 나타내는
UsernamePasswordAuthenticationToken을 만들어낼 것이다.
신원정보가 서비스 티켓의 불투명한 값이 될지라도 인증주체는
CasProcessingFilter.CAS_STATEFUL_IDENTIFIER와 동일할 것이다.
그리고 나서 인증 요청은 설정된 AuthenticationManager에 전달될 것이다.
AuthenticationManager 구현체는
ProviderManager가 될 것이며,
CasAuthenticationProvider를 이용하여 설정된다.
CasAuthenticationProvider는 오직 CAS에 특화되어 있는
인증주체(CasProcessingFilter.CAS_STATEFUL_IDENTIFIER)와
CasAuthenticationToken(차후 설명)을 포함하고 있는
UsernamePasswordAuthenticationToken에 대해서만 응답한다.
CasAuthenticationProvider는
TicketValidator 구현체를 이용하는 서비스 티켓의
유효성을 검사할 것이다. Acegi Security에는 CasProxyTicketValidator라는
구현체가 하나 포함되어 있다. 이 티켓 유효성 검증 클래스는 CAS 클라이언트 라이브러리에
포함되어 있다. CasProxyTicketValidator는 서비스 티켓의 유효성을
검증하기 위해 CAS 서버로 HTTPS 요청을 한다. 또한 CasProxyTicketValidator는
프록시 콜백 URL을 포함할 수도 있는데, 이 URL은 예제에 포함되어 있으며 다음과 같다:
https://my.company.com/cas/proxyValidate?service=https%3A%2F%2Fserver3.company.com%2Fwebapp%2Fj_acegi_cas_security_check&ticket=ST-0-ER94xMJmn6pha35CQRoZ&pgtUrl=https://server3.company.com/webapp/casProxy/receptor.
CAS 서버로 되돌아 오면 프록시 유효성 검증 요청을 받게 될 것이다. 만약 제시된 서비스 티켓이 티켓이 전달한 서비스 URL과 일치할 경우 CAS는 사용자명을 나타내는 XML 형태로 긍정적인 응답을 제공할 것이다. 만약 인증에 관련된 프록시가 존재할 경우(아래에서 논의할 것이다), 프록시 목록 역시 XML 응답에 포함될 것이다.
[선택사항] 만약 CAS 유효성 검증 서비스에 대한 요청이 프록시
콜백 URL(pgtUrl 매개변수를 통해)을 포함할 경우,
CAS는 pgtIou 문자열을 XML 응답에 포함할 것이다.
이 pgtIou는 프록시가 허가한 티켓 IOU를 나타낸다. 그리고 나서
CAS 서버는 자체적인 pgtUrl로의 HTTPS 연결을 생성할 것이다.
이는 CAS 서버를 상호 인증하고 서비스 URL을 요청하기 위한 것이다.
프록시가 허가한 티켓을 원래의 웹 애플리케이션으로 전달하는 데에는
HTTPS 연결이 사용될 것이다.
예를 들면,
https://server3.company.com/webapp/casProxy/receptor?pgtIou=PGTIOU-0-R0zlgrl4pdAQwBvJWO3vnNpevwqStbSGcq3vKB2SqSFFRnjPHt&pgtId=PGT-1-si9YkkHLrtACBo64rmsi3v2nf7cpCResXg5MpESZFArbaZiOKH와
같은 것이다. 필요한 경우 CAS의 ProxyTicketReceptor 서블릿을 이용하여
이러한 프록시가 허가하는 티켓을 받을 것을 권장한다.
CasProxyTicketValidator는 CAS 서버로부터 전달받은 XML을 파싱할 것이다.
CasProxyTicketValidator는 CasAuthenticationProvider에 TicketResponse를 돌려줄 것이며,
TicketResponse에는 사용자명(필수)과 프록시 목록(프록시가 관련되어 있을 경우),
그리고 프록시가 허가하는 티켓 IOU(프록시 콜백이 요청되었을 경우)가 포함되어 있다.
다음으로 CasAuthenticationProvider는 설정되어 있는
CasProxyDecider를 호출할 것이다.
CasProxyDecider는 TicketResponse안에 들어 있는
프록시 목록이 서비스에 수락될 수 있는지 여부를 나타낸다.
Acegi Security System에서 제공하는 구현체에는 다음과 같은 것들이 있다:
RejectProxyTickets, AcceptAnyCasProxy,
NamedCasProxyDecider.
이러한 이름들은 신뢰받은 프록시의 List를 제공하도록 해주는
NamedCasProxyDecider를 제외하고는 대체로
자기 서술적(self-explanatory)이다.
다음으로 CasAuthenticationProvider는
CasAuthoritiesPopulator가
TicketResponse내에 포함되어 있는 사용자에게
해당되는 GrantedAuthority 객체를 어드바이스하도록 요청할 것이다.
Acegi Security는 단순히 UserDetailsService 인프라스트럭처를
이용하여 UserDetails 및 그것들과 연결된
GrantedAuthority들을 탐색하는
DaoCasAuthoritiesPopulator를 포함하고 있다.
CAS 서버에 인증 결정에 대한 책임이 있으므로
UserDetailsService로부터 반환되는
UserDetails의 비밀번호와 활성/비활성 상태는 무시된다는 것에 주의한다.
DaoCasAuthoritiesPopulator는 오직
GrantedAuthority를 획득하는 것에만 관심이 있다.
지금까지 아무런 문제가 없었다면 CasAuthenticationProvider는
세부사항들을 포함하고 있는 TicketResponse와
GrantedAuthority을 포함하는
CasAuthenticationToken을 만들어낼 것이다.
CasAuthenticationToken은 키의 해시값을 포함하고 있으므로
CasAuthenticationProvider가 그것을 생성했다는 것을 알고 있을 것이다.
다음으로 제어는 CasProcessingFilter로 넘어가며,
CasProcessingFilter는 생성된 CasAuthenticationToken을
HttpSessionIntegrationFilter.ACEGI_SECURITY_AUTHENTICATION_KEY라는
이름을 가진 HttpSession 속성에 위치시킨다.
사용자의 브라우저는 AuthenticationException을 일으킨
원래의 페이지로 재지정된다.
이제 Authentication 객체는 잘 알려진 위치에 놓여져 있으므로,
Authentication 객체는 다른 인증 접근방법에 의해 처리될 수 있을 것이다.
일반적으로 각 요청을 지속시키기 위해서는
HttpSessionIntegrationFilter를 사용하여
Authentication 객체를
SecurityContextHolder에 연결한다.
여러분이 여기까지 왔다는 것은 정말 대단한 일이다! 복잡하게 보이지만, 여러분은 Acegi Security 클래스들이 그러한 복잡성을 상당수 감추기 때문에 휴식을 취할 수도 있을 것이다. 이제 이것들을 어떻게 설정하는지 살펴보기로 하자.
Acegi Security는 심지어 CAS 2.0 버전이나 3.0 서버가 이용하는 백엔드로서 작동할 수도 있다. 설정 접근법은 아래에 설명되어 있다. 물론 여러분이 기존 CAS 환경을 갖추고 있다면 설정하는 대신 단지 어떻게 사용하는지에만 관심이 있을 것이다.
위에서 언급했던 것처럼 Acegi Security에는 기존
AuthenticationManager과 CAS 2.0을 이어주는
PasswordHandler를 포함하고 있다. 여러분은 클라이언트측에서
Acegi Security를 사용하기 위해 PasswordHandler를 사용해야 할
필요는 없다(어떠한 CAS PasswordHandler도 작동할 것이다).
설치하려면 CAS 서버 압축파일을 다운로드하여 푼다.
우리는 2.0.12 버전을 사용하였다. 배치 디렉터리의 루트에는
/web 디렉터리가 있을 것이다.
CasPasswordHandler 뿐만 아니라
AuthenticationManager를 포함하고 있는
applicationContext.xml를 복사하여
/web/WEB-INF 디렉터리에 집어 넣는다.
예제 applicationContext.xml에는 아래와 같은 내용이
포함되어 있을 것이다:
<bean id="inMemoryDaoImpl" class="org.acegisecurity.userdetails.memory.InMemoryDaoImpl">
<property name="userMap">
<value>
marissa=koala,ROLES_IGNORED_BY_CAS
dianne=emu,ROLES_IGNORED_BY_CAS
scott=wombat,ROLES_IGNORED_BY_CAS
peter=opal,disabled,ROLES_IGNORED_BY_CAS
</value>
</property>
</bean>
<bean id="daoAuthenticationProvider" class="org.acegisecurity.providers.dao.DaoAuthenticationProvider">
<property name="userDetailsService"><ref bean="inMemoryDaoImpl"/></property>
</bean>
<bean id="authenticationManager" class="org.acegisecurity.providers.ProviderManager">
<property name="providers">
<list>
<ref bean="daoAuthenticationProvider"/>
</list>
</property>
</bean>
<bean id="casPasswordHandler" class="org.acegisecurity.adapters.cas.CasPasswordHandler">
<property name="authenticationManager"><ref bean="authenticationManager"/></property>
</bean>
주의할 점은 허가된 권한들이 CAS서버에 의해 무시된다는 것인데, 이는 허가된 권한들이 애플리케이션를 호출하는 허가된 권한들과 소통할 수단이 없기 때문이다. CAS는 오직 사용자명과 비밀번호에만 관심을 가진다(활성/비활성 상태 포함).
다음으로 여러분은 기존 /web/WEB-INF/web.xml 파일을
편집해야 할 것이다. 아래의 라인들을 추가한다(아니면
authHandler 프로퍼티를 수정한다):
<context-param>
<param-name>edu.yale.its.tp.cas.authHandler</param-name>
<param-value>org.acegisecurity.adapters.cas.CasPasswordHandlerProxy</param-value>
</context-param>
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>/WEB-INF/applicationContext.xml</param-value>
</context-param>
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
spring.jar와 acegi-security.jar 파일을
/web/WEB-INF/lib에 복사한다. 이제 디렉터리 계층 구조상의 루트에 위치해 있는
build.xml파일에 포함된 ant dist 태스크를 이용한다.
이렇게 하면 /lib/cas.war 파일이 생성되며, 이제 여러분의 서블릿 컨테이너에
배치할 준비가 된 셈이다.
CAS는 HTTPS에 깊게 의존하고 있다는 것을 명심하도록 한다. 심지어 여러분은
HTTPS 인증서 없이는 시스템을 테스트할 수도 없을 것이다. 여러분은 웹 컨테이너의
HTTPS 설정에 관한 문서를 참조해야 하며, 추가적인 도움이나 인증서에 대한 테스트가
필요할 경우에는 samples/contacts/etc/ssl 디렉터리를
확인해 보도록 한다.
위에서 언급했던 것과 같이 Acegi Security에는 여러분의 기존
AuthenticationManager와 CAS 3.0을 이어주는
AuthenticationHandler를 포함하고 있다. 여러분은 Acegi Security를
클라이언트 측에서 사용하기 위해 AuthenticationHandler를 이용할 필요는
없다(어떠한 CAS AuthenticationHandler로도 충분할 것이다).
설치하려면 여러분은 CAS 서버 압축파일을 다운로드하여 압축을 푼다.
우리는 3.0.4를 사용하였다. 배포 위치 루트에는 /webapp 디렉터리가
있을 것이다. deployerConfigContext.xml을 편집하여
CasAuthenticationHandler와
AuthenticationManager가 포함하도록 한다. 간단한
applicationContext.xml 예가 아래에 나타나 있다:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN//EN" "http://www.springframework.org/dtd/spring-beans.dtd">
<beans>
<bean
id="authenticationManager"
class="org.jasig.cas.authentication.AuthenticationManagerImpl">
<property name="credentialsToPrincipalResolvers">
<list>
<bean class="org.jasig.cas.authentication.principal.UsernamePasswordCredentialsToPrincipalResolver" />
<bean class="org.jasig.cas.authentication.principal.HttpBasedServiceCredentialsToPrincipalResolver" />
</list>
</property>
<property name="authenticationHandlers">
<list>
<bean class="org.jasig.cas.authentication.handler.support.HttpBasedServiceCredentialsAuthenticationHandler" />
<bean class="org.acegisecurity.adapters.cas3.CasAuthenticationHandler">
<property name="authenticationManager" ref="acegiAuthenticationManager" />
</bean>
</list>
</property>
</bean>
<bean id="inMemoryDaoImpl" class="org.acegisecurity.userdetails.memory.InMemoryDaoImpl">
<property name="userMap">
<value>
marissa=koala,ROLES_IGNORED_BY_CAS
dianne=emu,ROLES_IGNORED_BY_CAS
scott=wombat,ROLES_IGNORED_BY_CAS
peter=opal,disabled,ROLES_IGNORED_BY_CAS
</value>
</property>
</bean>
<bean id="daoAuthenticationProvider" class="org.acegisecurity.providers.dao.DaoAuthenticationProvider">
<property name="userDetailsService"><ref bean="inMemoryDaoImpl"/></property>
</bean>
<bean id="acegiAuthenticationManager" class="org.acegisecurity.providers.ProviderManager">
<property name="providers">
<list>
<ref bean="daoAuthenticationProvider"/>
</list>
</property>
</bean>
</beans>
주의할 점은 허가된 권한들이 CAS에 의해 무시된다는 것인데, 이는 허가된 권한이 애플리케이션을 요청하는 허가된 권한과 소통할 수단이 없기 때문이다. CAS는 오직 사용자명과 비밀번호(활성/비활성 상태도 포함)에만 관심이 있다.
acegi-security.jar와
acegi-security-cas.jar 파일을
/localPlugins/lib으로 복사한다.
이제 /localPlugins/lib 디렉터리의
build.xml에 들어있는
ant war 태스크를 실행한다.
태스크를 실행하면 /localPlugins/target/cas.war가
생성될 것이며, 이제 여러분의 서블릿 컨테이너에 배포할 준비가 된 셈이다.
http://www.ja-sig.org/products/cas/server/ssl/index.html
CAS는 HTTPS에 깊게 의존하고 있다는 것을 명심하도록 한다. 심지어 여러분은
HTTP 인증서 없이는 시스템을 테스할 수 조차도 없을 것이다. 여러분은 웹 컨테이너의
HTTPS 설정에 관한 문서를 참조해야 할 것이며, 추가적인 도움이나 인증서에 대한
테스트가 필요할 경우에는 아래의 SSL을 설정하는 것에 관한 CAS 문서를 확인해 보는 것도 도움될 것이다:
http://www.ja-sig.org/products/cas/server/ssl/index.html
CAS의 웹 애플리케이션 측면은 Acegi Security로 인해 용이해진다. 이미 여러분이 Acegi Security의 기초적인 내용에 대해서는 알고 있다고 가정하고 있으므로 아래에서는 그러한 내용들에 대해 다루지 않을 것이며 CAS에 특화되어 있는 빈들만 언급할 것이다.
여러분은 ServiceProperties 빈을
애플리케이션 컨텍스트에 추가해야 할 필요가 있으며
이 빈은 서비스를 나타낸다:
<bean id="serviceProperties" class="org.acegisecurity.ui.cas.ServiceProperties">
<property name="service"><value>https://localhost:8443/contacts-cas/j_acegi_cas_security_check</value></property>
<property name="sendRenew"><value>false</value></property>
</bean>
service는 CasProcessingFilter에 의해
모니터링될 URL과 동일해야 한다. sendRenew는 기본적으로
false이나 여러분의 애플리케이션이 특별히 민감할 경우에는
true로 지정되어 있어야 한다. 이 매개변수가 하는 일은
CAS 로그인 서비스에서 싱글 사인 온을 받아들일 수 없음을 알려주는 것이다.
그렇게 하지 않으면 사용자가 사용자명과 비밀번호를 재입력하여 서비스에
접근해야 할 것이다.
다음의 빈들은 CAS 인증 절차를 수행하도록 설정되어야 한다:
<bean id="casProcessingFilter" class="org.acegisecurity.ui.cas.CasProcessingFilter">
<property name="authenticationManager"><ref bean="authenticationManager"/></property>
<property name="authenticationFailureUrl"><value>/casfailed.jsp</value></property>
<property name="defaultTargetUrl"><value>/</value></property>
<property name="filterProcessesUrl"><value>/j_acegi_cas_security_check</value></property>
</bean>
<bean id="exceptionTranslationFilter" class="org.acegisecurity.ui.ExceptionTranslationFilter">
<property name="authenticationEntryPoint"><ref local="casProcessingFilterEntryPoint"/></property>
</bean>
<bean id="casProcessingFilterEntryPoint" class="org.acegisecurity.ui.cas.CasProcessingFilterEntryPoint">
<property name="loginUrl"><value>https://localhost:8443/cas/login</value></property>
<property name="serviceProperties"><ref bean="serviceProperties"/></property>
</bean>
뿐만 아니라 여러분은 web.xml에 CasProcessingFilter를
추가해야할 필요도 있을 것이다:
<filter>
<filter-name>Acegi CAS Processing Filter</filter-name>
<filter-class>org.acegisecurity.util.FilterToBeanProxy</filter-class>
<init-param>
<param-name>targetClass</param-name>
<param-value>org.acegisecurity.ui.cas.CasProcessingFilter</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>Acegi CAS Processing Filter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
CasProcessingFilter는
AuthenticationProcessingFilter(폼 기반 인증에 사용했던)와
매우 유사한 프로퍼티를 가지며 각 프로퍼티들은 자기 설명적이다(self-explanatory).
CAS가 작동하려면 ExceptionTranslationFilter가
자신의 authenticationEntryPoint
프로퍼티가 CasProcessingFilterEntryPoint
빈으로 지정되어 있어야만 한다.
CasProcessingFilterEntryPoint는
ServiceProperties 빈(위에서 논의했던)을 참조해야만 하며,
ServiceProperties는 기업의 CAS 로그인 서버에 대한 URL을 제공한다.
이 URL이 바로 사용자의 브라우저가 재지정될 곳이다.
다음으로 여러분은 CasAuthenticationProvider와
그것의 협력자를 사용하는
AuthenticationManager를 추가할 필요가 있다:
<bean id="authenticationManager" class="org.acegisecurity.providers.ProviderManager">
<property name="providers">
<list>
<ref bean="casAuthenticationProvider"/>
</list>
</property>
</bean>
<bean id="casAuthenticationProvider" class="org.acegisecurity.providers.cas.CasAuthenticationProvider">
<property name="casAuthoritiesPopulator"><ref bean="casAuthoritiesPopulator"/></property>
<property name="casProxyDecider"><ref bean="casProxyDecider"/></property>
<property name="ticketValidator"><ref bean="casProxyTicketValidator"/></property>
<property name="statelessTicketCache"><ref bean="statelessTicketCache"/></property>
<property name="key"><value>my_password_for_this_auth_provider_only</value></property>
</bean>
<bean id="casProxyTicketValidator" class="org.acegisecurity.providers.cas.ticketvalidator.CasProxyTicketValidator">
<property name="casValidate"><value>https://localhost:8443/cas/proxyValidate</value></property>
<property name="proxyCallbackUrl"><value>https://localhost:8443/contacts-cas/casProxy/receptor</value></property>
<property name="serviceProperties"><ref bean="serviceProperties"/></property>
<!-- <property name="trustStore"><value>/some/path/to/your/lib/security/cacerts</value></property> -->
</bean>
<bean id="cacheManager" class="org.springframework.cache.ehcache.EhCacheManagerFactoryBean">
<property name="configLocation">
<value>classpath:/ehcache-failsafe.xml</value>
</property>
</bean>
<bean id="ticketCacheBackend" class="org.springframework.cache.ehcache.EhCacheFactoryBean">
<property name="cacheManager">
<ref local="cacheManager"/>
</property>
<property name="cacheName">
<value>ticketCache</value>
</property>
</bean>
<bean id="statelessTicketCache" class="org.acegisecurity.providers.cas.cache.EhCacheBasedTicketCache">
<property name="cache"><ref local="ticketCacheBackend"/></property>
</bean>
<bean id="casAuthoritiesPopulator" class="org.acegisecurity.providers.cas.populator.DaoCasAuthoritiesPopulator">
<property name="userDetailsService"><ref bean="inMemoryDaoImpl"/></property>
</bean>
<bean id="casProxyDecider" class="org.acegisecurity.providers.cas.proxy.RejectProxyTickets"/>
여러분이 이전의 "CAS 작동 방법" 섹션을 참고한다면 빈들은 모두 적당히
자기 설명적이다. 주의깊은 독자라면 한 가지 의아한 점을 알아챌 수도 있는데,
바로 CasAuthenticationProvider의
statelessTicketCache 프로퍼티이다.
이는 "고급 CAS 사용방법" 섹션에서 자세히 논의할 것이다.
주의할 점은CasProxyTicketValidator가 trustStore
프로퍼티를 주석처리했다는 것이다. 이 프로퍼티는 여러분이
HTTPS 인증서 문제에 관한 경험이 있다면 도움이 될 것이다.
또한 proxyCallbackUrl은 프록시에서 허가한
티켓을 서비스에서 받을 수 있도록 설정되어 있다는 것도 알아두도록 한다.
만일 여러분이 이러한 기능을 사용한다면 프록시에서 허가한 티켓을
적절한 서블릿이 받도록 설정할 필요가 있을 것이다. 우리는 여러분이 아래의
설정을 웹 애플리케이션의 web.xml에 추가하여 CAS의
ProxyTicketReceptor를 사용할 것을 권장한다:
<servlet>
<servlet-name>casproxy</servlet-name>
<servlet-class>edu.yale.its.tp.cas.proxy.ProxyTicketReceptor</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>casproxy</servlet-name>
<url-pattern>/casProxy/*</url-pattern>
</servlet-mapping>
위 설정으로 CAS 설정이 완료된다. 만약 여러분이 아무런 실수도 하지 않았다면 여러분의 웹 애플리케이션은 CAS 싱글 사인 온 프레임워크상에서 잘 작동할 것이다. CAS가 인증을 처리하는 것에 관해 Acegi Security의 다른 부분들을 참고할 필요는 없다.
예제 애플리케이션 디렉터리에는 contacts-cas.war 파일도 포함되어 있다.
이 예제 애플리케이션은 위 설정을 이용하고 있으며, 애플리케이션을 배치하여
CAS가 작동하는 것을 볼 수 있을 것이다.
CasAuthenticationProvider는 상태를 갖는 클라이언트(stateful client)와
상태를 갖지 않는 클라이언트(stateless client)를 구별한다. 상태를 갖는 클라이언트는
CasProcessingFilter를 통해 들어오는 것으로 간주한다.
상태를 갖지 않는 클라이언트는
CasProcessingFilter.CAS_STATELESS_IDENTIFIER과 동일한 신원주체를 가진
UsernamePasswordAuthenticationToken를 통해 인증 요청을 나타내는 것이다.
상태를 갖지 않는 클라이언트는 Hessian가 Burlap과 같은 원격 프로토콜일 것이다.
이 경우에도 그대로 BasicProcessingFilter가 사용되며
원격 프로토콜 클라이언트는 위의 정적 문자열과 동일한 사용자명 및
CAS 서비스 티켓과 동일한 비밀번호를 제시할 것으로 예상한다.
클라이언트들은 CAS 서버로부터 직접 CAS 서비스 티켓을 획득해야만 한다.
원격 프로토콜은 자기 자신을 HttpSession의 컨텍스트내에서
표시하는 수단을 갖지 않기 때문에 HttpSession의
HttpSessionIntegrationFilter.ACEGI_SECURITY_AUTHENTICATION_KEY
속성에 의존하여 CasAuthenticationToken에 위치하도록 할 수 있다.
또한 CAS 서버가 TicketValidator에 의해 유효성 검증이 이루어진 다음
그러한 서비스 티켓을 무효화하므로 동일한 서비스 티켓을 연속된 요청에
제시하는 것은 효과가 없을 것이다. 비슷하게 원격 프로토콜 클라이언트에 대해 프록시에서 허가한
티켓을 획득하는 것도 쉽지 않은데, 왜냐하면 그러한 것들이 종종 CAS 서버에 접근할 수 있는
HTTPS URL이 거의 없는 클라이언트 머신에 배포되기 때문이다.
한 가지 분명한 선택사항은 원격 프로토콜 클라이언트들에 대해 CAS를 전혀 사용하지 않는 것이다. 하지만 이렇게 하면 CAS가 제공하는 대부분의 매력적인 기능들을 사용하지 못하게 된다.
중도로서 CasAuthenticationProvider는
StatelessTicketCache를 사용한다.
CasAuthenticationProvider는
CasProcessingFilter.CAS_STATELESS_IDENTIFIER과
동일한 신원주체를 표현하는 데 있어 단독적으로 사용된다.
이렇게 했을 때 일어나는 일은 CasAuthenticationProvider가
서비스 티켓에 맞춰진 StatelessTicketCache에
결과 CasAuthenticationToken이 저장된다는 것이다.
따라서 원격 프로토콜은 동일한 서비스 티켓을 제시할 수 있으며
CasAuthenticationProvider는 유효성 검증을 위해
CAS 서버에 접촉할 필요는 없을 것이다(최초 요청과는 별도로).
고급 CAS 사용에 있어 또 다른 측면은 프록시에서 허가한 티켓으로부터 프록시 티켓을
생성하는 것과 관련되어 있다. 위에서 언급했던 것과 같이 우리는
CAS의 ProxyTicketReceptor를 이용하여 이러한 티켓들을
부여받을 것을 권장한다. ProxyTicketReceptor는 여러분이
프록시가 허가하는 IOU 티켓을 제시함으로써 프록시 티켓을 획득할 수 있도록 해주는
정적 메소드를 제공한다. 여러분은
CasAuthenticationToken.getProxyGrantingTicketIou()를 호출하여
프록시에서 허가한 IOU 티켓을 획득할 수 있다.
Acegi Security 클래스들을 이용한 CAS 통합이 쉽고 유용하다는 것을 알게 되길 바란다. 엔터프라이즈 차원의 싱글 사인 온에 온 것을 환영한다!
Acegi Security의 매우 초기 버전은 최종 사용자와의 인증을 인터페이싱하는 데 있어 컨테이너 어댑터(Container Adapter)를 배타적으로 사용하였다. 이렇게 하는 것도 잘 동작하긴 했지만 다양한 버전의 컨테이너를 지원하는 데에는 상당한 시간을 필요로 하였으며 설정 자체가 개발자에게 상대적으로 시간 소모적이었다. 이러한 이유로 HTTP 폼 인증 및 HTTP Basic 인증 접근법이 개발되었으며, 그리고 오늘날 거의 모든 애플리케이션에 권장하고 있다.
컨테이너 어댑터는 최종 사용자의 애플리케이션을 호스팅하는데 사용되는
컨테이너에 Acegi Security를 직접적으로 통합할 수 있게 한다.
이러한 통합은 애플리케이션이 Acegi Security에 의해 제공되는 강화된
보안 인터셉터 기능의 이점을 누리면서(Acegi Security 역시
isUserInRole() 및 유사한 서블릿 스펙에 호환성 있는
메소드가 포함되어 있는 ContextHolderAwareRequestWrapper를
제공하고 있음을 알아둔다), 동시에 컨테이너에 내장되어 있는 인증 및
권한부여 기능(isUserInRole()과 폼 기반이나
BASIC 인증과 같은)들을 계속해서 이용할 수 있음을 의미한다.
컨테이너와 Acegi Security간의 통합은 어댑터를 통해 이루어진다. 어댑터는 컨터이너에 호환성있는 사용자 인증 제공자를 제공하며, 이러한 제공자는 컨테이너에 호환되는 사용자 객체를 반환할 필요가 있다.
어댑터는 컨테이너에 의해 인스턴스화되며 컨테이너마다 특화되어 있는 설정 파일에
정의되어 있다. 그런 다음 어댑터는 Spring의 애플리케이션 컨텍스트를 불러오는데,
여기에서는 요청에 대한 인증을 수행하는데 사용할 수 있는 인증 제공자와 같은
일반적인 인증 관리자에 대한 설정을 정의하고 있다. 일반적으로 애플리케이션
컨텍스트는 acegisecurity.xml라는 이름을 가지며
컨테이너마다 지정된 위치에 놓여진다.
현재 Acegi Security는 Jetty, Catalina(톰캣), JBoss, 및 Resin을 지원한다. 추가적인 컨테이너 어댑터는 어렵지 않게 작성할 수 있다.
항상 그래왔던 것 처럼 컨테이너 어댑터는
AbstractSecurityInterceptor에서 하는 것과 같이 요청되었을 경우
AuthenticationManager에서 인증받아야 할
Authentication 객체를 만들어 낸다.
AuthenticationManager는 어댑터에서 제공하는 특정
Authentication 객체가 유효하고 실질적으로 신뢰하는 어댑터에 의해
인증되었는지 확인해야 할 필요가 있다.
어댑터는 불변적이며 AuthByAdapter 인터페이스를 구현하는
Authentication 객체를 생성한다.
이러한 객체들은 어댑터에서 정의한 키의 해시 값을 저장한다.
어댑터는 Authentication 객체가
AuthByAdapterProvider에 의해 유효성 검증을 받을 수 있도록 한다.
AuthByAdapterProvider 인증 제공자는 다음과 같이 정의한다:
<bean id="authByAdapterProvider" class="org.acegisecurity.adapters.AuthByAdapterProvider"> <property name="key"><value>my_password</value></property> </bean>
키는 어댑터를 구동시키는 컨테이너에 특화된 설정 파일에 정의되어 있는 키와
일치해야 한다. AuthByAdapterProvider는 자동적으로
예상되는 키의 해시 값을 반환하는 어떠한 AuthByAdapter도
유효한 것으로 받아들인다.
거듭 말하지만 이렇게 하는 것은 어댑터가
DaoAuthenticationProvider와 같은 제공자를 사용하여
초기 인증을 수행할 것임을 의미하며, 키의 해시값을 포함하고 있는
AuthByAdapter 인스턴스를 반환한다.
나중에 애플리케이션에서 보안 인터셉터가 관리하는 자원을 요청할 경우
애플리케이션의 AuthByAdapterProvider가
SecurityContextHolder안에 들어있는
SecurityContext의 AuthByAdapter 인스턴스를
검사할 것이다. 애플리케이션에 특화되어 있는 애플리케이션 컨텍스트내의
DaoAuthenticationProvider와 같은 추가적인 인증 제공자에 대한 요구사항은
존재하지 않는데, 왜냐하면 애플리케이션에 의해 나타내어질
Authentication 인스턴스의 타입만이 컨테이너 어댑터로부터
오기 때문이다.
컨테이너와 컨테이너를 좀 더 세부적으로 기술하는 컨테이너 어댑터를 사용하는 데에는 클래스 로더 문제가 빈번하게 발생한다. 각각의 컨테이너는 매우 구체적으로 설정할 필요가 있으며, 설치지침은 아래에 나와 있다. 일단 설치하고 난 후 예제 애플리케이션을 실행하여 여러분의 컨테이너 어댑터가 적절히 설정되어 있는지 확인하도록 한다.
컨테이너 어댑터에서 DaoAuthenticationProvider를 사용할 경우
DaoAuthenticationProvider의
forcePrincipalAsString 프로퍼티가
true로 설정되어 있는지 확인한다.
다음은 Jetty 4.2.18에서 테스트한 내용이다.
$JETTY_HOME은 Jetty가 설치되어 있는 곳의 루트 디렉터리를 가리킨다.
$JETTY_HOME/etc/jetty.xml 파일을 편집하여
<Configure class>에 새로운 addRealm
요청이 포함되도록 한다:
<Call name="addRealm">
<Arg>
<New class="org.acegisecurity.adapters.jetty.JettyAcegiUserRealm">
<Arg>Spring Powered Realm</Arg>
<Arg>my_password</Arg>
<Arg>etc/acegisecurity.xml</Arg>
</New>
</Arg>
</Call>
acegisecurity.xml을 $JETTY_HOME/etc로 복사한다.
아래 파일들을 $JETTY_HOME/ext로 복사한다:
aopalliance.jar
commons-logging.jar
spring.jar
acegi-security-jetty-XX.jar
commons-codec.jar
burlap.jar
hessian.jar
위 JAR 파일(혹은 acegi-security-XX.jar)들 중 어떤 것도
애플리케이션의 WEB-INF/lib에 들어 있어서는 안된다.
web.xml 내에 들어있는 영역 이름(realm name)이 가리키는
내용이 Jetty에서는 중요하며, web.xml에서는 jetty.xml
(위 예제의 "Spring Powered Realm")와 동일한 <realm-name>을
표현해야 한다.
다음은 JBoss 3.2.6에서 테스트한 내용이다.
$JBOSS_HOME은 JBoss가 설치되어 있는 곳의 루트를 나타낸다.
Jboss 통합 클래스에서 사용할 수 있는 Spring 컨텍스트를 만드는 데에는 두 가지 방법이 있다.
첫 번째 접근법은
$JBOSS_HOME/server/your_config/conf/login-config.xml 파일을
편집하여 <Policy>하의 새로운 입력사항들을
포함하게 하는 것이다:
<application-policy name = "SpringPoweredRealm">
<authentication>
<login-module code = "org.acegisecurity.adapters.jboss.JbossAcegiLoginModule"
flag = "required">
<module-option name = "appContextLocation">acegisecurity.xml</module-option>
<module-option name = "key">my_password</module-option>
</login-module>
</authentication>
</application-policy>
acegisecurity.xml을
$JBOSS_HOME/server/your_config/conf로 복사한다.
이 설정에서는 acegisecurity.xml에 모든 인증 관리자 빈을
포함하는 Spring 컨텍스트 정의가 포함되어 있다. 여러분은
각각의 로그인 요청에 대해 SecurityContext을 생성하고 파괴하므로
로그인 작업에 비용이 많이 들 수 있다는 것을 명심해야 한다.
이것의 대안으로 두 번째 접근법은
org.springframework.beans.factory.access.SingletonBeanFactoryLocator.
을 통해 Spring의 싱글톤 기능을 이용하는 것이다.
이러한 접근법에 필요한 설정은 다음과 같다:
<application-policy name = "SpringPoweredRealm">
<authentication>
<login-module code = "org.acegisecurity.adapters.jboss.JbossAcegiLoginModule"
flag = "required">
<module-option name = "singletonId">springRealm</module-option>
<module-option name = "key">my_password</module-option>
<module-option name = "authenticationManager">authenticationManager</module-option>
</login-module>
</authentication>
</application-policy>
위 코드에서 authenticationManager는 여러분이
IoC 컨테이너에 몇 가지를 정의한 경우에 대비하여 예상되는
AuthenticationManager의 이름을 정의한 헬퍼 프로퍼티이다.
singletonId 프로퍼티는 beanRefFactory.xml 파일에
정의되어 있는 빈을 참조한다. 이 파일은
$JBOSS_HOME/server/your_config/conf를 포함하여
JBoss 클래스 패스 어디에서도 사용할 수 있어야 한다.
beanRefFactory.xml에는 다음의 선언들이 포함되어 있다.:
<beans>
<bean id="springRealm" singleton="true" lazy-init="true" class="org.springframework.context.support.ClassPathXmlApplicationContext">
<constructor-arg>
<list>
<value>acegisecurity.xml</value>
</list>
</constructor-arg>
</bean>
</beans>
마지막으로 설정 접근법과는 관계없이 여러분은 아래의 파일들을
$JBOSS_HOME/server/your_config/lib에 복사하도록 한다:
aopalliance.jar
spring.jar
acegi-security-jboss-XX.jar
commons-codec.jar
burlap.jar
hessian.jar
위 JAR 파일들(혹은 acegi-security-XX.jar) 중 어떤 것도
여러분 애플리케이션의 WEB-INF/lib에 들어 있어서는 안된다.
web.xml에 들어있는 영역 이름(realm name)이 가리키는
내용은 JBoss에서 문제가 되지 않는다. 하지만 웹 애플리케이션의
WEB-INF/jboss-web.xml은
여러분의 login-config.xml와 동일한
<security-domain>을 표현해야 한다.
예를 들어 위 예제와 맞춰보자면 jboss-web.xml는
다음과 같아야 할 것이다:
<jboss-web> <security-domain>java:/jaas/SpringPoweredRealm</security-domain> </jboss-web>
JBoss는 널리 사용되는 컨테이너 어댑터(대부분 레거시 EJB를 지원할 필요성에 기인한다)이므로, 사용상 어려움이 있다면 알려주길 바란다.
다음은 Resin 3.0.6에서 테스트한 내용이다.
$RESIN_HOME은 Resin이 설치된 곳의 루트를 나타낸다.
Resin은 컨테이너 어댑터를 지원하는 데 있어 몇 가지 방법을 제공한다. 아래의 설정 지침은 다른 컨테이너 어댑터 설정과의 일관성을 최대화하기 위해 산출한 내용이다. 이렇게 하면 Resin 사용자가 간단하게 예제 애플리케이션을 배치하고 적절한 설정을 지정할 수 있을 것이다. Resin에 익숙한 개발자들은 자연스럽게 Resin의 기능을 이용하여 웹 애플리케이션 자체를 JAR로 패키지화하고, 그리고/혹은 싱글 사인 온을 지원할 수 있게 될 것이다.
다음 파일들을 $RESIN_HOME/lib로 복사한다:
aopalliance.jar
commons-logging.jar
spring.jar
acegi-security-resin-XX.jar
commons-codec.jar
burlap.jar
hessian.jar
다른 컨테이너 어댑터에서 사용하고 있는 컨테이너 수준의
acegisecurity.xml 파일과는 달리, 각각의 Resin 웹 애플리케이션은
전용 WEB-INF/resin-acegisecurity.xml 파일을 포함하고 있다.
게다가 각각의 웹 애플리케이션마다 Resin이 컨테이너 어댑터를 시작하는데 사용하는
resin-web.xml 파일을 포함할 것이다:
<web-app>
<authenticator>
<type>org.acegisecurity.adapters.resin.ResinAcegiAuthenticator</type>
<init>
<app-context-location>WEB-INF/resin-acegisecurity.xml</app-context-location>
<key>my_password</key>
</init>
</authenticator>
</web-app>
기본적인 설정은 위에서 제시되어 있으며, 나열된 JAR
파일들(혹은 acegi-security-XX.jar) 중 어떤 것도
여러분 애플리케이션의 WEB-INF/lib에 들어 있어서는 안된다.
web.xml에 들어있는 영역 이름(realm name)이 가리키는 내용은
Resin에서는 문제가 되지 않는데, 해당 인증 클래스가
<authenticator> 설정에 의해 표현되기 때문이다.
다음은 Jakarta Tomcat 4.1.30과 5.0.19로 테스트한 내용이다.
$CATALINA_HOME은 Catalina (Tomcat)이 설치되어 있는 곳의
루트를 나타낸다.
$CATALINA_HOME/conf/server.xml 파일을 편집하여
<Engine> 섹션에는 오직 하나의 활성화된
<Realm> 입력사항만이 포함되도록 한다.
예제 realm 입력 내용은 다음과 같다:
<Realm className="org.acegisecurity.adapters.catalina.CatalinaAcegiUserRealm"
appContextLocation="conf/acegisecurity.xml"
key="my_password" /><Engine> 섹션에서
다른 <Realm>를 모두 제거하도록 한다.
$CATALINA_HOME/conf로
acegisecurity.xml을 복사한다.
$CATALINA_HOME/server/lib로
acegi-security-catalina-XX.jar을
복사한다.
다음 파일들을
$CATALINA_HOME/common/lib로 복사한다:
aopalliance.jar
spring.jar
commons-codec.jar
burlap.jar
hessian.jar
위 JAR 파일들(혹은 acegi-security-XX.jar) 중 어떤것도
여러분 애플리케이션의 WEB-INF/lib에 들어 있어서는 안된다.
web.xml에 들어있는 영역 이름(realm name)이 가리키는 내용은
Catalina에서 문제가 되지 않는다.
컨테이너 어댑터를 Mac OS X에서 사용할 때 발생하는 리포트를 받은 적이 있다. 해결 방법은 아래와 같은 스크립트를 사용하는 것이다:
#!/bin/sh export CATALINA_HOME="/Library/Tomcat" export JAVA_HOME="/Library/Java/Home" cd / $CATALINA_HOME/bin/startup.sh
마지막으로 Tomcat을 재시작한다.
Acegi Security의 고급 권한 부여 기능은 그것의 대중성으로 인해 가장 주목하지 않을 수 없는 것 중의 하나이다. 여러분이 어떻게 인증 방식 - Acegi Security에서 제공하는 메커니즘과 제공자, 혹은 컨테이너나 다른 Acegi Security가 아닌 인증 기관 - 을 선택했느냐와는 관계없이 여러분은 권한 부여 서비스를 여러분의 애플리케이션에서 일관성있고 단순한 방식으로 사용할 수 있다는 것을 알게 될 것이다.
여기에서는 1부에서 소개했던 여러 가지 AbstractSecurityInterceptor
구현체들을 살펴볼 것이다. 그리고 나서 도메인 접근 제어 목록을 이용하여
권한 부여에 관한 세부조정 방법을 알아 보는 것으로 넘어갈 것이다.
인증 섹션에서 간략하게 언급했듯이 모든 Authentication 구현체들은
GrantedAuthority 객체의 배열을 저장할 필요가 있다.
이러한 GrantedAuthority 객체의 배열은 인증 주체에
허용된 권한들을 나타낸다. GrantedAuthority 객체는
AuthenticationManager에 의해
Authentication 객체에 삽입되며 차후에 권한부여에 관한
의사결정을 할 경우 AccessDecisionManager에 의해 읽어들여진다.
GrantedAuthority는 다음의 메소드만을 포함하고 있는 인터페이스이다:
public String getAuthority();
이 메소드는 AccessDecisionManager들이
GrantedAuthority에 대한
정확한 String 표현을 획득하도록 한다.
String 표현으로 반환하므로
GrantedAuthority는 대부분의
AccessDecisionManager들에 의해 손쉽게 "읽혀 들여"질 수 있다.
GrantedAuthority가 정확히 String로
표현되지 못할 경우 GrantedAuthority는 "복합적(complex)"인 것으로
간주될 것이며 getAuthority()메소드는 null을
반환해야 한다.
복합적인 GrantedAuthority의 예는 서로 다른
고객 계좌 번호에 적용되는 오퍼레이션의 목록과 보안 진입점들을
저장하는 구현체를 들 수 있다. 이러한 복합적인
GrantedAuthority를 String으로
표현하는 것은 상당히 복잡할 것이며, 그리고 그 결과
getAuthority() 메소드는
null을 반환해야만 한다.
이는 모든 AccessDecisionManager가
GrantedAuthority의 내용을 이해하기 위해
GrantedAuthority 구현체를 상세하게 지원할 필요가
있음을 나타낸다.
Acegi Security에는 GrantedAuthorityImpl이라는 이름의
GrantedAuthority 구현체가 포함되어 있다.
GrantedAuthorityImpl은 모든 사용자에 특징적인
String이 GrantedAuthority로 변환될 수
있도록 해준다. 모든 AuthenticationProvider에는
Authentication 객체에 정보를 입력하는데 있어
GrantedAuthorityImpl을 이용하는 보안 아키텍처를
포함하고 있다.
AccessDecisionManager는
AbstractSecurityInterceptor에 의해 호출되며 최종적인
접근 제어 결정을 할 책임이 있다.
AccessDecisionManager 인터페이스에는 세 개의 메소드가
포함되어 있다:
public void decide(Authentication authentication, Object object, ConfigAttributeDefinition config) throws AccessDeniedException; public boolean supports(ConfigAttribute attribute); public boolean supports(Class clazz);
첫 번째 메소드에서 볼 수 있듯이, AccessDecisionManager는
권한부여 결정을 평가하는 데 유용할 모든 정보들을 메소드 매개변수를 통해 전달한다.
특히 안전한 Object를 전달하는 것은 실제 안전 객체 호출에 들어 있는
인자들이 검사될 수 있도록 한다. 예를 들어 안전 객체가
MethodInvocation이었다고 가정해 보자.
모든 Customer 인자에 대해
MethodInvocation을 조회한 다음,
AccessDecisionManager에 일종의 보안 로직을 구현하여
인증 주체가 그러한 고객에 대한 오퍼레이션을 할 수 있도록 허용되었는지를
확인하는 것이 쉬워질 것이다. 구현체는 접근이 거부되었을 경우
AccessDeniedException을 던질 것을 예상한다.
supports(ConfigAttribute) 메소드는 시작시
AbstractSecurityInterceptor에 의해 호출되어
AccessDecisionManager가 전달된
ConfigAttribute를 처리할 수 있는지를 판단한다.
supports(Class) 메소드는 보안 인터셉터 구현체에 의해
호출되어 설정된 AccessDecisionManager가
보안 인터셉터에서 전해줄 안전 객체의 타입을 지원하는지를 확인한다.
사용자가 독자적인 AccessDecisionManager를 구현하여
권한 부여상의 모든 측면을 제어할 수 있는 동시에,
Acegi Security에는 투표(voting)에 기반한 몇몇
AccessDecisionManager 구현체가 포함되어 있다.
그림 4는 관련 클래스들을 보여준다.

그림 4. 투표 결정 관리자
이러한 접근법을 이용하여 일련의 AccessDecisionVoter 구현체들은
인증 의사결정에 대하여 투표하게 된다. 그런 다음 AccessDecisionManager는
투표 결과에 따라 AccessDeniedException을 던질지를 결정한다.
AccessDecisionVoter 인터페이스에는
세 개의 메소드가 포함되어 있다:
public int vote(Authentication authentication, Object object, ConfigAttributeDefinition config); public boolean supports(ConfigAttribute attribute); public boolean supports(Class clazz);
구상 구현체는 int를 반환하며,
AccessDecisionVoter 내의 정적 필드인
ACCESS_ABSTAIN, ACCESS_DENIED 및
ACCESS_GRANTED를 나타내는 값이다.
투표 구현체는 인증 결정에 대해 아무런 의견이 없을 경우
ACCESS_ABSTAIN을 반환할 것이다.
만약 의견이 하나라도 있을 경우 AccessDecisionVoter는
ACCESS_DENIED나
ACCESS_GRANTED를 반환해야 한다.
Acegi Security에는 투표를 매기는 세 개의 구상
AccessDecisionManager가 포함되어 있다.
ConsensusBased 구현체는 기권이 아닌 투표 합의에 근거하여 접근을
허가하거나 거부할 것이다. 동일한 투표 결과가 나오거나 모든 투표결과가
기권일 경우에 행위를 제어하기 위한 프로퍼티를 제공한다.
AffirmativeBased 구현체는 하나 이상의
ACCESS_GRANTED 투표결과를 받게 될 경우
접근을 허가할 것이다(예를 들면 적어도 하나의 허가 투표가 있었다면
거부 투표는 무시될 것이다). ConsensusBased 구현체와
비슷하게 모든 투표자가 기권할 경우에 대한 행위를 제어할 매개변수가 존재한다.
UnanimousBased 제공자는 접근을 허가하기 위하여
기권은 무시하며 만장일치로 ACCESS_GRANTED 투표가
이루어질 것을 기대한다. UnanimousBased는
ACCESS_DENIED 투표가 하나라도 있다면 접근을 거부할 것이다.
다른 구현체와 유사하게 UnanimousBased 구현체 역시
모든 투표가 기권일 경우에 행위를 제어할 매개변수가 존재한다.
투표를 따로 매기는 사용자 정의 AccessDecisionManager를
구현하는 것도 가능하다. 예를 들면 특정
AccessDecisionVoter로부터의 투표는
특정 투표자가 행한 거부 투표가 거부 효과(veto effect)를
갖더라도 추가적인 가중치를 받을 수도 있을 것이다.
Acegi Security에는 두 개의 구상 AccessDecisionVoter가 제공된다.
RoleVoter 클래스는 ConfigAttribute가
ROLE_로 시작하는 것이 있을 경우 투표할 것이다.
RoleVoter는 하나 이상의 ROLE_로
시작하는 ConfigAttributes과 정확히 일치하는 문자열 연결을
반환하는(getAuthority() 메소드를 통해)
GrantedAuthority가 있을 경우
접근을 허가하도록 투표할 것이다. 만약 ROLE_로 시작하는
ConfigAttribute가 정확히 일치하는 것이 아무 것도 없을 경우
RoleVoter는 접근을 거부하도록 투표할 것이다.
ROLE_로 시작하는 ConfigAttribute가
아무 것도 없을 경우에는 투표자는 기권할 것이다.
RoleVoter는 ROLE_ 접두사를 포함하여
비교시 대소문자를 구분한다.
BasicAclEntryVoter는 Acegi Security에 포함되어 있는
또 다른 구상 투표자 클래스이다. BasicAclEntryVoter는 Acegi Security의
AclManager(차후에 설명할 것이다)에 통합된다.
BasicAclEntryVoter는 다음과 같은 동일한 애플리케이션 컨텍스트 내에서
여러 개의 인스턴스를 갖도록 설계되어 있다:
<bean id="aclContactReadVoter" class="org.acegisecurity.vote.BasicAclEntryVoter">
<property name="processConfigAttribute"><value>ACL_CONTACT_READ</value></property>
<property name="processDomainObjectClass"><value>sample.contact.Contact</value></property>
<property name="aclManager"><ref local="aclManager"/></property>
<property name="requirePermission">
<list>
<ref local="org.acegisecurity.acl.basic.SimpleAclEntry.ADMINISTRATION"/>
<ref local="org.acegisecurity.acl.basic.SimpleAclEntry.READ"/>
</list>
</property>
</bean>
<bean id="aclContactDeleteVoter" class="org.acegisecurity.vote.BasicAclEntryVoter">
<property name="processConfigAttribute"><value>ACL_CONTACT_DELETE</value></property>
<property name="processDomainObjectClass"><value>sample.contact.Contact</value></property>
<property name="aclManager"><ref local="aclManager"/></property>
<property name="requirePermission">
<list>
<ref local="org.acegisecurity.acl.basic.SimpleAclEntry.ADMINISTRATION"/>
<ref local="org.acegisecurity.acl.basic.SimpleAclEntry.DELETE"/>
</list>
</property>
</bean> 위 예제에서 여러분은 MethodSecurityInterceptor나
AspectJSecurityInterceptor에 대하여
ACL_CONTACT_READ나 ACL_CONTACT_DELETE를 정의하였다.
그러한 메소드가 호출될 때 위에서 정의해 놓은 적용 가능한 투표자는 접근을 허가하거나
거부하도록 투표할 것이다. 투표자는 메소드 호출을 살펴보고
sample.contact.Contact 타입의 첫번째 인자에 위치한 다음
그 Contact를 AclManager로 전달한다.
그런 다음 AclManager는 현재 Authentication에 해당되는
접근 제어 목록(access control list)을 반환할 것이다. 그러한 ACL에 나열된
requirePermission 중의 하나가 포함되어 있다고 가정하면
투표자는 접근 허가에 대하여 투표할 것이다. ACL이 투표자에서 정의되어 있는
접근권한 중 하나를 포함하고 있지 않을 경우 투표자는 접근 거부에 투표할 것이다.
BasicAclEntryVoter는 여러분이 전적으로 애플리케이션 컨텍스트에
정의되어 있는 도메인 객체 보안이 포함된 정말로 복잡한 애플리케이션을 구축하도록 해주므로
중요한 클래스이다. 여러분이 Acegi Security의 ACL 기능에 관해 좀 더 배우고 그것을 어떻게
적용하는지에 관심이 있다면 본 참조 가이드의 ACL 및 "호출후 처리(After Invocation)" 섹션과
연락처 예제 애플리케이션을 참고하도록 한다.
사용자 정의 AccessDecisionVoter를 구현하는 것 역시 가능하다.
ContactSecurityVoter 및 DenyVoter를 포함하여
몇 가지 예제가 Acegi Security의 단위 테스트에 제공되어 있다.
ContactSecurityVoter는
CONTACT_OWNED_BY_CURRENT_USER ConfigAttribute가
발견되지 않을 경우 결정 투표로부터 기권한다. 투표할 경우
ContactSecurityVoter는 MethodInvocation을 조회하여
메소드 호출에 제시된 Contact 객체의 소유자를 추출해 낸다.
ContactSecurityVoter는 Contact의 소유자명이 나타나 있는
Authentication 객체내의 인증주체와 일치할 경우 접근 허가에 투표한다.
ContactSecurityVoter는 Contact의 소유자와
Authentication 객체가 제시하는 몇몇
GrantedAuthority와 손쉽게 비교할 수 있다. 이러한 모든 것들은 상대적으로
몇 줄의 코드만으로도 가능하며 인증 모델의 유연함을 보여준다.
해야할 일: 기존 ACL 패키지가 비추천되고(deprecated), 그리고 새로운 ACL 구현체를 기술하고 있는 챕터에 제한되어 있는 대체 패키지에 대한 모든 참조를 가질 경우 이전 ACL 패키지에 대한 참조를 제거하도록 한다.
AccessDecisionManager가 보안 객체에 대한 호출을 처리하기 전에
AbstractSecurityInterceptor에 의해 호출되는 반면 몇몇 애플리케이션들은
실질적으로 보안 객체를 호출하여 반환되는 객체를 변경할 수단을 필요로 한다.
여러분은 이를 달성하기 위해 여러분만의 AOP 관심사(concern)를 손쉽게
구현할 수도 있겠지만 Acegi Security에서 Acegi Security의 ACL 기능에
통합되는 몇 가지 구상 구현체를 포함하고 있는 편리한 훅을 제공하고 있다.
그림 5는 Acegi Security의 AfterInvocationManager 및
구상 구현체를 보여준다.

그림 5: 호출후 처리(After Invocation) 구현체
Acegi Security의 다른 부분들에서 처럼 AfterInvocationManager는 하나의
구상 구현체인 AfterInvocationProviderManager를 제공하는데,
이것은 AfterInvocationProvider의 목록을 폴링한다.
각각의 AfterInvocationProvider는 반환 객체를 변경하거나
AccessDeniedException을 던질 수 있다.
사실 여러 개의 제공자들이 객체를 변경할 수 있으며,
이는 이전 제공자가 목록의 다음에 전달된 결과이다.
이제부터 AfterInvocationProvider의
ACL-aware 구현을 살펴보기로 하자.
만약 여러분이 AfterInvocationManager를 사용하고 있는 경우라도
여전히 여러분은 MethodSecurityInterceptor의
AccessDecisionManager가
작동할 수 있도록 해주는 설정 속성이 필요할 것이다.
여러분이 AccessDecisionManager 구현체를 포함하고 있는
일반 Acegi Security를 사용하고 있을 경우, 특정 보안 메소드 호출에 대한
설정 속성이 정의되어 있지 않으면 각각의 AccessDecisionVoter가
투표를 기권하게 될 것이다. 반대로 AccessDecisionManager의
"allowIfAllAbstainDecisions" 프로퍼티가 false이면
AccessDeniedException이 던져질 것이다. 여러분은 이러한 잠재적인 문제를
(i) "allowIfAllAbstainDecisions" 프로퍼티를 true로
지정하거나(일반적으로 권장하지는 않지만) (ii) 단순히 적어도
AccessDecisionVoter가 접근 허가에 투표할 하나의 설정 속성이 있다고 가정하여
방지할 수도 있다. 두 번째(권장함) 접근법은 일반적으로 ROLE_USER나
ROLE_AUTHENTICATED 설정 속성을 통해 이루어질 수 있다.