ひよっこ。

I want to…

Posts Tagged ‘Spring3’

SpringSecurityでIPアクセス制御を行う

Posted by hikaruworld : 2011 6月 15

SpringSecurityを使っていて、特定のIPアドレス以外からの認証を制御したくなりました。
最初はVoterを拡張して、IPAccessVoter的なものを実装していたんですが、
ドキュメントを読んでみると、15. Expression-Based Access Controlというのを発見しました。

読んでみれば、use-expressions=”true”を設定して、accessを利用すれよいとのこと。
ソンナー、コンナカンタンナホウホウガ。

というわけで、ドキュメントに従って以下のように設定しました。

<http use-expressions="true">
    <intercept-url pattern="/hoge*" access="hasIpAddress('127.0.0.1')"/>
</http>

で、確認してみるとOKー。

気がつくのが遅かったorz…。

以下、余談。

大体こういう制御はプロパティに出しておきたいので、プロパティファイルに押し出して@Valueで値を設定しておきました。
プロパティファイルへのキーがconfigとすると、大体こんな感じになります。

<http use-expressions="true">
    <intercept-url pattern="/hoge*" access="#{config.ipAddress}"/>
</http>

設定ファイルはこんな感じ

ipAddress=hasIpAddress('127.0.0.1')

注意点としてはuse-expressionsを利用した場合、accesssの戻り値はbooleanにする必要があるみたいです。
hasIpAddressを複数指定したい場合は、こんな感じで指定します。

ipAddress=hasIpAddress('127.0.0.1') or hasIpAddress('localhost')

以上です。

広告

Posted in program | タグ: , | Leave a Comment »

AMF48が便利だったのでSpringFramework3経由でもっと楽してみた

Posted by hikaruworld : 2011 4月 25

先日のfxug@北陸in富山で、わっきーさんがAMF通信用の軽量ライブラリとして、
AMF48を作ったよという紹介をしてくれていました。
某BlazeDSとかに比べてずいぶんお手軽そうだったので、Spring経由でもっと楽しようと思って遊んでいました。
(ちゃんとJava5対応してて、色々BlazeDS側で対応されてない部分も対応してくれてるらしい)

なお、AMF48(えーえむえふふぉーてぃーえいと)の使い方は公式サイトを参照ください。

なぜSpring経由で?

SpringMVCを利用すると、簡単にデータのバインディングを行う事が出来ます。
つまりHTTPリクエストをBeanにバインドしたり、
Beanの内容をHTTPリクエストに変換してViewに送出する事が出来ます。

Springの便利な部分は、これらのバインディグを様々なものに変換する事が簡単に出来る事です。
Spring自身によって提供されている形式としては、JSONや、XML、RSS、それだけはなくPDFやExcelにも対応していたはずです。
これらの実装はHttpMessageConverterを拡張されてそのコンバータが提供されていますので、
これを拡張する形でAMFに対応したものを構築できるはずです。

AMF48は便利なのですが、自力でdecode/encodeする必要があってちょっぴりめんどいので、
その部分はSpringさんに任せてしまえという思惑。

HttpMessageConverterを拡張する

JSONのコンバータとして提供されている、MappingJacksonHttpMessageConverterが
同じような事をやっているので参考になります。

実装するのは、HttpMessageConverterを実装している抽象クラス、AbstractHttpMessageConverterです。
とりあえずソースをば。詳細はコメントを見てください。
なお、細かい例外処理などは省いていますのであしからず。

public class AMFHttpMessageConverter extends AbstractHttpMessageConverter<Object> {
	public static final Charset DEFAULT_CHARSET = Charset.forName("UTF-8");

	public AMFHttpMessageConverter() {
		// 対応するメディアタイプを指定します。AMFの場合はapplicaiton/x-amf
		super(new MediaType("application", "x-amf", DEFAULT_CHARSET));
	}

	@Override
	public boolean canRead(Class<?> clazz, MediaType mediaType) {
		// 読み込み(AMF -> オブジェクト形式)が読み込み可能な場合trueを返します
		// とりあえずMediaTypeで判断。
		return checkMediaTypeAMF(mediaType);
	}

