Grails 2.4で追加されたdoWithSpring、doWithConfigについて

※この記事はGrails 2.4.3を元に記述しています

Grails 2.4からdoWithSpringdoWithConfigといったユニットテストの仕組みが導入された。

これを使うとユニットテスト内でSpringビーンを定義したり、コンフィグの値を変更したりできる。

doWithSpring、doWithConfigの使い方

まずは実際の使用例から。

import grails.test.mixin.TestMixin
import grails.test.mixin.support.GrailsUnitTestMixin
import spock.lang.Specification

@TestMixin(GrailsUnitTestMixin)
class DoWithSpringDoWithConfigSpec extends Specification {

    static doWithSpring = {
        myService(MyService)
    }

    static doWithConfig(ConfigObject config) {
        config.myConfigValue = 'Hello'
    }

    def "doWithSpringとdoWithConfigの動作確認"() {
        expect:
            grailsApplication.mainContext.getBean('myService') != null
            config.myConfigValue == 'Hello'
    }
}

doWithSpringstaticな変数として定義し引数にクロージャを取る。 クロージャ中では、SpringビーンDSLを使ってビーンを定義できる。

doWithConfigは変数ではなくメソッドとして定義する(doWithSpringとの統一性を考えるとクロージャでも良かった気がするのだが...)。 このメソッドの引数にはConfigObjectインスタンスが渡されるので、このインスタンスを操作することでコンフィグの変更ができる。

doWithSpringdoWithConfigの実行順はdoWithConfigdoWithSpringの順番になる。 そのため、ビーンの初期化処理でコンフィグの値に依存していたとしてもうまく機能する。

FreshRuntime

ConfigObjectやGrailsApplicationを含むアプリケーションコンテキストの初期化はスペッククラス内で1度だけ実行される。 Spockでキーワードで言えば@SharedsetupSpecで構築されたフィクスチャのように考えれば良い。

もしフィーチャ(テスト)メソッドごとにアプリケーションコンテキストの初期化をしたい場合はgrails.test.runtime.FreshRuntimeアノテーションを使う。

@FreshRuntime
@TestMixin(GrailsUnitTestMixin)
class DoWithSpringDoWithConfigSpec extends Specification {
...

FreshRuntimeアノテーションは上記のようにクラスレベルに設定するか、フィーチャメソッドに設定できる。 ただし、現状はこの問題によりクラスレベルに設定してもうまく動作しない。 この問題は2.4.4で修正される(2014/10/15時点の最新バージョンは2.4.3)。

モックをSpringビーンとして登録する

先ほどのFreshRuntimeアノテーションorg.codehaus.groovy.grails.commons.InstanceFactoryBeanを使うとSpockのモックをSpringビーンとして登録することができる。 FreshRuntimeアノテーションと組み合わせる必要があるのは、恐らくフィーチャメソッド間でモックを共有してはならないからだと思う。 Spockではインタラクションを持つモックは特定のフィーチャメソッドへの参照をもっているため、static、または@Sharedを使って共有してはならない。 そのため、モックをSpringビーンとして登録する場合も、FreshRuntimeアノテーションを使ってフィーチャメソッドごとに初期化する必要があるのだろう。

モックをSpringビーンとして登録したい状況としては、テスト対象の背後で動作しているSpringビーンのインスタンスをモックに差し替えたい場合が考えられる。 例えば次のようなコントローラがあったとする。

import grails.compiler.GrailsCompileStatic

@GrailsCompileStatic
class MyController {

    MyService myService

    def index() {
        render myService.hello()
    }
}

このコントローラのindexアクションでは、このコントローラにDIされるmyServiceに処理を委譲している。 このコントローラをテストしようとした時、myServiceをモックに差し替えたい場合がある。 これを、次のように記述できる。

import grails.test.mixin.TestFor
import grails.test.runtime.FreshRuntime
import org.codehaus.groovy.grails.commons.InstanceFactoryBean
import spock.lang.Specification

@FreshRuntime
@TestFor(MyController)
class MockedBeanSpec extends Specification {

    def myService = Mock(MyService)

    def doWithSpring = {
        myService(InstanceFactoryBean, myService, MyService)
    }

    def "コントローラの背後で動作するサービスをモックに差し替える"() {
        when:
            controller.index()

        then:
            response.text == 'hello'

        and:
            1 * myService.hello() >> { "hello" }
    }
}

まずはじめにdef myService = Mock(MyService)でSpockのモックを定義している。 次にmyService(InstanceFactoryBean, myService, MyService)といったように、InstanceFactoryBeanのコンストラクタの引数に、このモックのインスタンスとそのクラスの型を指定する。 これで生成したモックを使ってmyServiceという名前のSpringビーンを登録できる。

resources.groovyに定義したビーンをユニットテストで使用する

GrailsではSpringビーンDSLを使って、grails-app/conf/spring/resources.groovyにビーンの定義ができる。 インテグレーションテストや、Grailsのアプリケーションが起動する場合はこのファイルに定義したビーンが自動的に登録される。 しかし、ユニットテストでは自動的には登録されない。

これをstatic loadExternalBeans = trueという設定をスペッククラスに追加することで、自動的にビーンが登録されるようになる。

import grails.test.mixin.TestFor
import spock.lang.Specification

@TestFor(MyController)
class LoadingApplicationBeansSpec extends Specification {

    static loadExternalBeans = true

    ...
}

そんなとこで。

Grailsの追加、更新、削除処理について深く考える

※この記事はGrails 2.4.2をベースに記述しています

一見簡単にみえるが、意外と奥が深い。 次のようなドメインクラスがあったとする。

class Person {
    String username
    static constraints = {
        username unique: true
    }
}

この時、デフォルトのスキャフォルドは次のようなコントローラを生成する。

@Transactional(readOnly = true)
class PersonController {
    ...
    @Transactional
    def save(Person personInstance) {
        if (personInstance == null) {
            notFound()
            return
        }

        if (personInstance.hasErrors()) {
            respond personInstance.errors, view:'create'
            return
        }

        personInstance.save flush:true

        request.withFormat {
            form multipartForm {
                flash.message = message(code: 'default.created.message', args: [message(code: 'person.label', default: 'Person'), personInstance.id])
                redirect personInstance
            }
            '*' { respond personInstance, [status: CREATED] }
        }
    }

