6장. 공통적인 인증 서비스

6.1. 메커니즘, 제공자(Providers), 진입점(Entry Points)

여러분이 Acegi Security에서 제공되는 인증 접근방법을 이용하고 있다면, 일반적으로 AuthenticationProviderAuthenticationEntryPoint와 더불어 웹 필터를 설정해야 할 필요가 있을 것이다. 이 섹션에서는 폼 기반 인증(예를 들어 멋진 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 등과 같은 이름으로 불려질 것이다. 필터들이 나타나야 하는 순서는 필터 섹션에서 논의할 것이며, 위 예제에 있는 것들은 올바른 순서를 따르고 있다.

예제에서는 AuthenticationProcessingFilterBasicProcessingFilter가 사용될 것이다. 이러한 필터들은 각각 폼 기반의 인증과 BASIC HTTP 헤더 기반의 인증에 대응되는 "인증 메커니즘"이다(인증 메커니즘의 역할에 대해서는 참조 가이드의 앞 부분에서 알아보았다). 여러분이 폼 기반의 인증이나 BASIC 인증을 사용하고 있지 않았다면 이러한 빈들 또한 정의되어 있지 않을 것이다. 대신 DigestProcessingFilterCasProcessingFilter와 같은 여러분이 원하는 인증 환경에 필터 애플리케이션을 정의해야 할 것이다. 각각의 그러한 인증 메커니즘들을 설정하는 방법에 관해서는 참조 가이드의 각 메커니즘들에 관한 개별 챕터들을 참고하도록 한다.

HttpSessionContextIntegrationFilter가 HTTP 세션내의 각 호출 사이에서 SecurityContext의 내용을 유지한다는 점을 떠올려 보자. 이는 인증 주체가 인증을 시도하는 초기에 오직 한 번만 인증 메커니즘들이 사용된다는 것을 의미한다. 그 이후로는 인증 메커니즘이 정보를 필터에 유지하며 조용히 요청을 필터 체인상의 다음 필터로 전달하기만 할 뿐이다. 이렇게 하는 것이 각각의 모든 호출에 대한 신원정보를 제시하는 인증 접근방법이 거의 없다는(그 중에서 BASIC 인증은 예외지만) 사실에 비추어 보면 실용적인 요구사항이긴 하지만, 만약 인증주체의 계정이 말소 혹은 사용 중지 아니면 초기 인증 과정이 지난 후에 변경되었다면 (예를 들면 GrantedAuthority[]가 늘어나거나 줄어드는 경우) 어떻게 될 것인가? 지금부터 그러한 상황을 어떻게 처리하는지에 관해 살펴보기로 하자.

안전 객체를 사용하기 위한 주요 인증 제공자로서 AbstractSecurityInterceptor를 앞에서 소개하였다. 이 클래스는 AuthenticationManager에 접근할 것을 필요로 한다. 뿐만 아니라 이 클래스는 Authentication 객체가 안전 객체 호출에 대해 재인증되어야 할 것인지를 나타내는 구성 설정을 갖고 있기도 하다. 기본적으로 AbstractSecurityInterceptorAuthentication.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에 설정하고, 대응되는 AuthenticationProviderProviderManager에 등록되었는지를 확인하고 난 후 여러분이 해야할 마지막 단계는 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가 두 개의 협력자를 필요로 한다는 것이다. 첫 번째 협력자인 AccessDeniedHandlerImplRequestDispatcher의 포워드(forward)를 이용하여 지정된 접근 거부 오류 페이지를 보여준다. 우리는 포워드를 이용하여 SecurityContextHolder가 여전히 인증주체의 세부사항을 포함하고 있도록 하는데, 이렇게 하면 사용자에게 무언가를 보여주는데 도움이 된다(이전 버전의 Acegi Security에서는 서블릿 컨테이너가 403 오류 메시지를 처리하도록 하였는데, 이렇게 할 경우 문맥적인 정보가 부족했다). 또한 AccessDeniedHandlerImpl은 HTTP 헤더를 403으로 지정하는데, 이는 접근 거부를 의미하는 공식 오류 코드이다. AuthentionEntryPoint의 경우, 여기에서는 인증되지 않은 인증주체가 보호되어 있는 오퍼레이션을 수행하려 할 때 어떠한 동작을 취할 것인가를 지정하고 있다. 예제에서는 폼 기반의 인증을 사용할 것이므로 AuthenticationProcessinFilterEntryPoint와 로그인 페이지의 URL을 지정하였다. 일반적으로 여러분의 애플리케이션은 오직 하나의 진입점만을 가질 것이므로 대부분의 인증 접근법은 독자적인 AuthenticationEntryPoint를 정의한다. 각 인증 접근법에 대해 어떤 진입점을 사용할 것인가에 관한 세부 내용은 인증 접근법 관련 챕터에서 알아볼 것이다.