	@Override
	public boolean canWrite(Class<?> clazz, MediaType mediaType) {
		// 書き込み(オブジェクト -> AMF)が書き込み可能な場合trueを返します。
		// とりあえずMediaTypeで判断。
		return checkMediaTypeAMF(mediaType);
	}

	@Override
	protected boolean supports(Class<?> clazz) {
		// 利用禁止。canRead/canWriteを参照するため。
		throw new UnsupportedOperationException();
	}

	@Override
	protected Object readInternal(Class<?> clazz, HttpInputMessage inputMessage)
			throws IOException, HttpMessageNotReadableException {
		// AMF48を利用してdecode
		return AmfUtil.decode(inputMessage.getBody(), clazz);
	}

	@Override
	protected void writeInternal(Object o, HttpOutputMessage outputMessage)
			throws IOException, HttpMessageNotWritableException {
		// AMF48を利用してencode
		outputMessage.getBody().write(AmfUtil.encode(o));
	}

	private boolean checkMediaTypeAMF(MediaType mediaType) {
		return "application".equals(mediaType.getType())
				&& "x-amf".equals(mediaType.getSubtype());
	}
}

ちなみにこいつらは、org.springframework.web.bind.annotation.support.HandlerMethodInvoker
から呼び出されています。興味あればどうぞ。

AnnotationMethodHandlerAdapterの設定

作成したAMFHttpMessageConverterをDI上にロードします。
これらのConverterはAnnotationMethodHandlerAdapterで読み込まれるので、
このクラスのプロパティにインジェクションします。設定ファイルはこんな感じです。

<beans:bean id="annotationMethodHandlerAdapter" class="org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter">
  <beans:property name="messageConverters">
	<beans:array>
		<beans:bean class="com.wordpress.prepro.AMFHttpMessageConverter"></beans:bean>
	</beans:array>
  </beans:property>
</beans:bean>

なお、上記のソースではAMFHttpMessageConverterのみを設定していますが、
通常はコンストラクタで設定されている

this.messageConverters = new HttpMessageConverter[]{
		new ByteArrayHttpMessageConverter(),
		stringHttpMessageConverter,
		new SourceHttpMessageConverter(),
		new XmlAwareFormHttpMessageConverter()
};

あたりも設定しておかないときっと困るでしょう〜(通常のバインディング系が動かないはず)。
また、この設定は

<annotation-driven />

よりも先に設定する必要があります(最初これではまりましたorz…)。

Controllerへの設定

そして、本題。
実際に使う場合の話。Controllerに設定を埋め込みます。

クライアントからAMF通信を受け取る場合

@RequestBodyを使います。
@RequestBodyを使うと設定したオブジェクトに対して、HTTPリクエスト本体を渡します。
この渡されたHTTP情報を元に、対応するHttpMessageConverterが処理されます。
ソースはこんな感じになります。

// HttpリクエストがhelloCommandに変換されて設定されます
@RequestMapping(value="/amf", method=RequestMethod.POST, headers="Accept=application/x-amf")
public Result home(@RequestBody HelloCommand helloCommand) throws IOException {
	//....
}

サーバからAMF形式で送る場合

@ResponseBodyを使います。
@ResponseBodyを設定すると、オブジェクトをHttpレスポンスに変換する際にHttpMessagConverterによって処理されます。

@RequestMapping(value="/amf", method=RequestMethod.POST)
@ResponseBody
public Result home(@RequestBody HelloCommand helloCommand) throws IOException {
	Result result = new Result();
	result.message = "hoge";
	return result;
	// この後resultがHTTPレスポンスに変換されます。
}

おまけ

TODO 後でソースコードはlaunchpadにUpします。

以上です。
AMF48いいですね。使いやすい感じです。

Posted in program | タグ: , , , | Leave a Comment »

Spring3の@MVCの@InitBinderの挙動に関して

Posted by hikaruworld : 2011 2月 9

Springの@Controllerにはクライアントから渡された値を任意の型に変換する、
@InitBinderというアノテーションがあります。
Spring1.Xから知っている人にはおなじみのInitBinderそのものです。

