본문 바로가기

프로그래밍/Spring Security

Spring Security에서 DB를 이용한 자원 접근 권한 설정 및 판단 (2)

지난 글에서는 FilterSecurityInterceptor 클래스를 커스터마이징 하여 DB에서 권한을 조회하여 제어하는 방법에 대해 얘기해보았다. 클래스 소스 위주로 설명했기 때문에 이번에는 이렇게 만든 클래스를 어떤 식으로 설정하여 사용하는지에 대해 언급하도록 하겠다.

 

<authentication-manager>
	<authentication-provider user-service-ref="customJdbcDaoImpl">
		<password-encoder ref="bcryptPasswordEncoder" />
	</authentication-provider>
</authentication-manager>

<beans:bean id="filterSecurityInterceptor" class="org.springframework.security.web.access.intercept.FilterSecurityInterceptor">
	<beans:property name="authenticationManager" ref="org.springframework.security.authenticationManager" />
	<beans:property name="accessDecisionManager" ref="accessDecisionManager" />
	<beans:property name="securityMetadataSource" ref="reloadableFilterInvocationSecurityMetadataSource" />
</beans:bean>

<beans:bean id="accessDecisionManager" class="org.springframework.security.access.vote.AffirmativeBased">
	<beans:constructor-arg>
		<beans:list>
			<beans:bean class="org.springframework.security.access.vote.RoleVoter">
				<beans:property name="rolePrefix" value="" />
			</beans:bean>
		</beans:list>
	</beans:constructor-arg>
	<beans:property name="allowIfAllAbstainDecisions" value="false" />
</beans:bean>

<beans:bean id="reloadableFilterInvocationSecurityMetadataSource" class="com.terry.springsecurity.common.security.ReloadableFilterInvocationSecurityMetadataSource">
	<beans:constructor-arg ref="requestMap" />
	<beans:property name="securedObjectService" ref="securedObjectService" />
</beans:bean>

<beans:bean id="securedObjectService" class="com.terry.springsecurity.common.security.service.impl.SecuredObjectServiceImpl">
	<beans:property name="secureObjectDao" ref="securedObjectDao" />
</beans:bean>

<beans:bean id="securedObjectDao" class="com.terry.springsecurity.common.security.dao.SecuredObjectDao">
	<beans:property name="dataSource" ref="logDataSource_pos" />
	<beans:property name="sqlRolesAndUrl" value="
		SELECT A.RESOURCE_PATTERN AS URL, B.AUTHORITY AS AUTHORITY 
		FROM SECURED_RESOURCES A, SECURED_RESOURCES_ROLE B 
		WHERE A.RESOURCE_ID = B.RESOURCE_ID 
		AND A.RESOURCE_TYPE = 'url' 
		ORDER BY A.SORT_ORDER
	" />
</beans:bean>

<beans:bean id="requestMap" class="com.terry.springsecurity.common.security.UrlResourcesMapFactoryBean" init-method="init">
	<beans:property name="securedObjectService" ref="securedObjectService" />
</beans:bean>

 

위의 설정은 지난 글에서 설명한 클래스를 spring security 설정 xml에 설정한 내용을 보여주고 있다. 몇몇은 이미 이전 글들에서 설명한 적이 있다(<authentication-manager>, <bean id="accessDecisionManager">) 이전 글에서 설명한 적이 있던 것들은 FilterSecurityInterceptor 클래스에 설정하는 인증정보(Authentication Manager)와 판단 주체(Access Manager)이다. 여기 설정에서는 대상 정보(Security MetaDataSource)를 어떻게 설정하고 이를 FilterSecurityInterceptor 클래스에 어떤 식으로 설정하는지를 알아본다.

 

이전 글에서 DB에 저장되어 있는 URL 별 접근 권한을 조회하는 기능을 하는 SecureObjectDao 클래스에 대해 설명한 것이 있다. 이 클래스를 선언한 부분을 보자

 

<beans:bean id="securedObjectDao" class="com.terry.springsecurity.common.security.dao.SecuredObjectDao">
	<beans:property name="dataSource" ref="logDataSource_pos" />
	<beans:property name="sqlRolesAndUrl" value="
		SELECT A.RESOURCE_PATTERN AS URL, B.AUTHORITY AS AUTHORITY 
		FROM SECURED_RESOURCES A, SECURED_RESOURCES_ROLE B 
		WHERE A.RESOURCE_ID = B.RESOURCE_ID 
		AND A.RESOURCE_TYPE = 'url' 
		ORDER BY A.SORT_ORDER
	" />