    @Transactional
    def update(Person personInstance) {
        if (personInstance == null) {
            notFound()
            return
        }

        if (personInstance.hasErrors()) {
            respond personInstance.errors, view:'edit'
            return
        }

        personInstance.save flush:true

        request.withFormat {
            form multipartForm {
                flash.message = message(code: 'default.updated.message', args: [message(code: 'Person.label', default: 'Person'), personInstance.id])
                redirect personInstance
            }
            '*'{ respond personInstance, [status: OK] }
        }
    }

    @Transactional
    def delete(Person personInstance) {
        if (personInstance == null) {
            notFound()
            return
        }

        personInstance.delete flush:true

        request.withFormat {
            form multipartForm {
                flash.message = message(code: 'default.deleted.message', args: [message(code: 'Person.label', default: 'Person'), personInstance.id])
                redirect action:"index", method:"GET"
            }
            '*'{ render status: NO_CONTENT }
        }
    }
    ...
}

save/update/deleteメソッドがそれぞれ追加/更新/削除の処理になる。このコードをベースに説明する。

ドメインクラスをアクションの引数とした場合の動作

save/update/deleteメソッドは引数にPersonクラスの値を取るように定義されいる。

def save(Person personInstance)
def update(Person personInstance)
def delete(Person personInstance)

これはGrails 2.3から追加された機能で、アクションメソッドの引数にドメインクラスを指定すると

  1. パラメータ中にidがあった場合、自動的にデータがロードされる (ない場合は、新規にインスタンスが生成される)
  2. リクエストパラメータがある場合は、自動的にデータがバインドされる
  3. 自動的にバリデートが行われる

といったことが行われる。

  1. は非常に便利だが、柔軟性にもかける。このロジックはidパラメータがあった場合は、ドメインクラスのgetメソッドを使ってデータを取得するという単純なものである。この取得ロジックをカスタマイズできると非常に便利だが、現時点ではない。そのため、idパラメータ以外の独自のデータ取得ロジックを実装したい場合は、アクションメソッドの引数にドメインクラスを指定することを諦める必要がある。

  2. については自動的にバインドされるためbindDataなどを使った時のように、バインドするプロパティを指定することはできない。そのため、ドメインクラスのconstraintsでbindableを使ったmass assignment脆弱性の対応をしっかりしておく必要がある。

  3. も一見便利なようだが、冗長なバリデーションを引き起こす。バリデーションのコストが低い場合はあまり気にしなくても良いかもしれないが、データベースにアクセスするようなバリデーションが含まれている場合は注意する必要がある。

まず、deleteメソッドではバリデーションは不要だろう (これはバグと言ってもいいかもしれないが)。バインドするパラメータが存在しないからである。

save/updateについてはバリデーションが必要のため、問題ないように思える。ただ、このあとにsaveメソッドでデータを保存していることが問題になる。

@Transactional
def save(Person personInstance) {
    …
    personInstance.save flush:true
    ...
}

@Transactional
def update(Person personInstance) {
    …
    personInstance.save flush:true
    ...
}

saveは自動的にバリデーションを実行する。そのため、saveを実行したときにバリデーションエラーになることを考慮しなければならない。はじめにバリデーションを通過済みだからといって、途中で値を変更したり、データベースへのアクセスがある制約の場合は他のトランザクションが完了することにより、saveを実行した時に必ずバリデーションを通過するとは限らない。

またsave複数回バリデーションを実行することにも注意する必要がある。これはドメインクラスで用意されているイベントフックの仕組みで値が変更された場合を考慮したものである。

そのため、上記の例で言うとsave/updateメソッドはそれぞれ合計で3回のバリデーションが実行される。この例のPersonクラスはusernameにユニーク制約をつけているので、バリデーションでデータベースへSELECT文が発行され、計3回のSELECT文が発行されることになる。

この問題を回避するかはどうかは、どこまで最適化が必要かによるが、

  • アクションの引数にドメインクラスを指定するのをやめて、自前で取得する
  • イベントフックで値が更新されない前提であれば、自前でvalidateを呼んで、saveの時はバリデーションを無効にする

という案がある。

@Transactional
def save() {
    def person = new Person()
    bindData person, request
    if (!person.validate()) {
        respond person.errors, view: 'create'
        return
    }
    person.save validate: false, flush: true
    ...
}

@Transactional
def update(Long id) {
    def person = Person.get id
    if (!person) {
        notFound()
        return
    }

    bindData person, request
    if (!person.validate()) {
        respond person.errors, view: 'edit'
        return
    }
    person.save validate: false, flush: true
    ...
}

@Transactional
def delete(Long id) {
    def person = Person.get id
    if (!person) {
        notFound()
        return
    }

    person.delete flush: true
    ...
}

だが、person.save validate: falseなどは、通常はやり過ぎた最適化なので、ユニーク制約がないなどバリデーションのコストが低い場合は単に

if (!person.validate()) {
    respond person.errors, view: 'create'
    return
}
person.save validate: false, flush: true

if (!person.save(flush: true)) {
    respond person.errors, view: 'create'
    return
}

でよいだろう。または、バインド後にバリデーションエラーになる可能性がない場合は

@Transactional
def save(Person personInstance) {
    ...
    personInstance.save validate: false, flush:true
    ...
}

とする手もあるだろう。

楽観的排他制御

デフォルトのスキャフォルドが生成するコードには楽観的排他制御の考慮がない。

Grailsではドメインクラスに楽観的排他制御を行うためのversionプロパティが追加される。このプロパティを使って楽観的排他制御をするには2つの考慮が必要がである。