簡単にいうと、クライアント側で

2/12

という値を設定してデータが送られた場合、Date型に変換したり、
サーバからクライアントに値を送る際にDate型をDateFormatで
任意の表示形式に変換したりできます。

今回やりたかったのは、このInitBinderの値を特定のフィールド、ないしはパラメータにのみ適用する方法です。
@InitBinderには引数に値を設定出来、おそらくこの値を設定すれば良いのは分かるのですが、
この英語の説明がパッと分からなかったので、ソースを読んでいました。

実際に実行処理が行われているのは、HandlerMethodInvoker.initBinderの部分で以下のようになっています。

String[] targetNames = AnnotationUtils.findAnnotation(initBinderMethod, InitBinder.class).value();
if (targetNames.length == 0 || Arrays.asList(targetNames).contains(attrName)) {
	//ここで実処理
	...

気になるのはattrNameとtargetNamesの部分。
targetNamesはInitBinderのアノテーションで指定可能なvalue値の部分で、
何も設定していない場合は、そのまま全部のデータに対して処理が行われるようです(targetNames.length == 0)。

じゃあattrNameに何が渡されるかというのが今回の興味。

POJOを@RequestMappingの情報に渡した場合は、そのオブジェクトの値が渡されます。
つまり、以下のような設定をしていた場合、

@RequestMapping(value = "/", method = RequestMethod.GET)
public String hogehoge(Piyo piyo) {
  //...
}

attrNameにはpiyoが渡されます。
この変換ロジックはSpringのドキュメントのどこかに書いてあった気がしますが、
忘れたので割愛。

次に、@PathVariableを設定した場合はその変数名が渡されます。
こういうケースですね。この例ではabcが渡されます。

@RequestMapping(value="/{abc}", method=RequestMethod.GET)
public String home(@PathVariable("abc") int def) {
  //...
}

次に@RequestParamを利用したケースです。

@RequestMapping(value="/ab", method=RequestMethod.GET)
public String home2(@RequestParam int id) {
  logger.info("Welcome home!");
  return "home";
}

はい、言わずともがな、attrNameにはidが渡されます。

眠くなってきたのでこの辺で。

ちなみに、同名の名前があった場合はどちらにもInitBinderが適用されるようです。
これは規約ベースの書き方で素敵ですね。

Posted in program | タグ: , , | Leave a Comment »

DefaultHandlerExceptionResolverのExceptionHandlingに関する備忘録

Posted by hikaruworld : 2011 2月 9

SpringMVCでExpcetionのHandlingはorg.springframework.web.servlet.mvc.support.DefaultHandlerExceptionResolverが使われるのだけども、
これは、HttpStatusCodeによって以下の様に分類されます。

必要な時にいちいちドキュメント探すのが面倒なのでメモ。
ここに書いてあります。

Exception HTTP Status 補足
ConversionNotSupportedException 500 (Internal Server Error)
HttpMediaTypeNotAcceptableException 406 (Not Acceptable)
HttpMediaTypeNotSupportedException 415 (Unsupported Media Type)
HttpMessageNotReadableException 400 (Bad Request)
HttpMessageNotWritableException 500 (Internal Server Error)
HttpRequestMethodNotSupportedException 405 (Method Not Allowed)
MissingServletRequestParameterException 400 (Bad Request)
NoSuchRequestHandlingMethodException 404 (Not Found)
TypeMismatchException 400 (Bad Request)

ちなみに、SpringSecurityによる認証エラーは以下が返されます

AccessDeniedException 403 (Access is denied)

Posted in program | タグ: , | Leave a Comment »

SpringSecurtiyのSecurityContextを利用したクラスのテスト方法

Posted by hikaruworld : 2011 1月 26

SpringSecurityを利用するとControllerからもこんな感じで、簡単に主体情報を取得できます。

SecurityContextHolder.getContext().getAuthentication().getPrincipal();

ただし、この実装を利用した場合、
ユニットテスト時は当然主体情報を取得できません。
認証情報がありませんので。。。

そのためテストをする場合は、
その認証情報を以下のように、
TestingAuthenticationTokenとしてSecurityContextHolderに格納してあげると良いとのこと。

// User情報は通常格納する主体情報を利用します。
// SpringSecurityを利用する場合は大体UserDetailsを継承しているかと。
User user = new User() {};

// Test用の認証Tokenに格納します。
// 第1引数に実際のユーザ情報、第2引数に資格情報を設定します。
// 後者は@Securedのテストをするような場合に必要になりますが、詳しくは次回。
Authentication authentication = new TestingAuthenticationToken(principal , null);

// SecurityContextの実装クラスをインスタンス化します。
SecurityContext context = new SecurityContextImpl();
context.setAuthentication(authentication);

// staticメソッドでSecurityContextに設定します。
SecurityContextHolder.setContext(context );

実際にテストで利用する場合は@Before辺りで読むと楽です。

参考:Tapestry 5 with Spring Security – problem with unit tests.

以上です。

Posted in program | タグ: , , , | Leave a Comment »

Spring3の宣言的トランザクションが効かなくて焦るなど

Posted by hikaruworld : 2011 1月 26

原因は2つ.

<tx:annotation-driven/>の設定もれとrollback-forの設定漏れ。

<tx:annotation-driven/>に関して

@Transactionalによるアノテーションベースのトランザクション管理をする場合は、
設定ファイル上でこの設定を行う必要がある。
勘違いしたのは、mvc側のannotation-drivenと勘違いしたこと。

http://www.springframework.org/schema/mvcの設定と
http://www.springframework.org/schema/txは全く別物なので注意。

rollback-for

デフォルトでSpringのトランザクション管理をしている場合に例外が発生して、
rollbackされるのは、実行時例外だけ(つまりRuntimeExceptionを継承するクラス)。

つまり、トランザクション内で任意の例外クラスを実装していてtry…catchを強制させたいような
コードを書いている場合は、そのエラーが発生してもロールバックしてくれない。
Exceptionが発生した場合に問答無用でロールバックしたい場合はrollback-for=”java.lang.Exception”
あたりを設定しておく必要がある。

前者は自分の勘違いだから仕方がないとしても、後者の設定はちょっといやだなぁ…

Posted in program | タグ: , , | Leave a Comment »

Spring3(というかJSR-303)でBeanValidationのテスト

Posted by hikaruworld : 2011 1月 15

Spring3(というかJSR-303)でBeanValidationのテスト

Spring3にはJSR-303のBeanValidationの機能が実装されています。
これのテストの仕方について調べていました。

対象のコード

例えば以下のようなBeanがあるとします。

public class User {
  private int userId;
  private String userName;
  
  // getter/setter
}

これに、以下のような制約を加えたいとします。

userId

  • 1以上の数字
  • 9桁以内

userName

  • 20文字以内
  • 必須項目

BeanValidation実装後の修正コード

必要なアノテーションを設定すると以下の通りになります。

public class User {
  @Min(value=1)
  @Max(value=999999999)
  private int userId;
  
  @Length(max=20)
  @NotNull
  private String userName;
  
  // getter/setter
}

テストケース

テストケースは以下のようになります。
一部のテストケースだけ抜粋しています。

// Springではなくjavax側のValidatorをimport
import javax.validation.Validator;

public class UserTest {
  /** 検証対象のクラス */
  public User user;
  
  /** Validatorの実行クラス */
  private static Validator validator;
  /** 実行結果格納用 */
  private Set<ConstraintViolation<User>> violations;
  
  /**
   * validatorのインスタンスをFactoryから取得
   */
  @BeforeClass
  public static void beforeClass() {
    validator = Validation.buildDefaultValidatorFactory().getValidator();
  }

  /** 検証対象のクラスを初期化 */
  @Before
  public void before() {
    this.user = new User();
  }

  //---------------------------------------------------------------------------------------------
  //
  // Validationのテスト
  //
  //---------------------------------------------------------------------------------------------
  
  @Test
  public void userIdの最小値の検証_0を許さない() throws Exception {
    this.user.setUserId(0);
    
    this.violations = validator.validate(this.user);
    
    Map<String, Object> map = new HashMap<String, Object>();
    map.put("value", 1L);
    assertThat(this.violations, hasItems(
    		IsViolation.<User>hasViolation("userId", "{javax.validation.constraints.Min.message}", map ))
    );
  }
}

キモは、以下の部分で、validateを実行した結果、
エラーがあった場合にviolationsに格納されます。

Validator validator = Validation.buildDefaultValidatorFactory().getValidator();
Set<ConstraintViolation<User>> violations = validator.validate(this.user);

IsViolation.hasViolationは、
テスト結果がviolationsに格納されてテストしづらいので、
Matcher用のクラスを実装した任意のクラスです。

ちなみにIsViolationクラスは以下の通りです。

package com.wordpress.prepro.controller.utils;

import java.util.HashMap;
import java.util.Map;

import javax.validation.ConstraintViolation;
import javax.validation.Path;

import org.hamcrest.Description;
import org.hamcrest.Matcher;
import org.hamcrest.Matchers;
import org.hamcrest.TypeSafeMatcher;

/**
 * アノテーションフィールドを用いたバリデーションで利用されるMatcherクラス.
 * 未テスト
 * 
 * <h1>利用方法</h1>
 * 以下のようなクラスがあった場合
 * <pre>
 * pubic class User {
 *      @Length(max = "3")
 *      private String title;
 * }
 * </pre>
 * 以下のようにユニットテスト側で実行時に利用される。
 * <pre>
 * User user = new User();
 * user.setTitle("hoge");
 * 
 * Validator validator = Validation.buildDefaultValidatorFactory().getValidator();
 * Set&lt;ConstraintViolation&lt;User&gt;&gt; violations = validator.validate(user);
 * 
 * assertThat(violations, hasItem(IsAnnotationValidate.&lt;User&gt;hasViolation(
 *  "title", "{org.hibernate.validator.constraints.Length.message}")
 * )));
 * </pre>
 *
 * @author morikawa
 * @version 0.0.1 
 *
 * @param <T> アノテーションが付与されているBean
 */
public class IsViolation<T> extends TypeSafeMatcher<ConstraintViolation<T>> {
  
  /** 検証対象フィールド */
  private String propertyField;
  /**
  * エラーメッセージ
  * ローカライズされていないkeyを検証する
  */
  private String messageTemplate;
  /** アノテーションのフィールド検証用 */
  private Map<String, Object> annotationValue;
  
  /**
  * コンストラクタ
  * @param propertyField 検証対象フィールド
  * @param messageTemplate エラーメッセージ
  * @param annotationValue アノテーションに設定された値。ValueのObjectはkeyに依存する
  */
  public IsViolation(String propertyField, String messageTemplate, Map<String, Object> annotationValue) {
    this.propertyField = propertyField;
    this.messageTemplate = messageTemplate;
    this.annotationValue = annotationValue;
  }
  
  /**
  * {@inheritDoc}
  * @see org.hamcrest.SelfDescribing#describeTo(org.hamcrest.Description)
  */
  public void describeTo(Description description) {
    description.appendText("a annotation validate ")
        .appendText("PropertyPath:")
        .appendValue(this.propertyField)
        .appendText(" ")
        .appendText("MessageTempate:")
        .appendValue(this.messageTemplate)
        .appendText(" ")
        .appendText("annotationValue:")
        .appendValue(this.annotationValue);
  }
  
  /**
   * {@inheritDoc}
   * @see org.hamcrest.TypeSafeMatcher#matchesSafely(java.lang.Object)
   */
  @Override
  protected boolean matchesSafely(ConstraintViolation<T> item) {
    if (item == null) {
      return false;
    }
    // PropertyPath
    if (item.getPropertyPath() == null) {
      return false;
    }
    boolean msgField = false;
    for (Path.Node node : item.getPropertyPath()) {
      if (node.getName().equals(this.propertyField)) {
        msgField = true;
        break;
      }
    }
    
    // messageTemplate
    boolean msgTemp = this.messageTemplate.equals(item.getMessageTemplate());
    
    // annotation
    Map<String, Object> atrMap = item.getConstraintDescriptor().getAttributes();
  
    boolean atr = true;;
    for (Map.Entry<String, Object> entry : this.annotationValue.entrySet()) {
      if (atrMap.containsKey(entry.getKey())) {
        if (Matchers.is(entry.getValue()).matches(atrMap.get(entry.getKey()))) {
          continue;
        }
        atr = false;
        break;
      }
      atr = false;
      break;
    }
    return msgField && msgTemp && atr;
  }

  /**
   * 
   * @param <T> 検証対象のPOJO
   * @param propertyField 検証対象フィールド
   * @param messageTemplate エラーメッセージ
   * @param annotationValue アノテーションに設定された値。ValueのObjectはkeyに依存する
   * @return {@link IsViolation}のインスタンス
   * @see IsViolation
   */
  public static <T> Matcher<ConstraintViolation<T>> hasViolation(String propertyField, String messageTemplate, Map<String, Object> annotationValue) {
    return new IsViolation<T>(propertyField, messageTemplate, annotationValue);
  }

  /**
   * Annotationプロパティ検証の省略版
   * @see IsViolation#hasValidate(String, String, String, Map)
   */
  public static <T> Matcher<ConstraintViolation<T>> hasViolation(String propertyField, String messageTemplate) {
    return new IsViolation<T>(propertyField, messageTemplate, new HashMap<String, Object>());
  }	
}

以上です。

Posted in program | タグ: , , , , | Leave a Comment »

SpringSecurityのMethodSecurityとFilterSecurityではまる

Posted by hikaruworld : 2011 1月 10

結論は、Interceptorを行うFIlterの実装が異なっていた事でした。

現在SpringSecurityを使って設定する場合には、
階層構造定義を以前設定したように行っています。

ROLE_ADMIN > ROLE_LEADER
ROLE_LEADER > ROLE_USER

ただ、この設定でメソッドレベルアノテーションに@Securedを設定した場合に、
上手く行きませんでした。

@Secured(value = "ROLE_LEADER")
public String hoge() {
	return "hoge";
}

ロギングを有効にして(org.springframework.security -> debug)確認してみると、
org.springframework.security.access.vote.RoleHierarchyVoterと、
org.springframework.security.access.vote.RoleVoterがなぜか呼ばれています。

RoleHierarchyVoterは自信で階層構造を設定しているので、
利用されていて当然なんですが、RoleVoterが謎。

ちょっとデバッグしてみると、
FilterSecurityInterceptorとMethodSecurityInterceptorに注入されています。

つまり、Filterレベル(主として認証)の実装は指定した物を参照しているが、
Methodレベル(主として認可)の実装がデフォルト値が利用されているということでしょうか。

というわけで、こんな感じに設定したら解決しました。

<!-- accessDecisionManagerは任意に設定したアクセス管理のインスタンス -->
<global-method-security secured-annotations="enabled" access-decision-manager-ref="accessDecisionManager"/>

うーん。Springの空気読みっぷりを読まなかった自分が悪いなぁと、痛感。

以上です。

Posted in program | タグ: , , | Leave a Comment »

SpringのexceptionResolver設定時の挙動

Posted by hikaruworld : 2010 12月 28

ちょっと勘違いしていてハマったもので。
これで問題ないかは自信なし…

SpringのExceptionResoloverは起動時にBeanFactoryを舐めて、
HandlerExceptionResolverに連なる実装クラスを自動でロードしてくれるみたいです。

但し、exceptionResoloverを定義した場合、
つまり明示的にHandlerExceptionResolverを継承したクラスを読み込んだ場合、
exceptionResolverで定義したインスタンスしか読み込みしなくなるみたいですね。

というわけで、Spring3.0で追加された各種ExceptionHandler
(DefaultHandlerExceptionResolver,AnnotationMethodHandlerExceptionResolver,ResponseStatusExceptionResolver)
と併用して利用したい場合は、こんな感じで設定しておけばOKみたいです。
※何も設定しない場合、上記3種のExceptionHandlerはDispatcherServlet読み込み時にデフォルトで有効になっています。

余計なことするなってことですかね。

デフォルト設定とSimpleMappingExceptionResoloverを有効にする

<!-- デフォルトの設定を明示的に読み込む -->
<bean name="defaultHandlerExceptionResolver" class="org.springframework.web.servlet.mvc.support.DefaultHandlerExceptionResolver" />
<bean name="annotationMethodHandlerExceptionResolver" class="org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerExceptionResolver" />
<bean name="responseStatusExceptionResolver" class="org.springframework.web.servlet.mvc.annotation.ResponseStatusExceptionResolver" />
<!-- SimpleMappingExceptionResoloverを読み込む -->
<bean id="simpleMappingExceptionResolver" class="org.springframework.web.servlet.handler.SimpleMappingExceptionResolver">
	<!-- 適当に設定を書く -->
	<property name="defaultErrorView"  value="/errors/error"/>
</bean>

ちょっとソースを

ちなみに、DispatcherServlet側の読み込み処理はこんな感じです。

private void initHandlerExceptionResolvers(ApplicationContext context) {
	this.handlerExceptionResolvers = null;

	if (this.detectAllHandlerExceptionResolvers) {
		// Find all HandlerExceptionResolvers in the ApplicationContext, including ancestor contexts.
		Map<String, HandlerExceptionResolver> matchingBeans = BeanFactoryUtils
				.beansOfTypeIncludingAncestors(context, HandlerExceptionResolver.class, true, false);
		if (!matchingBeans.isEmpty()) {
			this.handlerExceptionResolvers = new ArrayList<HandlerExceptionResolver>(matchingBeans.values());
			// We keep HandlerExceptionResolvers in sorted order.
			OrderComparator.sort(this.handlerExceptionResolvers);
		}
	}
	else {
		try {
			HandlerExceptionResolver her =
					context.getBean(HANDLER_EXCEPTION_RESOLVER_BEAN_NAME, HandlerExceptionResolver.class);
			this.handlerExceptionResolvers = Collections.singletonList(her);
		}
		catch (NoSuchBeanDefinitionException ex) {
			// Ignore, no HandlerExceptionResolver is fine too.
		}
	}

	// Ensure we have at least some HandlerExceptionResolvers, by registering
	// default HandlerExceptionResolvers if no other resolvers are found.
	if (this.handlerExceptionResolvers == null) {
		this.handlerExceptionResolvers = getDefaultStrategies(context, HandlerExceptionResolver.class);
		if (logger.isDebugEnabled()) {
			logger.debug("No HandlerExceptionResolvers found in servlet '" + getServletName() + "': using default");
		}
	}
}

以下、備忘録的に補足。

明示的に指定した場合

<bean name="defaultHandlerExceptionResolver" class="org.springframework.web.servlet.mvc.support.DefaultHandlerExceptionResolver" />

上記の設定だけを行った場合、L6でmatchingBeansが設定され、handlerExceptionResolversが設定されるため、デフォルト値が読み込まれません。

何も設定しなかった場合(デフォルト値)

L27のhandlerExceptionResolversがnullのため、

this.handlerExceptionResolvers = getDefaultStrategies(context, HandlerExceptionResolver.class);

で、デフォルトのexceptionResoloverが設定されます。

以上です。

Posted in program | タグ: , | Leave a Comment »

SpringSecurity3で権限の階層構造を定義する

Posted by hikaruworld : 2010 12月 24

参考ページ

バージョンはSpringSecurity3.0.5です。

SpringSecurityでは、認証と認可の設定を比較的簡易に設定することが可能ですが、
実際に運用する場合、権限を包括するような設定を行いたくなります。
つまり、以下のように、上位の権限が下位の権限を包括するようなケースです。
管理者 > ユーザ > ゲスト

この仕組みはRoleHierarchyVoterを利用することで設定することが可能です。

認証の仕組みに関して

Springにおける認可の仕組みを簡単に書いておきます。
※MethodSecurtiyのAccessDecisionManagerの認証処理のみです。

特定のユーザで認証処理が通った場合、そのユーザの持つ権限で
該当画面にアクセスできるか認可の処理が実行されます。
その遷移は以下の絵のようになります。

つまり、

  1. 特定ユーザでアクセスする
  2. 1.のもつユーザ権限が該当画面に適切か投票処理(Voter)が行われる。
    Voterが複数ある場合はそれぞれ認可処理が検証される
  3. 2で実行された投票処理の結果をAccessDecisionManagerで判定する。
    AccessDecisionManagerはデフォルトでは、3パターン(いずれか一つがOK,全てOK,Voter単位での多数決)準備されており、任意に設定可能
  4. 3の結果問題なければ画面が表示され、問題あれば403が返される。
  5. 実際の構成

    サンプルプロジェクトはここにUPしています。

    以下、セキュリティ設定のみ抜粋しています。

    <bean id="roleHierarchy" class="org.springframework.security.access.hierarchicalroles.RoleHierarchyImpl">
    	<property name="hierarchy">
    		<value>
    			ROLE_ADMIN > ROLE_USER
    			ROLE_USER > ROLE_GUEST
    		</value>
    	</property>
    </bean>
    <bean id="roleHierarchyVoter" class="org.springframework.security.access.vote.RoleHierarchyVoter">
    	<constructor-arg ref="roleHierarchy" />
    </bean>
    
    <bean id="accessDecisionManager" class="org.springframework.security.access.vote.UnanimousBased">
    	<property name="decisionVoters">
    		<list>
    			<ref bean="roleHierarchyVoter"/>
    			<bean class="org.springframework.security.access.vote.AuthenticatedVoter" />
    		</list>
    	</property>
    </bean>
    

    今回はROLE_ADMIN > ROLE_USER > ROLE_GUESTと階層構造で権限を持たせています。
    まずは、accessDecisionManagerにUnanimousBased(いずれか一つのVoterがOKであればよい)を設定しています。
    そのdecisionVotersとして、AuthenticatedVoterとRoleHierarchyVoterを定義しています。

    RoleHierarchyVoterはコンストラクタインジェクションを用いてRolehierarchyImplを定義しています。

    //org.springframework.security.access.vote.RoleHierarchyVoter
        private RoleHierarchy roleHierarchy = null;
    
        public RoleHierarchyVoter(RoleHierarchy roleHierarchy) {
            Assert.notNull(roleHierarchy, "RoleHierarchy must not be null");
            this.roleHierarchy = roleHierarchy;
        }
        /**
         * Calls the <tt>RoleHierarchy</tt> to obtain the complete set of user authorities.
         */
        @Override
        Collection<GrantedAuthority> extractAuthorities(Authentication authentication) {
            return roleHierarchy.getReachableGrantedAuthorities(authentication.getAuthorities());
        }
    }
    

    RoleHierarchyVoterが実際に認証処理い利用されるのがRoleHierarchyのため、
    この値をRoleHierarchyImplを定義して、プロパティとしてhierarchyを定義します。
    hierarchyは > で各権限を列挙します。

    おまけ

    なお、IS_AUTHENTICATED_FULLYや、
    IS_AUTHENTICATED_ANONYMOUSLYを
    認証処理で利用している場合は、AuthenticatedVoterも
    Voterに入れてあげる必要があります。

    入れてあげないと、

    org.springframework.beans.factory.BeanCreationException: Error creating bean with name ‘org.springframework.security.web.access.intercept.FilterSecurityInterceptor#0’: Invocation of init method failed; nested exception is java.lang.IllegalArgumentException: Unsupported configuration attributes: [IS_AUTHENTICATED_FULLY, IS_AUTHENTICATED_ANONYMOUSLY]

    といった感じで怒られます。
    これは、ここにもあるようにこれらの定義がAuthenticatedVoterで定義されているためです。

    // org.springframework.security.access.vote.AuthenticatedVoter
    
    //~ Static fields/initializers =====================================================================================
    
    public static final String IS_AUTHENTICATED_FULLY
                      = "IS_AUTHENTICATED_FULLY";
    public static final String IS_AUTHENTICATED_REMEMBERED
                      = "IS_AUTHENTICATED_REMEMBERED";
    public static final String IS_AUTHENTICATED_ANONYMOUSLY
                      = "IS_AUTHENTICATED_ANONYMOUSLY";
    

    以上です。

Posted in program | タグ: , , , | Leave a Comment »