</beans:bean>

 

DB에서 조회해야 하기 때문에 DataSource를 받아야 하는 부분이 있다. 그래서 dataSource property에 DataSource를 지정했다. 그리고 sqlRolesAndUrl 프로퍼티에 URL 패턴 별 권한을 조회하는 쿼리를 설정해두어 SecureObjectDao 클래스 내부에서 JdbcTemplate을 이용해 조회하게 된다. 쿼리를 보면 ORDER BY 문을 사용하고 있는데 이를 통해 우선 적용되어야 할 패턴을 제일 먼저 검색이 되게끔 하도록 하고 있다.

 

다음은 이렇게 선언된 SecureObjectDao bean 클래스를 사용하는 SecureObjectService 클래스를 선언한 부분이다.

 

<beans:bean id="securedObjectService" class="com.terry.springsecurity.common.security.service.impl.SecuredObjectServiceImpl">
	<beans:property name="secureObjectDao" ref="securedObjectDao" />
</beans:bean>

 

지난 글에서 이 클래스를 설명했을때도 얘기했지만 이 클래스는 단순히 SecureObjectDao 클래스를 통해 나온 조회 결과를 받아다가 다시 SecureObjectService bean을 사용하는 클래스에 넘겨주기 때문에 SecureObjectDao bean만 설정하면 된다. 그렇지만 SecureObjectService는 사용되는 곳이 몇몇 군데가 있다.

 

<beans:bean id="reloadableFilterInvocationSecurityMetadataSource" class="com.terry.springsecurity.common.security.ReloadableFilterInvocationSecurityMetadataSource">
	<beans:constructor-arg ref="requestMap" />
	<beans:property name="securedObjectService" ref="securedObjectService" />
</beans:bean>

<beans:bean id="requestMap" class="com.terry.springsecurity.common.security.UrlResourcesMapFactoryBean" init-method="init">
	<beans:property name="securedObjectService" ref="securedObjectService" />
</beans:bean>

 

지난 글에서 누누히 강조했던 requestMap을 생각해보자. 이 requestMap에 들어있는 것은 SecuredObjectDao를 통해 조회된 URL 패턴별 접근권한이 LinkedHashMap으로 들어있다고 했었다. 그리고 이렇게 조회된 requestMap을 DefaultFilterInvocationSecurityMeatadataSource 클래스와 같은 구조로 만들게 되는 클래스인 ReloadableFilterInvocationSecurityMetadataSource 클래스의 생성자를 이용해서 셋팅된다고 했었다. 그래서 ReloadableFilterInvocationSecurityMetadataSource를 등록할때 constructor-arg 를 이용해서 생성자에 requestMap을 셋팅하도록 했다. 

 

또한 이 requestMap은 FactoryBean을 구현한 UrlResourcesMapFactoryBean을 통해 bean이 만들어져 셋팅이 된다. 이때 이 FactorytBean에 SecuredObjectService를 property에 셋팅해 줌으로써 UrlResourcesMapFactoryBean에서 DB를 이용한 URL 패턴 별 권한 조회를 할 수 있도록 하여 requestMap을 만들수 있도록 한다. 

 

ReloadableFilterInvocationSecurityMetadataSource 클래스에도 property에 SecuredObjectService를 셋팅하여 줌으로써 여기서도 DB를 이용한 URL 패턴별 권한 조회를 하여 requestMap을 재구성할 수 있도록 한다(이전 글에서 화면에 에러가 발생하여 접근 권한을 관리자로 셋팅하여 일반인이 접근하지 못하도록 한 뒤에 조치를 취하는 예를 설명한 적이 있다. 이부분을 구현하기 위함인 것이다. 즉 관리자 화면을 처리하는 Controller 클래스에 ReloadableFilterInvocationSecurityMeatadataSource를 Injection 한 뒤에 reload 메소드를 Controller에서 호출하면 reload 메소드 내부에서 SecureObjectService를 이용해 DB를 다시 조회한뒤 requestMap을 재구성하는 것이다. 저번 글에 올라와 있는 ReloadableFilterInvocationSecurityMetadataSource 클래스를 참조하기 바란다)

 

그리고 다음과 같이 FilterSecurityInterceptor에 인증 정보(Authentication Manager)와 대상 정보(Security MetaDataSource)와 판단 주체(Access Manager)를 설정한 뒤에 FilterSecurityInterceptor 클래스를 등록함으로써 DB를 이용한 자원 접근 권한 설정을 마무리한다

 

