90-3.アクセス認可設定を自動リロードするサンプル
(Security2.x系)
概要
Spring Security 2.x系のサンプルです。
自作したフィルタを使いたいときにどうすればよいか?
そのサンプルと思っていください。
サンプル全体の説明もお読みください。
目標
まずはゴールを示します。
Spring Securityでは、アクセス認可の設定はSpringの設定ファイルに記述します。
そして一度ロードされるとtomcatを再起動しない限り、変更を反映することはできません。
しかし、厳しい運用環境ではtomcatの再起動はしずらいものです。
Spring Securityのアクセス認可の部分だけ外部ファイル化してWARの外に置いて、
そのファイルのタイムスタンプが新しくなっていたら、アクセス認可を読み込み直すということはできないものでしょうか?
【考え方】
まず、どの機能をカスタマイズすればよいかを考えます。
そのためには、内部の仕組みの記事の図の左側(処理フロー)を見ます。
このうちどれをカスタマイズすればよいかを見ると、認可なので、アクセス認可処理フィルタに行き当たります。
この図ではフィルタ内はaccessDecisionManagerしか記述されていませんが、
説明を読むとaccessDecisionManagerは投票を制御するもので「アクセス認可の設定」を保持しているとは考えにくいです。
そこで、フィルタのjavadocを見てみます(FilterSecurityInterceptorクラス)。
すると、値を取得できるメソッドで、getObjectDefinitionSource() やobtainObjectDefinitionSource()あたりがそれっぽい名前で、
取得できるクラスをjavadocで読むと、探しているアクセス認可定義を表すクラスであることが分かります。
結局、フィルタで保持しているFilterInvocationDefinitionSource(アクセス認可定義)を置き替えてやれば目的を達成できそうです。
【実装】
ここではSpring Securityの認可の設定だけを外部ファイルに置き、認可の処理のたびにタイムスタンプを見て、変更されていればロードすることにします。
<WEBの問題>
注意しなければならないことがあります。
それは、WEBは同時アクセスがあるということです。
つまり、あるリクエストが古い設定ファイルで認可処理をしている途中で、もうひとつのリクエストでリロードすることが起きえます。
そうすると、1つ目のリクエスト処理をしている最中に認可設定が変わるので、動作不良が起きる可能性があります。
そこで、1つのリクエスト処理中は認可設定が変わらないように、スレッドローカルに設定を参照できるようにします。
⇒具体的な実装
内部のスレッドローカルにFilterInvocationDefinitionSource(アクセス認可定義)を持ち、
外部からアクセス認可定義を変えられる仲介クラス①を作ります。
仲介クラス①はFilterInvocationDefinitionSourceを継承し、FilterSecurityInterceptor(アクセス認可処理フィルタ)に設定します。
そして、自作フィルタ②を作り、FilterSecurityInterceptorフィルタの前に置き、自作フィルタ②を通った時に仲介クラス①を介して、
FilterSecurityInterceptorフィルタにアクセス認可定義オブジェクトを設定することにします。
フィルタ②は1回のリクエストで1度しか呼ばれないので、あるリクエスト処理の途中で認可定義が変わることがなくなります。
実際のサンプル
自作クラス(/src/com/my/security/PuppetDefinitionSource.java)
package com.my.security;
import java.util.Collection;
import org.springframework.security.ConfigAttributeDefinition;
import org.springframework.security.intercept.ObjectDefinitionSource;
import org.springframework.security.intercept.web.FilterInvocationDefinitionSource;
/**
* 1つのリクエストの途中で情報が変わらないように、スレッドローカルで認可の情報を管理するクラス。
* このクラスを介して、FilterSecurityInterceptorの認可定義を変更することを目的としたクラス。
*/
public class PuppetDefinitionSource implements FilterInvocationDefinitionSource {
static private ThreadLocal<ObjectDefinitionSource> localDef = new ThreadLocal<ObjectDefinitionSource>();
@Override
public ConfigAttributeDefinition getAttributes(Object att)
throws IllegalArgumentException {
return localDef.get().getAttributes(att);
}
@Override
public Collection getConfigAttributeDefinitions() {
return localDef.get().getConfigAttributeDefinitions();
}
@Override
public boolean supports(Class paramClass) {
return localDef.get().supports(paramClass);
}
public void setDefinitionSource(FilterInvocationDefinitionSource def){
localDef.set(def);
}
}
このクラス自体は、スレッドローカルに保存した実体に処理を委譲するだけです。
自作クラス(/src/com/my/security/DefinitionSourceRefreshFilter.java)
package com.my.security;
import java.io.File;
import java.io.IOException;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.BeanFactoryAware;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.context.support.FileSystemXmlApplicationContext;
import org.springframework.core.Ordered;
import org.springframework.core.io.Resource;
import org.springframework.security.intercept.web.FilterInvocationDefinitionSource;
import org.springframework.security.intercept.web.FilterSecurityInterceptor;
import org.springframework.util.Assert;
/**
* リクエストのたびにFilterSecurityInterceptorに認可定義currentDefSourceを設定するだけのフィルタ。
* 初めてのdoFilter時に強引にFilterSecurityInterceptorの認可定義(FilterInvocationDefinitionSource)を
* localDefinitionSourceに変更している。
* FilterSecurityInterceptorフィルタの前に設定すること。
*/
public class DefinitionSourceRefreshFilter
implements Filter, Ordered, InitializingBean, BeanFactoryAware {
private FilterSecurityInterceptor nextFilter;
private boolean isAlreadySetDef = false;
private FilterInvocationDefinitionSource currentDefSource;
private PuppetDefinitionSource localDefinitionSource = new PuppetDefinitionSource();
private Resource resource;
private long curFileTimestamp = 0L;
private BeanFactory beanFactory;
@Override
public int getOrder() {
return 1650; //FilterSecurityInterceptorが1700なのでそれより前の値にした
}
@Override
public void destroy() {
}
@Override
public void init(FilterConfig conf) throws ServletException {
}
@Override
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
throws IOException, ServletException {
//nextFilterにカスタマイズクラスを設定しておく
if(!this.isAlreadySetDef){
this.nextFilter.setObjectDefinitionSource(this.localDefinitionSource);
this.isAlreadySetDef = true;
}
//定義の読み込みと定義の設定
reload();
this.localDefinitionSource.setDefinitionSource(this.currentDefSource);
chain.doFilter(req, res);
}
/**タイムスタンプが新しい場合、
* AppilicationContextを読み込み直し、currentDefSource を設定しなおす
* @throws Exception
*/
synchronized
protected void reload() throws ServletException{
FileSystemXmlApplicationContext securityContext = null;
try{
//タイムスタンプが新しくない場合は何もしない
long fileTimestamp = this.resource.lastModified();
if(this.curFileTimestamp == fileTimestamp) return;
//context xmlファイルの読み込み
File file = this.resource.getFile();
securityContext = new FileSystemXmlApplicationContext(file.getAbsolutePath());
FilterInvocationDefinitionSource def
= securityContext.getBean(FilterInvocationDefinitionSource.class);
//Sourceを設定する
if(def == null){
throw new RuntimeException("FilterInvocationDefinitionSourceがNULLだった。file=" + file);
}
this.currentDefSource = def;
this.curFileTimestamp = fileTimestamp;
}catch(Exception e){
throw new ServletException("security contextをリロード中にエラー発生", e);
}finally{
//applicationContextのclose
if(securityContext != null){
try{ securityContext.close(); }catch(Exception e){}
}
}
}
//-------------------------------------------------
@Override
public void afterPropertiesSet() throws Exception {
this.nextFilter = this.beanFactory.getBean(FilterSecurityInterceptor.class);
Assert.notNull(this.nextFilter, "filter is required.");
Assert.notNull(this.resource, "resource is required.");
reload();
}
public void setResource(Resource resource) {
this.resource = resource;
}
@Override
public void setBeanFactory(BeanFactory factory) throws BeansException {
this.beanFactory = factory;
}
}
【認可設定のロードの仕方】
外部ファイルに認可設定を記述しておき、それを読み込みます。
このとき、FileSystemXmlApplicationContextを新たにnewして読み込んでいます。
ただ、目的のFilterInvocationDefinitionSourceはSpringSecurityがXMLファイルの読み込み時に自動で作ります。
bean名が分からないのです。
こんなときはクラス名を指定してgetBean()します。
【フィルタの取得の仕方】
FilterSecurityInterceptorフィルタを取得したいのですがbean名が分かりませんので上記と同じテクニックを使います。
また、DIコンテナを取得するためにBeanFactoryAwareをimplementsしているところもポイントです。
SpringがsetBeanFactory()に自動でオブジェクトを設定します(参考)。
リロード用の外部Spring設定ファイル(c:/temp/security2.xml)
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:util="http://www.springframework.org/schema/util"
xmlns:aop="http://www.springframework.org/schema/aop"
xmlns:sec="http://www.springframework.org/schema/security"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
http://www.springframework.org/schema/util
http://www.springframework.org/schema/util/spring-util-3.0.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop-3.0.xsd
http://www.springframework.org/schema/security
http://www.springframework.org/schema/security/spring-security-2.0.1.xsd">
<!-- 認可のカスタマイズ -->
<sec:filter-invocation-definition-source id="myDefinitionSource">
<sec:intercept-url pattern="/error.jsp" access="IS_AUTHENTICATED_ANONYMOUSLY"/>
<sec:intercept-url pattern="/login-provs.jsp" access="IS_AUTHENTICATED_ANONYMOUSLY"/>
<sec:intercept-url pattern="/403.jsp" access="IS_AUTHENTICATED_ANONYMOUSLY"/>
<sec:intercept-url pattern="/top2.jsp" access="ROLE_ADMIN,ROLE_READ"/>
<sec:intercept-url pattern="/**" access="IS_AUTHENTICATED_FULLY"/>
</sec:filter-invocation-definition-source>
</beans>
単純に、FilterInvocationDefinitionSource オブジェクトを作成するだけのタグを記述します。
ファイルは、このサンプルではc:/temp/配下に置きます。
Spring設定ファイル(/WEB-INF/spring/applicationContext-security-interc.xml)
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:util="http://www.springframework.org/schema/util"
xmlns:aop="http://www.springframework.org/schema/aop"
xmlns:sec="http://www.springframework.org/schema/security"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
http://www.springframework.org/schema/util
http://www.springframework.org/schema/util/spring-util-3.0.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop-3.0.xsd
http://www.springframework.org/schema/security
http://www.springframework.org/schema/security/spring-security-2.0.1.xsd">
<!-- 認可のカスタマイズ -->
<!-- SpringSecurityのメイン設定
-->
<sec:http path-type="ant" access-denied-page="/403.jsp" auto-config="false" >
<sec:intercept-url pattern="/error.jsp" access="IS_AUTHENTICATED_ANONYMOUSLY"/>
<sec:intercept-url pattern="/login-provs.jsp" access="IS_AUTHENTICATED_ANONYMOUSLY"/>
<sec:intercept-url pattern="/403.jsp" access="IS_AUTHENTICATED_ANONYMOUSLY"/>
<sec:intercept-url pattern="/top2.jsp" access="ROLE_ADMIN"/>
<sec:intercept-url pattern="/**" access="IS_AUTHENTICATED_FULLY"/>
<sec:form-login login-page="/login-provs.jsp" default-target-url="/top.jsp"
authentication-failure-url="/login-provs.jsp?error=true"/>
<sec:logout logout-url="/logout" logout-success-url="/login-provs.jsp" invalidate-session="true"/>
<sec:anonymous granted-authority="ROLE_ANONYMOUS"/>
</sec:http>
<!-- 認証手形発行所 -->
<sec:authentication-provider>
<sec:user-service>
<sec:user name="taro" password="taro" authorities="ROLE_ADMIN"/>
<sec:user name="hanako" password="hanako" authorities="ROLE_READ"/>
</sec:user-service>
</sec:authentication-provider>
<!--
自作のフィルタ。
-->
<bean id="accessDecisionManagerRefreshFilter" class="com.my.security.DefinitionSourceRefreshFilter">
<!-- フィルタの挿入位置を指定する。ここでは1つ前を指定している。 -->
<sec:custom-filter before="FILTER_SECURITY_INTERCEPTOR"/>
<property name="resource" value="file:c:/temp/security2.xml" />
</bean>
</beans>
【自作フィルタの設定の仕方】
custom-filterタグを使用します。
自作したクラスをbeanタグで設定を記述し、beanタグの中にcustom-filterを記述するだけです。
このとき、フィルタをどの位置に置くかを指定します。それが、before属性で、フィルタ名 (例:FILTER_SECURITY_INTERCEPTOR) を指定します。
属性は以下のものがあります。
属性:
position ・・・指定のフィルタと置き換えをします。
before ・・・指定のフィルタの前に置きます。ですので、指定のフィルタは生きています。
after ・・・指定のフィルタの後に置きます。ですので、指定のフィルタは生きています。
位置名: eclipse上でctrl+spaceキーを押すと表示されますので、それで指定してみてください。
※Spring Security3.x系では設定方法が少し変わるのでご注意ください。
簡単ですよね。
追記:
フィルタの置き替え(position属性)で、FILTER_SECURITY_INTERCEPTORを指定するとorderが被ってうまくいかないという記事を読みました。
実際試してみるとうまくいきません。原因はよくわかりません。すみません。
他の、認証処理(AuthenticationProcessingFilter)などではうまくいきます。
最悪、このフィルタの設定をbeanタグで行うという逃げ道はあるかと思いますが、めったにないケースかと思います。
Created Date: 2013/07/07