Acegi로 웹 애플리케이션 보안하기 7

지금까지 등록한 필터들을 나열 하면 다음과 같습니다.

1. httpSessionContextIntegrationFilter => 세션 문맥 통합 필터
2. logoutFilter => 로그 아웃 필터
3. authenticationProcessingFilter => 인증 처리 필터
4. exceptionTranslationFilter => 예외 처리 필터
5. filterInvocationInterceptor => 권한 처리 필터

FilterChainProxy의 filterInvocationDefinitionSource속성에 등록한 이 들의 순서는 매우 중요합니다.

일례로, exceptionTranslationFilter가 filterInvocationInterceptor보다 뒤에 있으면 어떤 일이 발생할까요? 예외 처리 필터의 존재 가치가 없어집니다. 예외 처리 필터가 예외를 확인하기 전에 권한 처리 필터에게 넘겨지기 때문에, 예외 처리 필터는 예외가 발생한지도 모른 상태이고, 웹 브라우저는 그냥 에러를 출력하게 됩니다.

또 다른 예로, 권한 처리가 인증 처리 필터보다 앞에 있으면 어떤 일이 발생할까요? 위의 상태에서, 1, 2, 4, 5, 3형태로 배열 했더니 웹 요청 처리가 무한루프에 빠지는 것을 볼 수 있었습니다.ㅎㅎ

그런 반면, 로그 아웃 필터의 위치는 어디에 두던지 별 영향을 미치지 않는 것을 확인할 수 있었습니다.

필터들의 순서를 가지고 여러 가지 실험을 해봤을 때, 가장 안전한 선택은 다음과 같습니다.

1. 세션 문맥 통합 필터는 가장 앞에 둔다.
2. 권한 처리 필터는 가장 마지막에 둔다.
3. 예외 처리 필터는 권한 처리 필터 바로 앞에 둔다.
4. 인증 처리 필터는 예외 처리 필터 앞에 둔다.
5. 기타 인증과 관련된 필터(리멤버미 필터, 익명 사용자 처리 필터)는 인증 처리 필터 바로 뒤에 연달아 둔다.

예를 들어 다음과 같은 순서대로 필터를 나열하면 안전 합니다.

httpSessionContextIntegrationFilter,
authenticationProcessingFilter,
anonymousProcessingFilter,
rememberMeProcessingFilter,
logoutFilter,
exceptionTranslationFilter,
filterInvocationInterceptor

Acegi로 웹 애플리케이션 보안하기 6

6. 예외 다루는 필터 등록하기

사용자 삽입 이미지
org.acegisecurity.ui.ExceptionTranslationFilter 를 등록해 줍니다. 그리고 이 필터에 authenticationEntryPoint 속성과 accessDeniedHandleerImpl 속성을 설정해 줍니다. authenticationEntryPoint 는 인증 예외가 발생했을 때 인증을 요구하는 페이지로 이동하도록 설정했으며, accessDeniedHandleerImpl 는 해당 사용자가 권한이 없을 요청을 했을 때 보여질 페이지를 설정했습니다.

7. 로그아웃 필터 등록하기

사용자 삽입 이미지이 필터는 생성자 인젝션을 사용했네요. 흠~ 독특합니다. 첫 번째 인자로는 로그아웃을 한 뒤 보여줄 URL을 설정하고, 두 번쨰 인자로는 로그아웃 핸들러를 등록해 주었습니다. 이 객체가 실제로 Session 객체에서 Security Context 객체를 제거하는 일을 담당할 것입니다.

위 두 개의 필터를 역시 FilterChainProxy에 등록해주면, 잘 동작하는 모습을 확인할 수 있습니다.

Acegi로 웹 애플리케이션 보안하기 5

5. HttpSessionIntegrationFilter 사용하기

앞에서 인증과 보안에 관련된 필터과 bean들을 모두 등록했지만, 애플리케이션은 동작하지 않습니다. 그 이유는 보안 정보가 여러 요청들 간에 유지되지 않기 때문입니다. 이 문제를 해결하기 위해 보안 정보를 Session에서 관리하는 필터를 등록해 줍니다.

    <bean id=”httpSessionContextIntegrationFilter”
        class=”org.acegisecurity.context.HttpSessionContextIntegrationFilter” />

이 필터는 다른 객체를 사용하지 않고 있어서 설정이 매우 간단합니다. 이렇게 등록한 다음 역시 다른 필터들과 마찬가지로 FilterChainProxy에 등록해 줍니다.
사용자 삽입 이미지이 필터를 제일 앞에 등록합니다. 그 이유는 이 녀석이 하는 일과 관련이 있습니다.