  • 変更画面にhiddenなどでバージョンの値を仕込んでおき、ポストされたときにデータベースのバージョンの値と比較する
  • Hibernateが楽観的排他エラーを検出したときのハンドリングをする

Hibernateによる楽観的排他制御ではユーザが設定したバージョンの値は完全に無視される。Grailsversionプロパティをバインド可能にして、あとはHibernateに任せておけば簡単に楽観的排他制御ができそうだが、恐らくセキュリティ上の理由から残念ながらそうはなっていない。

そのため、前者の考慮はユーザ自身でやらなければならない。Grails 2.2までのデフォルトのスキャフォルドでは考慮されていたのだが、なぜか2.3でREST対応のテンプレートになった時になくなってしまった。

対処は次のようにデータベースのversionの値と、ユーザから送られてきたversionの値を比較すればよい。

@Transactional
def update(Long id, Long version) {
    def person = Person.get id
    if (!person) {
        notFound()
        return
    }

    if (person.version > version) {
        person.errors.rejectValue('version', 'default.optimistic.locking.failure', [message(code: 'person.label', default: 'Person')] as Object[], 'Another user has updated this Person while you were editing')
        respond person.errors, view: 'edit'
        return
    }

    bindData person, request
    if (!person.validate()) {
        respond person.errors, view: 'edit'
        return
    }
    person.save validate: false, flush: true
    ...
}

必ずbindDataを実行する前といったように、データベースから取得したpersonの値を変更する前にチェックを入れる必要がある。これは、データをバインドしたあとに比較したら同じバージョンの値を比較してしまうからではない。Grailsのデータバインディングではデフォルトでversionプロパティがバインディングの対象外になっている。

問題はこのアクションメソッドに@Transactionalが設定されていることだ。このアノテーションGrails 2.3から追加された新しいトランザクションアノテーションである。以前はサービスクラス限定だったが、新しいトランザクションアノテーションはコントローラなど、どのような場所で使えるようになっている。

@Transactionalが指定されたメソッドの中で、データベースにすでに保存されている (Merged状態の) ドメインクラスのインスタンスの変更を行うと、明示的にsaveを呼びださなくても、メソッドが終了する時点でインスタンスへの変更がフラッシュされ、トランザクションがコミットされる。トランザクションロールバックするには、非検査例外をメソッドからスローするなどしなければならない。

上記のアクションメソッドは、バージョンチェックを行ったあとに、単にメソッドをreturnで終了しているだけである。そのため、このアクションメソッドは、実際にはトランザクションがコミットされて終了している。そのため、この前になにか値を変更すると、その変更は自動的にコミットされてしまうのだ。

次はHibernateの楽観的排他エラーの対処である。Hibernateは楽観的排他エラーを検出すると、org.hibernate.StaleObjectStateExceptionをスローする。ただ、Springを経由してHibernateを使っている場合は、Springが例外をラップして再スローするため、実際にはユーザにはorg.springframework.dao.OptimisticLockingFailureExceptionのサブクラスの例外がスローされる (参考: http://d.hatena.ne.jp/yomama/20070802)

この例外はsaveの呼び出し時にキャッチしてハンドリングすればよい。

@Transactional
def update(Long id, Long version) {
    def person = Person.get id
    if (!person) {
        notFound()
        return
    }

    if (person.version > version) {
        person.errors.rejectValue('version', 'default.optimistic.locking.failure', [message(code: 'person.label', default: 'Person')] as Object[], 'Another user has updated this Person while you were editing')
        respond person.errors, view: 'edit'
        return
    }

    bindData person, request
    if (!person.validate()) {
        respond person.errors, view: 'edit'
        return
    }

    try {
        person.save validate: false, flush: true
    } catch (OptimisticLockingFailureException e) {
        person.errors.rejectValue('version', 'default.optimistic.locking.failure', [message(code: 'person.label', default: 'Person')] as Object[], 'Another user has updated this Person while you were editing')
        respond person.errors, view: 'edit'
        return
    }
    ...
}

このOptimisticLockingFailureExceptionは更新/削除時にデータが存在しなかった場合にもスローされる。

def person = Person.get id
if (!person) {
    notFound()
    return
}

でチェック済みのように思えるが、この処理のあとに別のトランザクションでデータが削除されている場合があるからだ。 楽観的排他エラーの場合も、データが存在しない場合もOptimisticLockingFailureExceptionがスローされるため、状況に応じて処理を分岐させることは難しいが、そうそう同時にトランザクションが実行されることがないなら、そのあたりは割りきりで良いだろう。もし、割り切れない場合は、データ取得時に悲観的ロックを取得して、同時に処理が走らないようにするなどの考慮が必要になる。

また、データが存在しない場合の考慮はdeleteメソッドでもする必要ある。

@Transactional
def delete(Long id) {
    def person = Person.get id
    if (!person) {
        notFound()
        return
    }

    try {
        person.delete flush: true
    } catch (OptimisticLockingFailureException e) {
        notFound()
        return
    }
    ...
}

一意制約違反の考慮

Grailsではusername unique: trueといったようなにユニーク制約をつけると、バリデーション実行時にデータベースにSELECT文を発行して制約違反がないかを確認する。そのため通常は一意制約違反の場合はバリデーションエラーになる。

しかし、このチェックは同時に複数トランザクションが同時に走った場合、ともにSELECT文でのチェックをパスしてしまう可能性ある。このような場合は、そのあとのINSERT文や、UPDATE文を発行した時点でデータベース上で一意制約違反となる。

Hibernateはこのエラーを検出した場合、org.hibernate.exception.ConstraintViolationExceptionをスローするが、先ほどの楽観的排他制御と同様にSpringがorg.springframework.dao.DataIntegrityViolationExceptionのサブクラスでラップするため、アプリケーションではこの例外をキャッチする。

一意制約違反は、追加/更新時に考慮する必要があるため、save/updateメソッドで対応を行う。

@Transactional
def save() {
    def person = new Person()
    bindData person, request
    if (!person.validate()) {
        respond person.errors, view: 'create'
        return
    }

    try {
        person.save validate: false, flush: true
    } catch (DataIntegrityViolationException e) {
        person.errors.rejectValue('username', 'default.not.unique.message', ['username', message(code: 'person.label', default: 'Person'), person.username] as Object[], 'Username must be unique')
        respond person.errors, view: 'create'
        return
    }
    ...
}

@Transactional
def update(Long id, Long version) {
    ...
    try {
        person.save validate: false, flush: true
    } catch (OptimisticLockingFailureException e) {
        person.errors.rejectValue('version', 'default.optimistic.locking.failure', [message(code: 'person.label', default: 'Person')] as Object[], 'Another user has updated this Person while you were editing')
        respond person.errors, view: 'edit'
        return
    } catch (DataIntegrityViolationException e) {
        person.errors.rejectValue('username', 'default.not.unique.message', ['username', message(code: 'person.label', default: 'Person'), person.username] as Object[], 'Username must be unique')
        respond person.errors, view: 'edit'
        return
    }
    ...
}

これで動くと言いたいところだが、saveメソッドで実際にDataIntegrityViolationExceptionが発生する状況を発生されると以下のエラーが発生してしまう (何故かupdateメソッドやOptimisticLockingFailureExceptionの場合は発生しないのだが...) 。

2014-07-27 14:55:01,417 [http-bio-8080-exec-5] ERROR StackTrace  - Full Stack Trace:
org.hibernate.AssertionFailure: null id in Person entry (don't flush the Session after an exception occurs)
...

これは@Transactionalをつけているため、ロールバック処理をしてあげなければならないのだろう。Springの@TransactionアノテーションではTransactionAspectSupportを経由して、TransactionStatusのインスタンスを取得し、setRollbackOnly()を呼び出すことで、明示的にトランザクションロールバックとして扱うことができる。しかし、新しいGrails@Transactionalアノテーションではこの手段が使えない。そのため、トランザクションロールバックするには、先ほど説明したようにアクションメソッドを非検査例外で終了しなければならない (裏技としてAST変換で追加されるtransactionStatusという変数を使ってtransactionStatus.setRollbackOnly()とすることもできるが、公開されたAPIではないのでお勧めしない)。

非検査例外をスローすれば良いのだが、これはコントローラではアクションごとに画面遷移を制御しなければならないため、非検査例外をスローしてしまうと扱いが難しくなる。遷移先の情報を詰め込んで例外をスローし、ロールバック後にGrails 2.3から追加されたコントローラでの例外ハンドリング機能を使って例外をキャッチして画面遷移を制御する案もあるが、あまりスマートな解決方法ではない。

そのため、コントローラ内でロールバックの遷移が必要な場合は、ドメインクラスのwithTransactionを使う、または処理をサービスクラスに移してトランザクションの境界をサービスクラスにすると良いだろう。

ただ、今回の例でいうと、そもそもトランザクションは必要なのだろうか。今回はPersonに対して変更が1度しかないこともあり、トランザクションはなくても良い。ただ、Personのsaveが一度しか無いからといって、常にトランザクションが必要ないわけではない。ドメインクラスに関連がある場合は、他のドメインクラスへ変更がカスケードされることがあるため、このような場合はトランザクションを使うべきだろう。

ということで今回は@Transactionalを消してしまう。

class PersonController {
    ...
    def save(Person personInstance) { ...  }
    def update(Person personInstance) { ...  }
    def delete(Person personInstance) { ...  }
    ...
}

またDataIntegrityViolationExceptionをキャッチした時、一意制約違反があったプロパティを特定できないことにも注意が必要である。同一ドメイン上に複数の一意制約が存在することはそうそうないと思うが、一律DataIntegrityViolationExceptionがスローされるため、どのプロパティで一意制約が発生したのか特定するのが難しい。このような場合は割り切ったエラー処理が必要になるだろう。テーブルロックをかけて、この例外が発生しないようにもできるが、これは良い選択ではないだろう。

一意制約が発生する可能性のプロパティが1つしかない場合は、それを前提にエラー処理を行うことができる。

そもそも、当たり前だが制約違反が発生しないのであればDataIntegrityViolationExceptionをキャッチする必要はない。

まとめ

追加、更新、削除処理は一見簡単にみえるが、まじめに実装すると色々考慮すべきことがある。 最終的には以下のような形になった。

def save() {
    def person = new Person()
    bindData person, request
    if (!person.validate()) {
        respond person.errors, view: 'create'
        return
    }

    try {
        person.save validate: false, flush: true
    } catch (DataIntegrityViolationException e) {
        person.errors.rejectValue('username', 'default.not.unique.message', ['username', message(code: 'person.label', default: 'Person'), person.username] as Object[], 'Username must be unique')
        respond person.errors, view: 'create'
        return
    }
    ...
}

def update(Long id, Long version) {
    def person = Person.get id
    if (!person) {
        notFound()
        return
    }

    if (person.version > version) {
        person.errors.rejectValue('version', 'default.optimistic.locking.failure', [message(code: 'person.label', default: 'Person')] as Object[], 'Another user has updated this Person while you were editing')
        respond person.errors, view: 'edit'
        return
    }

    bindData person, request
    if (!person.validate()) {
        respond person.errors, view: 'edit'
        return
    }

    try {
        person.save validate: false, flush: true
    } catch (OptimisticLockingFailureException e) {
        person.errors.rejectValue('version', 'default.optimistic.locking.failure', [message(code: 'person.label', default: 'Person')] as Object[], 'Another user has updated this Person while you were editing')
        respond person.errors, view: 'edit'
        return
    } catch (DataIntegrityViolationException e) {
        person.errors.rejectValue('username', 'default.not.unique.message', ['username', message(code: 'person.label', default: 'Person'), person.username] as Object[], 'Username must be unique')
        respond person.errors, view: 'edit'
        return
    }
    ...
}

def delete(Long id) {
    def person = Person.get id
    if (!person) {
        notFound()
        return
    }

    try {
        person.delete flush: true
    } catch (OptimisticLockingFailureException e) {
        notFound()
        return
    }
    ...
}

重複したコードが増えるため、Domain Lockingプラグインや、独自のカスタムメソッドを用意するとよい。

最後にドメインクラスや、処理の内容に応じてこのコードが最適なわけではないことに注意してほしい。 そんなところで。

GrailsのSpring Security Core Pluginでパスワードのソルトとストレッチング

パスワードをハッシュで保存して置くのは当たり前ですが、レインボーテーブル使用した総当たり探索の対策として、ソルトとストレッチングを組み合わせ、より安全にパスワードを保存するのが一般的になってきました。

GrailsSpring Security Core PluginはデフォルトではSHA-256のハッシュでパスワードを保存しますが、ソルトやストレッチングは使用されていません。しかし、そこは認証のデパートSpring Security、ソルトとストレッチングのサポートが組み込まれています。

まずはソルトのサポートから見て行きましょう。

usernameをソルトとする

usernameをソルトとして扱うにはConfig.groovyに以下の設定を追加します。

grails.plugins.springsecurity.dao.reflectionSaltSourceProperty = 'username'

これで認証時に、usernameをソルトとしてパスワードに付与しハッシュを計算するようになります。ただし、この設定はあくまで認証時の設定であり、ユーザのパスワードを保存する時にソルトが付与されるわけではありません。

もし、s2-quickstartコマンドを使って生成したユーザドメインクラスを使用している場合は、このドメインクラスに手を入れます。デフォルトでは以下のようにパスワードがエンコードされています。

protected void encodePassword() {
    password = springSecurityService.encodePassword(password)
}

これを以下のように変更します。

protected void encodePassword() {
    password = springSecurityService.encodePassword(password, username)
}

この様にspringSecurityService#encodePasswordの第2引数にソルトとしてusernameを指定します。

これでパスワード保存時にusernameをソルトとして付与してハッシュが計算されます。

ソルト専用のプロパティを使う

徳丸先生の資料によるとソルトには一定の長さが必要です。usernameとpasswordの組み合わせで一定以上の長さが確保できない場合は、一定の長さをもつソルト専用のプロパティが欲しくなります。

以下のようにユーザドメインクラスを変更します。

import org.apache.commons.lang.RandomStringUtils

class User {

    ...

    String salt

    String getSalt() {
        // 新規作成時にsaltがない場合にsaltを生成
        if (!salt) {
            // ランダムである必要ないが一定の長さをもつ文字列として
            // ランダムが扱いやすいのでRandomStringUtilsを使用
            salt = RandomStringUtils.randomAlphanumeric(20) // ランダムな20文字
        }
        return salt
    }

    static constraints = {
        ...
        salt blank: false
    }

    static mapping = {
        ...
        // 安全のためinsert以外でsaltが更新されないようにする
        salt updateable: false
    }

    protected void encodePassword() {
        // usernameをsaltに変更
        password = springSecurityService.encodePassword(password, salt)
    }
}

合わせてConfig.groovyreflectionSaltSourcePropertyも変更しましょう。

grails.plugins.springsecurity.dao.reflectionSaltSourceProperty = 'salt'

これで設定完了!と言いたいところですが、まだいくつかの準備が必要です。

上記のreflectionSaltSourcePropertyはSpringSecurityのUserDetailsを実装したクラスのプロパティを指定しています。GrailsのSpring Security Core PluginのデフォルトではUserDetailsの実装クラスにGrailsUserが使われます。 しかし、GrailsUserにはsaltというプロパティは存在しないため、新たにクラスを用意する必要があります。

また、このUserDetailsインスタンスUserDetailsServiceの実装クラスで生成されます。GrailsのSpring Security Core Pluginでは、これがGormUserDetailsServiceというクラスになります。

つまり独自のUserDetailsServiceを作成し、saltというプロパティを持つ独自のUserDetailsを生成する必要があります。

クラスはsrc/groovyなどに作成すると良いでしょう。

まずはUserDetailsの実装クラスを以下のよう作成します。

import org.codehaus.groovy.grails.plugins.springsecurity.GrailsUser
import org.springframework.security.core.userdetails.UserDetails

class MyUser implements UserDetails {

    @Delegate
    GrailsUser grailsUser

    String salt
}

次に上記のMyUserクラスを生成するUserDetailsServiceを作ります。既存のGormUserDetailsServiceを継承して作るがカンタンです。

import org.codehaus.groovy.grails.plugins.springsecurity.GormUserDetailsService
import org.springframework.security.core.GrantedAuthority
import org.springframework.security.core.userdetails.UserDetails

class MyUserDetailsService extends GormUserDetailsService {

    @Override
    protected UserDetails createUserDetails(user, Collection<GrantedAuthority> authorities) {
        new MyUser(grailsUser: super.createUserDetails(user, authorities), salt: user.salt)
    }
}

groovyの@Delegateを使用したお手軽実装にしてみました。

あとはこのサービスをSpringのビーンとして登録します。resource.groovyに以下のように追加してください。

beans = {
    ...
    userDetailsService(MyUserDetailsService) {
        grailsApplication = ref('grailsApplication')
    }
}

これでソルト専用のプロパティを使うことができます。

ストレッチング

GrailsのSpring Security Core PluginのデフォルトではpasswordEncoderorg.springframework.security.authentication.encoding.MessageDigestPasswordEncoderが使われています。

このクラスはストレッチングをサポートしています。 しかし、プラグインの設定ではgrails.plugins.springsecurity.password.algorithmというハッシュアルゴリズムの設定があるだけで、ストレッチング回数の設定はありません(デフォルトでは1回です)。

自分で新たにビーンを定義することもできますが、ここはGrailsが持つ、設定ファイルからSpringビーンのプロパティを上書きする仕組みを活用します。

Config.groovyに以下の設定を追加します。

beans.passwordEncoder.iterations = 1000

これで1000回ストレッチングします。

テスト

ソルトとストレッチングが機能しているか不安になるのでテストを書いてみましょう。

def "ソルトとストレッチングの確認"() {
    setup:
    def password = "password"
    def user = new User(username: "test", password: password, enabled: true)

    and: "パスワードにランダム文字列をソルトとして付与"
    def digest = "$password{$user.salt}".getBytes("UTF-8")

    and: "1000回ストレッチング"
    1000.times { digest = MessageDigest.getInstance("SHA-256").digest(digest) }
    def hashedPassword = new String(Hex.encode(digest))

    when: "新規Userを保存"
    user.save()

    then: "ソルトとストレッチングを使用したハッシュになっていること"
    user.password == hashedPassword
}

うまく動いているようです。

おわりに

ということでソルトとストレッチングで安全にパスワードを保存しましょう。 プラグインのリファレンスは以下を詳しく見ておくとよいです。

http://grails-plugins.github.io/grails-spring-security-core/docs/manual/guide/12%20Password%20and%20Account%20Protection.html

なお、この記事の内容は自己責任でご利用ください。ではでは。

この記事はGrails2.2.1、Spring Security Core Plugin1.2.7.3をもとに記述しています。

Struts1ユーザにGrailsをオススメする6の理由 - The search is over -

Struts1のEOLがアナウンスされました。 最後のリリースから長く時間が経過しており、実質開発は終了している状態でしたが、このタイミングでのアナウンスとなりました。

アナウンスの中では、次の乗り換え先として、Struts2・Spring Web MVCGrails・Stripesといったフレームワークがお薦めされていますが、私はダンチな生産性を提供するGrailsをお勧めします。

私のフレームワーク遍歴は、Struts1、WicketSeasar2、Springと色々渡り歩いてきましたが、ここ1年はがっつり業務でGrailsを使用しています。 1年がっつり使ってみた経験から、Struts1ユーザにGrailsをお勧めする理由をいくつか考えてみました。

Grailsは既存Javaフレームワークの延長線上にある

Grailsを支える基盤は、Spring・Hibernateといった、Javaの世界でよく知られ、また広く使われているフレームワークです。 ビューはSpring MVCをベースとしており、モデルはHibernateをベースとしています。 このため、既存のJavaシステムで、これらフレームワークの知識を有している場合は、そのノウハウを思う存分活用できます。

Struts1ユーザからみてGrailsをお勧めする理由の1つは、Grailsのコントローラがリクエスト駆動ベースであることです。 WicketJSFなどのように、コンポーネントベースではありません。 このため、Struts1でActionクラスを実装してしてきたユーザにとって、GrailsのControllerは非常に馴染みやすいはずです。

さらに、Java EE互換である点も重要です。 Grailsでは、単にgrails warとコマンドを実行するだけでwarファイルを生成できます。 このwarファイルは、Tomcatなど今までStrutsアプリケーションをデプロイしてきたアプリケーションサーバにデプロイ可能です。 既存のアプリケーションサーバが使用できるため、今までの運用ノウハウも引き続き活用できます。

GroovyはJava開発者のための言語である

RubyPythonといった言語に比べて、Javaコードの冗長さは、よく批判の対象になります。 Groovyの目標は、常に、このJavaの定型的な冗長さを排除し、Java開発者に異次元の生産性を提供することです。

GroovyはJava言語の延長にある、Java開発者のための言語です。 Java開発者のための言語である証拠に、一部の例外を除き、Javaのコードは、Groovyのコードとして実行可能です。 これは、Groovyのシンタックスは、Javaのシンタックスと互換があるということです。 そのため、Javaプログラマであれば、今すぐにでもGroovyのコードが書き始められます! 世界中の多くのJavaエンジニアは、潜在的なGroovyエンジニアといっても過言ではないでしょう。 若干言い過ぎたかも知れませんが、Javaエンジニアにとって非常に学びやすい言語であることは間違いありません。

もちろん、Javaコードとの親和性だけでなく、GroovyにはJava開発者に異次元の生産性を提供する多くの機能が含まれています。 型宣言の省略、クロージャ、便利なコレクション操作、メタプログラミング、演算子のオーバーロードなど、Rubyや他言語で羨ましかった機能、またはそれ以上の機能がGroovyで使用できます。

Grailsは、このGroovyでコードを書きます。 Struts1ユーザにとっては、このGroovyが逆に障壁になるかと思います。 いくらJavaとの親和性が高いとはいえ、言語が変わるというのは大きな話です。 しかし、新たなフォースを手に入れるには常に痛みが付き物です。 Java開発者にとって、比較的少ない痛みで、異次元の生産性が手に入るのはGroovy以外にありません。

既存のJava資産をそのまま活用できる

GroovyとJavaの親和性の高さも、既存のJava開発者にとって大きな魅力の1つです。 Groovyからは、既存のJavaのライブラリ・フレームワークといった、Javaのコードを簡単に呼び出すことができます。 これは、Javaのコード内でJavaのコードを呼ぶように、Groovyのコード内でJavaコードを呼び出せます。

また、GrailsでもJavaライブラリや、Javaのコードと連携する仕組みが予め用意されています。 jar形式のJavaライブラリは、単にlibディレクトリにファイルを置くだけで、簡単にアプリケーションの依存関係として追加できます。 Javaコードには、デフォルトでsrc/javaというディレクトリがGrailsによって用意されており、このディレクトリでJavaコードをすぐに書き始めることができます。

もし、Strutsアプリケーションで使用してた、ライブラリや、ビジネスロジックなどの資産がある場合は、 Grails上でもその資産を活用できます。

フレームワークの連携で悩むことはありません

Struts1を単独で使用していたユーザもいるかも知れませんが、SpringやHibernateといった、他のフレームワークと組み合わせて使用していたユーザも多くいると思います。 これらフレームワークを組み合わせて使用する場合は、自分で連携の設定をしなければなりません。 この連携の設定を調べるために、インターネットを彷徨い、気がつくと1日2日経過していた、という話は珍しくありません。 一度連携できたら、使い回すだけでしょ?と思いがちですが、フレームワークのバージョンに伴い設定方法が変更になり、また1日2日インターネットにダイブする羽目になります。

たかが数日と思うかもしれませんが、迅速な開発が求められる昨今においては、これは非常に足かせになります。ぐぐってる暇なんてありません。

Grailsフルスタックフレームワークです。create-appとコマンドを打てば、数秒で全てのフレームワーク連携が完了した雛形が手に入ります。 Struts1のように、フレームワークの連携で悩むことは、Grailsではありません。

進化し続けるフレームワーク

Struts1ユーザにとって、乗り換えたフレームワークが、数年で使い物にならなくなる自体はあまり嬉しくないでしょう。 そのため、現在Grailsの開発が活発に行われているか、今後もメンテンナスが続くかは関心の1つかと思います。

現在Grailsは、VMwareとEMCとの合併会社Pivotalの配下にある、SpringSourceで開発が行われています。 開発自体はオープンに行われており、ベンダー依存を気にする必要は今のところ無いかと思います。 ソースコードGithubで公開されています。

開発は活発に行われており、世の中の動向に合わせて新しい機能が今なお追加されています。 ただ、新しい機能がどんどん追加されているため、正直枯れているとは言えない状況です。

Sturts1ユーザにとっては、安定性が心配になるかと思いますが、商用レベルでの使用に十分耐えるレベルではあるので、安心してください。

学習環境

Grailsドキュメントは非常にしっかりと書かれており、日本語版も鋭意翻訳中です。

まとめ

GrailsJava開発者のための、フレームワークです。 Java開発者が魅力を感じないとしたら、ほんと訴求先がないと言っていいかもしれません(言語、フレームワークは素晴らしいのは間違いないのですが、Java開発者以外には訴求力が弱い)。

ということでSturts1ユーザの皆さん、一緒にGrailsやりましょう!

GrailsでCucumberを使用する

RubyRailsの世界では、プレーンテキストで記述できる受け入れテストツールとして有名なCucumberですが、Pure-Javaで実装されたcucumber-jvmのお陰で、Grailsでもプラグインをインストールすることで簡単にcucumberが利用できるようなります。

準備

cucumber pluginをBuildConfig.groovyに追加しインストールします。

plugins {
    ...
    test ":cucumber:0.8.0"
}

featureの準備

デフォルトではcucumberに必要なfeatureやstepファイルはtest/functionalに格納します。これは設定ファイルで変更することも可能です。 まず始めに以下の様なfeatureをtest/functional/NewBook.featureに作成します。

#language: ja

フィーチャ: 新しい本の登録
    本の所有者として
    私はBookTrackerに自分の本を追加したい
    自分でそれを覚えておく必要がないように

シナリオ: 新しい本
    前提 BookTrackerを開く
    もし "Specification by Example"を追加する
    ならば "Specification by Example"の詳細を参照できる

日本語でfeatureファイルを記述するには#language: jaをファイルの先頭に追加する必要があります。

実行

実行はGrailsのfuctionalテストとして実行します。

grails test-app functional:
grails test-app :cucumber

まだstepを実装していないため、以下のようなエラーが出力されるはずです。

You can implement missing steps with the snippets below:

前提(~'^BookTrackerを開く$') { ->
    // Express the Regexp above with the code you wish you had
    throw new PendingException()
}
もし(~'^"([^"]*)"を追加する$') { String arg1 ->
    // Express the Regexp above with the code you wish you had
    throw new PendingException()
}
ならば(~'^"([^"]*)"の詳細を参照できる$') { String arg1 ->
    // Express the Regexp above with the code you wish you had
    throw new PendingException()
}

stepの実装

test/functional/steps/Book_steps.groovyを作成してstepを実装していきます。デフォルトではstepはtest/functional/steps/ディレクトリに格納する必要があります。step実装の主な流れは以下のようになります。

  • 先ほどの出力をコピーする
  • PendingExceptionをインポートする
  • JAのlanguageをインポートする
  • (ダブルクオートで使用している場合は$をエスケープする、GStringの制約のため)

これを実施すると以下のようなファイルになります。

import cucumber.runtime.PendingException

this.metaClass.mixin(cucumber.api.groovy.JA)
// 以下のようにstaticインポートしても同じ
//import static cucumber.api.groovy.JA.*

前提(~'^BookTrackerを開く$') {->
    // Express the Regexp above with the code you wish you had
    throw new PendingException()
}
もし(~'^"([^"]*)"を追加する$') { String arg1 ->
    // Express the Regexp above with the code you wish you had
    throw new PendingException()
}
ならば(~'^"([^"]*)"の詳細を参照できる$') { String arg1 ->
    // Express the Regexp above with the code you wish you had
    throw new PendingException()
}

ここで再度テストを実行すると、PendingExceptionがスローされます。それでは次に必要な実装を追加していきます。

前提(~'^BookTrackerを開く$')

必要なセットアップがあるならここで実装を追加しますが、今回は特に必要なセットアップが存在しないため、単に何もしないように変更します。

前提(~'^BookTrackerを開く$') {->
    // NOP
}

もし(~'^"([^"]*)"を追加する$')

まず、必要となるdomainとcontrollerを実装します。今回作成するcontrollerは単にJSONを返すインタフェースになっています。

domain:

package books

class Book {
    String title
}

controller:

package books

import grails.converters.JSON

class BookController {

    def add() {
        render new Book(params).save() as JSON
    }

}

次にstepを次のように実装します。

もし(~'^"([^"]*)"を追加する$') { String bookTitle ->
    bookController = new BookController()
    bookController.params.title = bookTitle
    bookController.add()
}

ここでは詳細には説明しませんが、これはGrailsにおけるcontrollerのテストの仕組みにそってテスト実装する必要があります。 この状態で再度テストを実行するとjava.lang.IllegalStateExceptionがスローされます。 これを解消するには、コントローラが外部から呼ばれているように見せるために、コントローラのテストに必要なセットアップとクリーンアップのコードを追加する必要があります。

Before & After

test/functional/hooks/env.groovyに以下のようなファイルを追加します。これによりコントローラをテストする際のモック機能が有効になります。

import org.codehaus.groovy.grails.test.support.GrailsTestRequestEnvironmentInterceptor

this.metaClass.mixin(cucumber.api.groovy.Hooks)

GrailsTestRequestEnvironmentInterceptor scenarioInterceptor

Before() {
    scenarioInterceptor = new GrailsTestRequestEnvironmentInterceptor(appCtx)
    scenarioInterceptor.init()
}

After() {
    scenarioInterceptor.destroy()
}

この状態でテストを実施すると、先ほどのjava.lang.IllegalStateExceptionがスローされずにstepの最後のブロックで、PendingExceptionが発生します。

ならば(~'^"([^"]*)"の詳細を参照できる$')

以下のように実装します。

ならば(~'^"([^"]*)"の詳細を参照できる$') { String bookTitle ->
    def actual = bookController.response.json

    assert actual.id
    assert actual.title  == bookTitle
}

controllerのレスポンスに格納されているjsonの値を参照し、saveした際に生成されるidと、paramsから取得したtitleが正しく設定されているか検証しています。

最後にもう一度テストを実行してみます。

$ grails test-app functional:
| Server running. Browse to http://localhost:8080/cucumber
| Running 1 cucumber test...
| Completed 1 cucumber test, 0 failed in 2429ms
| Tests PASSED - view reports in /Users/yamkazu/IdeaProjects/grails-examples/cucumber/target/test-reports

うまくテストが通りました。

まとめ

Grailsで簡単にCucumberが利用できました! 次回は?Gebとの連携を紹介します。

参考

GrailsでアノテーションベースでBeanを登録する

元ネタ

Grailsではgrails-app/serviceディレクトリ配下などにクラスを置くと自動的にSpringのbeanとして認識されますが、src/groovysrc/javaといったディレクトリでは自動的にはbeanとして登録されません。

src/groovysrc/java配下のクラスをbeanとして登録したい場合はSpring Bean DSLを使用して登録することができますが、もう一つの方法としてSpringのcomponent-scanを使用する方法がGrailsでも提供されています。

component-scanを使用すると指定したパッケージ配下のクラスに対してアノテーションベースでbean登録ができるようになります。

設定の準備

Grailscomponent-scanを使用するにはConfig.groovyでgrails.spring.bean.packagesを指定します。

grails.spring.bean.packages = ["grails.example"]

これであとはgrails.example配下にアノテーションベースで定義したクラスを置くことで自動的にbean登録されます。

アノテーションベースでbeanを定義する

Grails特有のルールというのは基本的になくSpringのルールに従うだけです。詳細はSpringのドキュメントを参照してくだい。

いくつかサンプルを紹介します。

Springのアノテーションを使用して登録する

Springの@Component@Autowired@Qualifierなどを使用して登録します。

package grails.example

import org.springframework.beans.factory.annotation.Autowired
import org.springframework.beans.factory.annotation.Qualifier
import org.springframework.stereotype.Component

@Component
class MyBean {

    @Autowired
    MyBeanHoge myBeanHoge

    @Autowired
    @Qualifier("myPiyo")
    def piyo

    @Autowired
    @Qualifier("grailsApplication")
    def grailsApplication

    String toString() { "This is MyBean" }
}

@Component
class MyBeanHoge {
    String toString() { "This is MyBeanHoge" }
}

@Component("myPiyo")
class MyBeanPiyo {
    String toString() { "This is MyBeanPiyo" }
}

JSR330を使って登録する

JSR330の@Inject@Namedを使用して登録します。

package grails.example

import javax.inject.Inject
import javax.inject.Named

@Named
class NamedBean {

    @Inject
    NamedBeanHoge namedBeanHoge

    @Inject
    @Named("namedPiyo")
    def piyo

    @Inject
    @Named("grailsApplication")
    def grailsApplication

    String toString() { "This is NamedBean" }
}

@Named
class NamedBeanHoge {
    String toString() { "This is NamedBeanHoge" }
}

@Named("namedPiyo")
class NamedBeanPiyo {
    String toString() { "This is NamedBeanPiyo" }
}

JSR330のアノテーションを使用するには依存ライブラリの追加が必要です。

dependencies {
    ...
    compile 'javax.inject:javax.inject:1'
}

JSR250系も使える

package grails.example

import javax.annotation.PostConstruct
import javax.annotation.PreDestroy
import javax.inject.Named

@Named
class PostConstructAndPreDestroyBean {

    def number

    @PostConstruct
    def init() { number = 100 }

    @PreDestroy
    def destroy() {}
}

Database Migration Pluginでcontextを使用する

LiquibaseではchangeSetにcontext属性が設定できます。このcontext属性を使用することで、実行時に適用するchangeSetの範囲を指定できます。

databaseChangeLog = {

    changeSet(author: "yamkazu", id: "default-ctx") {
        ...
    }

    changeSet(author: "yamkazu", id: "prod-ctx", context: "prod") {
        ...
    }

    changeSet(author: "yamkazu", id: "test-ctx", context: "test") {
        ...
    }
}

実行時にcontextを指定するには--contextsオプションを使用します。

grails> dbm-update --contexts="test"

いくつかの特徴的なルールがあるので以下にまとめます。

  • 複数指定する場合はカンマで区切る
  • changeSetでcontextが無指定の場合は常に実行される
  • --contextsを指定しないとすべてのコンテキストが対象になる

後ろ2つが若干わかりにくいですが、--contextsの指定と実行されるidは以下のようになります。

  • dbm-update - default-ctxtest-ctxtest-ctxのchangeSetを実行
  • dbm-update --contexts="test" - default-ctxtest-ctxのchangeSetを実行
  • dbm-update --contexts="test,prod" - default-ctxtest-ctxtest-ctxのchangeSetを実行

特に指定しないとすべてのコンテキスト実行される点は注意が必要です。例えばtestのようなコンテキストを作成した時点で、本番環境に適用する際には明示的に何かのコンテキストを指定しないと、testを含む全てのchangeSetが適用されるといった動作をします。この場合は実際にそのコンテキストが存在するかは別として、明示的に何らかの適当なコンテキストを指定しておく必要があります。

updateOnStartでアプリケーション起動時に連動させるにはConfig.groovyでgrails.plugin.databasemigration.updateOnStartContextを指定してください。

grails.plugin.databasemigration.updateOnStartContext="prod"