웹 어플리케이션에 SpringSecurity 2.0.X 를 적용하기 by 오리대마왕

들어가기


Spring framework 의 모듈 중 Spring Security는 일반적인 url 기반의 보안뿐 아니라 domain object의 보안 등 웹 어플리케이션이 필요로 하는 다양한 보안 기능을 제공한다. 이 글을 통해 spring seuciry 2.0.x의 기능 중 가장 보편적으로 사용되는 url 기반의 보안 기능을 namespace 기반의 설정을 사용하여 웹 어플리케이션에 적용하는 방법을 설명해 보고자 한다. spring framework 은 2.5.x 기반을 가정하였다.

맛보기


일단 만들어진 application에 붙여보자. 이 부분은 reference 문서의 2.2 절에 잘 나와있다.

1. web.xml 에 spring security filter를 추가

    <filter>
        <filter-name>springSecurityFilterChain</filter-name>
        <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
    </filter>
    <filter-mapping>
        <filter-name>springSecurityFilterChain</filter-name>
        <url-pattern>/*</url-pattern>
    </filter-mapping>


2. spring security 관련 내용을 spring bean 설정에 추가

namespace 기반의 기본적인 설정을 추가해보자. 물론, spring config에 spring security 관련 namespace 정의를 추가해줘야 한다. 대략 아래와 같은 모습이다.

<beans:beans xmlns="http://www.springframework.org/schema/security"
    xmlns:beans="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.0.xsd
                        http://www.springframework.org/schema/security http://www.springframework.org/schema/security/spring-security-2.0.1.xsd">


다음은 http element 를 통해 어떤 경로에 어떤 권한을 설정할 지를 부여한다.

       <http autoconfig="true">
            <intercept-url pattern="/**" acces="ROLE_USER">
       </http>   


마지막으로 인증/인가 정보를 어떻게 얻어올 것인가에 대한 authentication-provider 를 설정한다. 역시 namespace 기반으로 설정한다.

   <authentication-provider>
       <user-service>
           <user name="aaa" password="pass" authorities="ROLE_USER,ROLE_ADMIN">
           <user name="bbb" password="pass" authorities="ROLE_USER">
       </user>
   </user>

끝!

위의 설정은 다음의 환경에서만 유효하다. 즉, 실전에서는 전혀 사용할 수가 없다.
1. 사용자 정보를 xml 로 관리
2. 인가 정보를 xml로 관리

그럼, 먼저 사용자 정보를 xml 이 아닌 DB에서 읽어오도록 하여 조금 더 발전시켜 보자.

DB를 통한 사용자정보 가져오기


여기에는 namespace가 기본 제공하는 <jdbc-user-service> 라는 element를 사용해서 설정하는 방법이 있지만, 간편하긴 하나 너무 제약사항이 많으므로 좀 더 실전에서 써먹기 좋은 DaoAuthenticationProvider 를 사용하자.

1. 앞서 만들어놓은 설정에서 <authentication-provider> 부분을 삭제하고 다음으로 바꾸자.


    <beans:bean id="daoAuthenticationProvider">
        class="org.springframework.security.providers.dao.DaoAuthenticationProvider">
        <custom-authentication-provider/>
        <beans:property name="userDetailsService" ref="userService"/>
        <beans:property name="hideUserNotFoundExceptions" value="false"/>
        <beans:property name="passwordEncoder">
          <beans:ref local="md5PasswordEncoder">
        </beans:ref>
    </beans:property>
    <beans:bean id="md5PasswordEncoder" class="org.springframework.security.providers.encoding.Md5PasswordEncoder">
    </beans:bean>

갑자기 확확 복잡해지기 시작한다. DaoAuthenticationProvider는 spring security 가 제공하는, DB를 통해 인증을 할 때 사용하는 모듈이다. <custom-authentication-provider> 를 부여하여, 이 빈이 실질적인 authentication-provider 로 동작할 것이라고 spring security에 알려준다. 다음으로 DB로 부터 사용자 정보를 가져오는 작업을 담당할 userDetailsService 를 등록하자. 여기 등록하는 bean은 사용자가 직접 만들어야 한다. hideUserNotFoundExceptions 를 true로 설정할 경우, 로그인 화면에서 "그런 사용자 없습니다." 를 표시해 줄 수 있다. (이름 그대로 사용자가 없을 경우 UserNotFoundExceptions 이 throw 된다.) 마지막으로 패스워드에 대해 암호화를 적용하기 위해 passwordEncoder 를 설정한다. spring security는 다양한 암호화 모듈을 제공하고 있고, 위에서는 많이 쓰이는 Md5 방식을 적용하였다.

2. 남은 것은 위에서 사용하는 userDetailsService 속성에 해당하는 bean을 만드는 것이다. 이건 알아서 만들어서 spring bean으로 등록해야 한다. 이것도 만들기 싫다면 <jdbc-user-service> element를 쓰는 수 밖에 없다. 우리가 만들 사용자 정보 가져오기 service 구현체는 org.springframework.security.userdetails.UserDetailsService interface 를 구현해야 한다. 또한 사용자 정보를 담는 도메인 객체는 org.springframework.security.userdetails.UserDetails interface 를 구현해야 한다.

UserDetailsService 의 경우엔 구현해야 할 method가 매우 직관적이라서 별도로 언급할 필요가 없으나, UserDetails 는 조금 아리까리한 구석이 있다. GrantedAuthority[] getAuthorities() 라는 method 인데, GrantedAuthority 는 일반적으로 의미하는 Role 이라고 생각하면 되겠다. 열심히 구현하여 만든 bean을 등록하자. 그럼 끝.

자, 이제 사용자와 권한 정보를 DB에서 가져오는 것 까지 되었다. 대부분은 여기까지 하면 ok 일 터이나, 만약 권한 설정 대상 자원까지 DB로 설정을 하고자 한다면 추가적인 작업이 필요하다.


DB를 통한 권한 설정 대상 자원 정보 가져오기


제일 복잡하다.

1. AccessDecisionManager 설정하기
이름이 의미하는 바와 같이, 어떤 자원에 대한 access 여부를 판단해 주는 bean을 만들어야 한다. 이를 통해 맨 처음 예에서 봤던 intercept-url 이 해 주는 작업을 사용자가 customizing 할 수 있다. 나의 경우 다음과 같이 설정하였다.

    <beans:bean id="myAccessDecisionManager" class="sample.MyAccessDecisionManager">
        <beans:constructor-arg index="0" ref="resourceService">
    </beans:constructor-arg>

해줘야 할 일이 많다. 우선, 어떤 url 에 누가 접근이 가능한지에 대한 정보를 DB(혹은 파일 등등)로 부터 읽어들이는 작업을 수행하는 서비스 bean이 필요하다. (예를 들어 /customer/** 에는 ROLE_CUSTOMER 만 들어갈 수 있다 등등..)  여기서는 resourceService 라고 명명하였다. 이 service에서 가져온 정보를 토대로 실제로 허용 가/부를 결정하기 위해서는 org.springframework.security.AccessDecisionManager interface 를 구현하는 class를 생성해야 한다. 구현해야 할 method 중 decide(Authentication authentication, Object object, ConfigAttributeDefinition config) 가 핵심이다. 다음은 decide method에 대한 간단한 구현이다.

 public void decide(Authentication authentication, Object object, ConfigAttributeDefinition config)
            throws AccessDeniedException, InsufficientAuthenticationException {

        UserDetail user = (UserDetail) authentication.getPrincipal();
 
        String url = WebUtil.getOriginatingRequestUri(((FilterInvocation) object).getHttpRequest());

        if( isAllowed( user, url ) ){
              return;
       } else if( user == null ) {
      {
            throw new InsufficientAuthenticationException("login please");
      } else {
            throw new AccessDeniedException("access denied");
      }
    }

열심히 만든 AccessDecisionManager 는 <http> element 에 설정해 주어야 동작한다.

    <http access-decision-manager-ref="myAccessDecisionManager" auto-config="true">
                ...
    </http>

아직 끝난 것이 아니다!!

2. FilterSecurityInterceptor 의 objectDefinitionSource 속성 변경
위의 단계에서는 "어떤 url 에 대한 요청이 들어왔고, 이것에 대한 판단이 필요하다고 판단된 경우" 실제 허용 가/부를 결정하는 내용을 구현했다. 그러나 그 전에 앞서 "어떤 url 에 대한 요청이 들어왔는데, 이것에 대한 판단이 필요한가?" 에 대한 판단을 내려야 한다. 결국 이를 위해서는 어떤 어떤 url 리소스들이 보안이 걸린 리소스인가에 대한 정보를 Spring Security 에 주어야 한다.

위 내용을 정리하면 다음과 같다. /secret/diary.htm 에는 ROLE_ADMIN 만 접근가능하다고 가정하자. 이때 /secret/diary.htm 요청이 들어왔는데, 현재 로그인한 사용자가 허가된 사용자인가 아닌가는 1단계에서 판단하다. 그러나 그 전에 /secret/diary.htm 자체가 보안 적용되는 자원인지 아닌지에 대한 정보를 주어야 한다.

이를 위해서는 FilterSecurityInterceptor 의 objectDefinitionSource 속성을 customizing 한 objectDefinitionSource로 변경해야 한다. 우선 customizing한 objectDefinitionSource 를 생성한다. 이를 위해서는 org.springframework.security.intercept.web.FilterInvocationDefinitionSource 인터페이스를 구현한 class를 생성해야 한다. 이때의 핵심은 ConfigAttributeDefinition getAttributes(Object filter) 메서드를 구현하는 것이다.  나의 경우 대략 다음과 같이 작성하였다. 아래 경우는 우리 프로젝트의 특성 상 좀 일반적이지 않는 구현인데, 다른 더 좋은 구현체는 google에서 쉽게 찾을 수 있을 것이다.

 public ConfigAttributeDefinition getAttributes(Object filter) throws IllegalArgumentException {
        FilterInvocation filterInvocation = (FilterInvocation) filter;

        String url = WebUtil.getOriginatingRequestUri(filterInvocation.getHttpRequest());

       if( isThisUrlSecured( url ) )
       {
          ConfigAttributeEditor configAttrEditor = new ConfigAttributeEditor();
         configAttrEditor.setAsText("SECURE_OBJ");
         return (ConfigAttributeDefinition) configAttrEditor.getValue();
       }
        return null;
    }

자, 만들었으니 Spring의 bean으로 등록하자. 이제, 마지막으로  FilterSecurityInterceptor 의 objectDefinitionSource 속성을 우리가 만든 myObjectDefinitionSource 으로 바꾸기만 하면 된다.

그러나 namespace 를 사용하여 설정하였을 경우, FilterSecurityInterceptor 가 밖으로 노출되지 않고 namespace 처리기가 알아서 생성해 버리므로 명시적으로 FilterSecurityInterceptor를 얻어올 방법이 딱히 존재하지 않는다. 이때는 생성된 FilterSecurityInterceptor 의 기본 bean id 인 _filterSecurityInterceptor 를 통해서 생성된 FilterSecurityInterceptor bean에 접근하던가, @Autowired 를 사용해서 이미 생성되어 spring bean으로 등록된 FilterSecurityInterceptor 를 낚아채자.

내 경우는 별로 좋지 않는 구현이긴 하지만 @Autowired 를 쓰지 않고 _filterSecurityInterceptor 로써 FilterSecurityInterceptor에 접근하여 objectDefinitionSource 를 바꿔치기 했다. spring security 의 namespace 설정을 통해서 특정 위치의 filter를 customizing한 filter로 갈아치우는 것도 가능하긴 하지만, FilterSecurityInterceptor 의 경우 미리 bean이 생성되어 등록되기 때문인지 이 방법이 통하지 않았다. 나는 오로지 objectDefinitionSource 바꿔치기만을 위한 class 를 하나 만들고 이를 spring bean으로 등록하였다.

    <beans:bean class="sample.FilterSecurityInterceptorModifier">
        <beans:constructor-arg ref="_filterSecurityInterceptor">
        <beans:property name="objectDefinitionSource" ref="myFilterInvocationDefinitionSource">
    </beans:property>

이쁘게! form 화면 교체


여기까지 했어도 아직 login 화면은 추리하기 그지없는 모양일 것이다. 이때는  element 내부에서 form 을 사용자의 jsp 로 바꿔주는 작업을 하면 된다. 나의 경우는 다음과 같이 설정하였다.

     <http access-decision-manager-ref="myAccessDecisionManager" auto-config="true">
        <form-login login-page="/login.htm" default-target-url="/index.htm"
            authentication-failure-url="/login.htm?error=true" login-processing-url="/security/process_login" />
        <logout logout-url="/security/process_logout" logout-success-url="/index.htm" />
    </http>

맺음말


이상으로 Spring security 를 이용한 url 기반 보안에 대해 살펴보았다. 이것저것 손 대야 해서 복잡하게 보일 수도 있지만, spring security 를 사용함으로써 보안과 관련된 많은 부분을 framework 단에서 처리하므로 개발자의 노력을 상당히 줄일 수 있다. 또한 일반적으로 web container에 종속적으로 구현하는 경우가 많은 보안 역시 container와 독립적으로 구현함으로써 사용자의 web application 의 이식성을 더 높일 수 있다. 굳굳굳!

덧글

  • 세라딘 2008/10/10 13:18 # 삭제

    요즘은 가벼운건 안 올려요? ㅎㅎㅎ
  • 오리대마왕 2008/10/10 14:50 #

    고객사에서 올리려니 뻘쭘해서요. 읽은 책, 본 영화들 백만개인데 정리하기가 쉽지가 않네요. 흑, 버려져가는 나의 블로그...
  • 2009/09/08 20:33 # 삭제 비공개

    비공개 덧글입니다.
  • lahuman 2010/06/04 09:45 # 삭제

    우선 좋은 정보 감사 드립니다.

    DB를 통한 권한 설정 대상 자원 정보 가져오기

    부분을 하면서 막히는 곳이 있습니다.
    <beans:bean id="myAccessDecisionManager" class="sample.MyAccessDecisionManager">
    <beans:constructor-arg index="0" ref="resourceService">
    </beans:constructor-arg>
    이 부분에서 sample.MyaccessDecisionmanager 가 어떤 것을 확장한 것인지 모르겠습니다.
    resourceService 는 org.springframework.security.AccessDecisionManager
    이 인터페이스의 구현체 인것 까지는 따라 갔었는데요
    그리고,
    2. FilterSecurityInterceptor 의 objectDefinitionSource 속성 변경
    부터는 springSecurityFilterChain 쪽에서 에러가 나서 (아마도 위쪽 설정을 제가 잘못 한거겠지만요..)
    더이상 진행을 못하고 있었습니다.
    sample.FilterSecurityInterceptorModifier 이 class 도 어떤 interface나 class를 확장 해야 하는지요?

    혹시 위에 테스트 하신 source가 있으면 제가 염치 불구 하고 요청 드릴 수 있을까요?

    좋은 하루 되세요 읽어 주셔서 감사합니다.

※ 로그인 사용자만 덧글을 남길 수 있습니다.