사용자 삽입 이미지그림은 IBM 기사에서 가져왔습니다. 먼저 Filter Chain Proxy에서 SIF를 호출하고 이 필터에게 요청을 넘깁니다.(1) 그럼 SIF는 이 요청이 이전에 다뤘던 요청인지 아닌지 판단합니다.(2) 만약에 이전에 다뤘던 요청이면 일을 끝내고 다음 필터로 요청을 넘깁니다.(4) 처음 다루는 요청이면 플래그를 설정하고(이미 다룬 요청이라는 것을 기억하기 위해) 세션 객체가 있는지 그리고 그 안에 보안 문맥(Security Context)를 담고 있는지 확인합니다. 있으면, 해당 보안 객체를 Security Context Holder에 넣어 둡니다. 만약 세션 객체가 없으면 새로운 보안 문맥을 생성하고 그것을 Security Context Holder에 넣어 둡니다.(3) 그런 다음 요청을 다음 필터로 넘깁니다.(4)

다른 필터들이 이 보안 문맥을 수정할 수 있습니다.(5) 모든 필터의 처리가 완료된 다음 SIF가 제어권을 가지게 됩니다.(6) 필터 체인 특성 상 그렇게 되어있습니다.
사용자 삽입 이미지만약 이 때 Security Context가 변경되어 있다면, 이 정보로 Session 객체있는 Security Context를 수정합니다.(7)

이제는 애플리케이션에 한 번 로그인 상태로 끝내더라도, 다음에 접속할 때 정보를 계속 유지하고 있게 됩니다. 즉, 서버를 꼈다 켜도 사용자 정보를 기억하고 있습니다. 신기하네요.

이제 사용자 정보를 유지하는 방법은 알아냈는데, 문제는 한 번 로그인 한 사용자의 정보를 Session에서 삭제하는 방법입니다. 계속해서 한 사용자로 로그인 되어 있으면 안 되겠죠. 로그아웃이 필요합니다.
그리고 사용자 정보가 없을 때 예외를 브라우저에 출력하지 말고 로그인 폼으로 이동하도록 하는 예외 처리가 필요합니다.

다음 글에서 예외 처리와 로그아웃 필터를 다루겠습니다.

Acegi로 웹 애플리케이션 보안하기 4

4. 권한 확인하기.

앞에서 인증은 제대로 동작하지만, 인증 만으로는 보안을 할 수 없습니다. 사용자는 식별할 수 있지만, 해당 사용자가 요청에 대한 권한이 있는지는 별도로 확인을 해야 합니다. 이 일을 해줄 필터과 bean을 등록하겠습니다.

권한을 위해 Acegi에서 제공하는 필터는 FilterSecurityInterceptor입니다.
사용자 삽입 이미지FilterSecurityInterceptor에서는 앞에서 설저한 AuthenticationManager를 사용하며, 그 외에도 AccessDecisionManager와 objectDefinitionSource라는 속성을 설정합니다.

AuthentiactionManager를 사용하여 사용자 정보를 가져오고, ObjectDefinitionSource에 있는 설정과, AccessDecisionManager를 사용하여, 해당 사용자가 요청에 대한 권한이 있는지 확인합니다.

어떤 요청에 대한 필요한 권한을 설정해 놓은 것이 바로 ObejctDefinitionSource입니다. 위 그림의 빨간색 박스에 해당하며, /protected/로 시작하는 모든 요청은 ROLE_HEAD_OF_ENGINEERING 의 role을 가진 사용자에게만 허가합니다. 그것을 제외한 모든 요청은 IS_AUTHENTICATED_ANONYMOUSLY 라는 role이 사용 하도록 했는데, 이 것은 anonymous Filter를 등록했을 때 익명 사용자에게 부여한 권한이라고 생각하시면 됩니다. 나중에 더 자세히 살펴보겠습니다.

이렇게 설정한 상태에서 FilterChainProxy에 여기서 등록한 필터의 이름을 추가해 줍니다.

사용자 삽입 이미지