<beans:bean id="filterSecurityInterceptor" class="org.springframework.security.web.access.intercept.FilterSecurityInterceptor">
	<beans:property name="authenticationManager" ref="org.springframework.security.authenticationManager" />
	<beans:property name="accessDecisionManager" ref="accessDecisionManager" />
	<beans:property name="securityMetadataSource" ref="reloadableFilterInvocationSecurityMetadataSource" />
</beans:bean>

 

그러면 이렇게 등록된 FilterSecurityInterceptor를 이제는 사용해야 하는데 문제는 FilterSecurityInterceptor는 Spring Security에서 기본적으로 올라가는 Filter 라는 것이다. 즉 이렇게 <bean> 태그를 이용해 등록을 해도 이렇게 등록된 것을 사용하는 것이 아니라 Spring Security가 기본적으로 올리고 있는 FilterSecurityInterceptor를 사용하게 된다는것이다. 기본적으로 올라가는 것을 사용하게 되면 우리가 이제껏 설정한 내용이 아무 소용이 없지 않은가? 즉 Spring Security가 올리는 FilterSecurityInterceptor를 사용하지 말고 우리가 설정한 FilterSecurityInterceptor를 사용하도록 해주어야 한다. 이 부분은 어떻게 하면 좋을까?

 

<http> 태그에는 하위 태그로 <custom-filter> 태그란 것이 있다. 이 태그를 사용하면 Spring Security가 올리는 특정 Filter의 이전, 다음 또는 해당 특정 Filter가 올라가는 그 위치에 원하는 Filter를 셋팅할 수가 있다. 예를 들어..

 

<http>
	<custom-filter before="FORM_LOGIN_FILTER" ref="mycustomformloginfilter"/>
</http>

 

위와 같이 <custom-filter> 태그를 설정하는  UsernamePasswordAuthenticationFilter(Filter의 alias로 FORM_LOGIN_FILTER가 된다)가 올라가는 위치의 바로 전에 <bean> 태그로 등록한 mycustomformloginfilter를 올리겠다는 의미이다. 즉 mycustomformloginfilrer를 거쳐서 Spring Security가 등록하는 UsernamePasswordAuthenticationFilter를 거친다는 의미가 되겠다. 이렇게 내가 올리고자 Filter를 원하는 위치에 셋팅할 수 있다. 위치를 지정하는 속성은 before, after, position이 있는데 before는 지정된 Filter의 이전, after는 지정된 Filter의 이후, position은 지정된 Filter 그 위치에 해당 Filter를 등록하게 된다. 즉 before나 after의 경우는 Spring Security가 올리는 Filter를 대체하지는 않지만 position으로 설정할 경우는 지정된 Filter 대신 사용자가 설정한 Filter가 올라가게 된다. 그리고 이렇게 before나 after, position을 통해 지정되는 Filter는 alias로 Spring Security 관련 글에서 맨 처음 글에 썼던 Spring Security가 사용하는 Filter 목록에서 해당 Filter의 alias를 사용하면 된다.

 

이때 주의점이 있다. before나 after의 경우 모든 Filter의 alias를 사용할 수 있으나 position의 경우 사용할 수 없는 alias가 있다. 즉 특정 위치를 대신해서 사용자 정의 Filter를 넣고자 할때 그걸 할 수 없는 Filter가 존재함을 의미한다. SecurityContextPersistenceFilter, ExceptionTranslationFilter, FilterSecurityInterceptor 가 바로 이에 해당하는 filter 들이다. 왜 그런가 하면 이 3가지의 Filter는 <http> 태그 사용시 자동으로 올라가기 때문이다. <custom-filter> 태그의 사용여부와는 상관없이 이 3개의 filter는 항상 올라가게 되며 이미 올라간 시점에서 <custom-filter> 태그를 이용해 대체를 할 수 없기 때문이다. 또한 position을 사용해서 Filter를 대체할 경우 그 Filter가 올라가게끔 만드는 태그는 사용하면 안된다. 위에서 예를 들은 UsernamePasswordAuthenticationFilter를 예로 들어보다. 위에서는 before로 했지만 만약 before가 아닌 position을 사용해서 UsernamePasswordAuthenticationFilter를 대체할 경우 UsernamePasswordAuthenticationFilter가 올라가게끔 만드는 태그인 <form-login> 태그를 사용하면 안된다는 얘기이다. 즉 <form-login> 태그에서 했던 각종 설정 작업들을 UsernamePasswordAuthenticationFilter를 대체하는 Filter에서 대신 설정되어져서 올라가야 한다는 뜻이다.

 

※ SecurityContextPersistenceFilter, ExceptionTranslationFilter, FilterSecurityInterceptor 필터를 대체하고 싶을 경우 방법이 아주 없지는 않다. Spring Security는 Spring Security가 사용하는 Filter들을 chain 개념으로 연결해서 사용하고 있는데 그런 역할을 해주는 클래스가 org.springframework.security.web.FilterChainProxy 클래스이다. 이 클래스를 이용해서 다음과 같은 형식으로 Filter 순서를 지정하면 <custom-filter> 태그로도 지정할 수 없는 것을 지정할 수 있게 된다. 그러나 이렇게 설정할 경우 Spring Security가 제공하는 설정 관련 xml 태그(<http>태그나 <authentication-manager> 태그 등)를 사용하면 안된다(즉 Spring Framework가 제공하는 <bean> 태그와 <property> 태그 등으로 관련 설정을 하라는 얘기이다.)

 

<bean id="filterChainProxy" class="org.springframework.security.web.FilterChainProxy">
  <constructor-arg>
    <list>
      <sec:filter-chain pattern="/restful/**" filters="
           securityContextPersistenceFilterWithASCFalse,
           basicAuthenticationFilter,
           exceptionTranslationFilter,
           filterSecurityInterceptor" />
      <sec:filter-chain pattern="/**" filters="
           securityContextPersistenceFilterWithASCTrue,
           formLoginFilter,
           exceptionTranslationFilter,
           filterSecurityInterceptor" />
    </list>
  </constructor-arg>
</bean>

 

 

이러한 내용을 보았으니 이제는 <custom-filter> 태그를 사용해서 우리가 만든 FilterSecurityInterceptor 클래스를 사용하도록 해보자. 방금 전에 설명했듯이 <custom-filter> 태그의 position 속성을 통해서 기존 FilterSecurityInterceptor 클래스를 대체하도록 할 수는 없다. 그래서 before 속성을 이용해서 Spring Security가 등록하는 FilterSecurityInterceptor 클래스 전에 우리가 만든 FilterSecurityInterceptor 클래스를 거치게 하고 기존 Spring Security가 등록하는 FilterSecurityInterceptor  클래스는 bypass 하도록(그렇게 전개될 수 밖에 없는 것이 <http> 태그의 <intercept-url> 태그가 선언되어 있어야 Spring Security가 올리는 FilterSecurityInterceptor 클래스에서 권한 체크를 할텐데 그런 부분이 없기 때문이다) 그렇게 컨셉을 잡는다. 이렇게 해서 우리가 만든 FilterSecurityInterceptor 클래스에서 권한 체크를 하도록 한다

 

<http auto-config="true" use-expressions="true">
	<form-login
		username-parameter="loginid"
		password-parameter="loginpwd" 
		login-page="/login.do"
		default-target-url="/main.do"
		authentication-success-handler-ref="customAuthenticationSuccessHandler"
		authentication-failure-handler-ref="customAuthenticationFailureHandler"
	/>
	<anonymous granted-authority="ANONYMOUS" />
	<logout 
		logout-success-url="/main.do"
		delete-cookies="JSESSIONID"
	/>
	<custom-filter before="FILTER_SECURITY_INTERCEPTOR" ref="filterSecurityInterceptor"/>        
</http>

 

이<http> 태그의 <custom-filter> 태그를 사용한 것을 보자. Spring Security가 올리는 FilterSecurityInterceptor 전에 우리가 만든 FilterSecurityInterceptor 클래스를 올리기 위해 before 속성에 FilterSecurityInterceptor의 Alias인 FILTER_SECURITY_INTERCEPTOR를 사용했고 ref 속성에 우리가 만든 FilterSecurityInterceptor bean을 지정했다.

 

이번글로 Spring Security에서 DB를 이용한 자원 접근 권한 설정 및 판단에 대한 설명을 마치겠다. 다음글에서는 이렇게 URL 패턴별로 접근 권한을 검증하도록 했는데 해당 권한을 가지고 있지 않아 접근할 수 없을 경우 사용자가 지정하는 화면으로 이동하는 방법을 설명하도록 하겠다.