자 이 상태에서 바라는 것은 다음과 같습니다.
1. /**로 접속 했을 때는 익명 사용자로써, 화면이 보여집니다. 따라서 첫 페이지에 접속할 수 있으며, 로그인 페이지에도 접속할 수가 있습니다.
2. 로그인을 하지 않은 상태에서 /protected/**로 접속했을 때 예외가 발생할 것입니다. 해당 요청에 대한 권한이 없기 때문이죠.
3. ROLE_HEAD_OF_ENGINEERING role을 가진 사용자로 로그인을 한 상태에서라면, /protected/**로 접근 했을 때 화면을 볼 수 있습니다.

발생하는 예외는 다음과 같습니다.
사용자 삽입 이미지org.acegisecurity.AuthenticationCredentialsNotFoundException
필요한 사용자에 대한 정보가 Security Context 객체에 없기 때문입니다.

서버를 종료한 다음 다시 실행하면, 로그인 정보를 기억하지 못합니다. 사용자 정보를 계속해서 유지하려면 필터 하나만 등록하면 됩니다.

Acegi로 웹 애플리케이션 보안하기 3

3. 인증하자.(보안된 정보에 접근할 때 로그인을 하도록…)

이제부터 이전 글에서 등록한 FilterChainPorxy에 필터를 하나씩 등록하면 됩니다. 그러려면 일단 필터를 bean으로 등록해야겠죠. 그리고 그 필터가 종속성을 가지는 객체들도 역시 bean으로 등록하면 됩니다.

인증을 하기 위해 Acegi에서 제공하고 있는 필터는 AuthenticationProcessingFilter입니다.
사용자 삽입 이미지이 필터는 authenticationManager를 사용하여 인증 처리를 하고 있습니다. 동작하는 원리는 IBM에 올라온 기사를 참조하면 자세히 설명되어 있습니다.
사용자 삽입 이미지간략히 설명을 하면, APF로 요청, 응답, 필터 체인 객체가 넘어옵니다.(1) 그 뒤에 APF에서 인증 토큰(username, password, 등 기타 요청 개체에 딸려온 정보)을 만들고(2), 이 것을 AuthenticationManager에 넘겨줍니다.(3)

그럼 이제, AuthenticationManager를 등록해야겠습니다.
사용자 삽입 이미지authenticationManager에서는 하나 이상의 authenticationProvider를 사용하여, 인증 작업을 처리하고 있습니다. manager는 여러 provider를 사용하여 어떤 provider가 APF로 부터 받은 인증 토큰을 지원하는지 확인합니다.(4) 그러려면 인증 토큰을 provider에게 보내야겠죠.(5)

authentication Provider는 manager로 부터 받은 인증 토큰에서 username을 꺼낸 다음, userChache가 등록되어 있다면 이 정보를 user cache service라는 녀석에게 넘깁니다.(6) 하지만 위의 예제는 아직 userChache를 사용하고 있지 않기 때문에, (7), (8)에 대한 설명은 생략하겠습니다.

userChache가 없거나, userChache에서 username으로 확인(9) 결과가 null 이면, 다시 username을 이번에는 user detail service에게 넘깁니다. (10)userDetailService는 뒷단(위의 예제에서는 프로퍼티 파일을 사용했네요.)에서 사용자 정보를 찾습니다.(11) 해당 username에 대한 사용자 정보를 찾아서 가져오거나, 그런 사용자가 없다는 예외를 발생시킬 것 입니다.(12)

이렇게해서 찾은 정보와 인증 토큰의 정보를 가지고 비번 확인을 합니다. 일치하면, 해당 사용자 정보를 다시 Authentication Manager에게 넘겨주고, 일치하지 않으면 예외를 발생시킵니다.(13) Authentication Manager는 다시 이 정보를 APF에게 잔달합니다.(14) 그러면 APF는 이 정보를 Security Context에 저장하고(15) 다음 필터를 호출합니다.(16)

자 이제 거의 다 끝났습니다.

마지막으로 할 일은 APF를 “Acegi로 웹 애플리케이션 보안하기 2″에서 만들었던 FilterChainProxy에 등록하는 일입니다.
사용자 삽입 이미지filterChainProxy의 filterInvocationDefinitionSource 속성에 위와 같이 설정해 줍니다. 모든 URL은 비교하기 전에 모두 소문자로 바꾸고, ANT 스타일의 표현식을 사용할 것이라는 설정과, 모든 url이 authenticationProcessingFilter를 거치도록 설정했습니다.

http://www.acegisecurity.org/acegi-security/apidocs/constant-values.html#org.acegisecurity.util.FilterChainProxy.TOKEN_NONE

위 링크에서 Acegi에서 사용할 수 있는 상수들을 참조할 수 있습니다.

자 이렇게 하면…
이제 인증(로그인)은 제대로 동작하지만, 여전히 인증을 하지 않고도 보안이 필요한 자원에 누구나(로그인을 하든 안하든 관계없이)접근할 수 있습니다.