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プラグインや、独自のカスタムメソッドを用意するとよい。

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

G* Advent Calendar 2013 1日目 - Gaiden

この記事は毎年恒例の G* Advent Calendar 2013 の1日目の記事です。1日目担当のyamakzuです。こんにちは。

今日は、先月10月頭にリリースしたGaidenについて紹介します。

GaidenはGroovy製の軽量ドキュメンテーションツールです。 この手のドキュメンテーションツールではSphinxやAsciiDocが有名ですが、ちょっとしたドキュメントを記述するには少し大きすぎるツールに感じるときがあります。

Gaidenはもっと簡単に気軽にドキュメントを記述できるようなツールを目指しています。 シンタックスにはMarkdownを採用しています。 Markdownでさっと書いて、ドキュメントを生成できる。そんなツールです。

とかいう表向きのセールストークはありますが、正直なところはGroovy使いとしてG製なツールが欲しかっただけです。

開発は主に@gantawitterと私、そしてid:nobeansのサポートのもと進めています。また、ドキュメントテンプレートの作成にid:labduckに協力してもらっています。

では早速使ってみましょう。

インストール

Groovy界隈ではお馴染みのGVMからインストールできます。

$ gvm install gaiden

WindowsではGVMが使えないのでバイナリをダウンロードして、展開したディレクトリのbinディレクトリにPATHを通してください。

プロジェクトの作成

まず始めにドキュメントのプロジェクトを作る必要があります。 create-projectコマンドで生成します。

$ gaiden create-project mydoc
$ cd mydoc

プロジェクトの構成は次のようになっています。

$ tree
mydoc
├── GaidenConfig.groovy    : Gaidenの設定ファイル
├── pages                  : Markdownで書かれたページを格納するディレクトリ
│   ├── index.md
│   └── toc.groovy         : 目次を定義するファイル
├── static                 : CSS、JSなどの静的ファイルを格納するディレクトリ
│   ├── ...
└── templates
    └── layout.html        : ページの雛形となるHTMLファイル

ページを記述する

ページはpagesディレクトリ配下にファイル名.mdというファイル名でMarkdownで記述します。 試しにpages/mypage.mdを作り、次のように記述します。

# はじめてのGaiden

Gaidenやばい   

ドキュメントを生成する

ページを記述したらドキュメントを生成してみましょう。 ドキュメントを生成するにはbuildコマンドを実行します。

$ gaiden build

ドキュメントはbuildディレクトリに生成されます。 build/mypage.htmlをブラウザで開いてみると次のような画面が生成されているはずです。

f:id:yamkazu:20131129152118p:plain

目次を作る

目次はpages/toc.groovyに記述します。 次のように記述します。

"index.md"(title: "はじめに") {
    "mypage.md"(title: "はじめてのGaiden")
} 

もう一度gaiden buildを実行してbuild/toc.htmlを開いてみてください。 次のようなページが表示されるはずです。

f:id:yamkazu:20131129152152p:plain

このtoc.groovyはGroovyのDSLで記述します。 ページ名の文字列に続けて引数にtitleという属性を設定していきます。 階層構造を持つ場合はクロージャ{}を使って、ブロック内でページを指定します。

もう少し複雑な例では次ようになります。

"introduction.md"(title: "はじめに") {
    "introduction/whatis.md"(title: "Gaidenとはなにか")
    "introduction/install.md"(title: "インストール")
}
"quickstart/quickstart.md"(title: "クイックスタート") {
    "quickstart/addingcontent.md"(title: "ページの生成")
} 

テンプレートを編集する

ページのデザインを変更したい場合はtemplates/layout.htmlを編集します。 Gaiden 0.3ではデフォルトで次のようなテンプレートになっています。

<html>
<head>
    <meta charset="UTF-8">
    <title>$title</title>
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <link rel="stylesheet" href="${resource('/css/bootstrap.css')}">
    <link rel="stylesheet" href="${resource('/css/prettify.css')}">
    <link rel="stylesheet" href="${resource('/css/style.css')}">
</head>
<body>
    <div class="navbar navbar-static-top">
        <div class="container">
            <a class="navbar-brand" href="$tocPath">$title</a>
            <a class="toc btn btn-primary btn-lg" href="$tocPath"><span class="glyphicon glyphicon-list"></span></a>
        </div>
    </div>
    <div class="container">
        <section class="page-content">
            $content
        </section>
    </div>
    <footer id="footer" class="footer">
        <div class="container">
            <p class="credit text-muted">Powered by <a href="#">Gaiden</a>.</p>
        </div>
    </footer>

    <script src="${resource('/js/jquery-1.10.2.min.js')}"></script>
    <script src="${resource('/js/bootstrap.js')}"></script>
    <script src="${resource('/js/prettify.js')}"></script>
    <script src="${resource('/js/application.js')}"></script>
</body>
</html>

テンプレートの中では${resource(..)}$content$title$tocPathといった拡張変数、メソッドが使えるようになっています。 詳しくはリファレンスを参照してください。 このHTMLを変更することで自由にデザインを変更できます。

今後について

まだまだ機能が少ない状況ですが少しずつ改善していく予定です。

年内には0.4をリリース予定です。 是非使ってフィードバックをください!

明日からは...

なんと3日間連続できょんくんです!どうしてそうなった! ではでは、よろしくお願いしますー

JJUG CCC 2013 FallのJVM言語パネルディスカッションにGroovy代表として参加してきた

あれ、気がつけば半年ぐらいブログ書いてなかった...

ということでJJUG CCC 2013 FallのJVM言語パネルディスカッションにGroovy代表として参加してきました。 いろいろあってJVM言語パネルディスカッションのGroovy代表として声をかけてもらったのですが、 Groovyについて語る自信があまりなかったのでid:nobeansに一緒に参加してもらいました。ありがとうございます!

緊張しまくって自分の資料説明するのが精一杯で、ほとんどディスカッションらしいことはできなかった気がしますが、良い経験になりました。

次は一人で参加できるように頑張ります(?)

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をもとに記述しています。

G*ワークショップZ May 2013 - Spockハンズオン

でしゃべってきました。

http://jggug.doorkeeper.jp/events/3872

さっそく@orangecloverさんがまとめてくれています。ありがとうございます!

http://d.hatena.ne.jp/orangeclover/20130518/1368845593

当日の資料はGitHubにおいてあります

モックの説明以降はずいぶん駆け足になって申し訳なかったのですが、それを見越してドキュメントと学習テストを充実させておいたので、それで勘弁して下さいw

ドキュメントは先程のGitHubのdocsディレクトリに、学習テストはsrc/test/groovyディレクトリにおいてあります。参加しなかった人でもなんとなく眺めればSpockがわかったような気になれる教材に仕上がっています。

また、当日@uehajから質問されてた「oldはどのように値を保持しているのか」という質問に対してコピーして持ってるのではないかと適当なことを言ってしまいましたが、裏で@kiy0takaさんが調べてくれていました。

ということでAST変換で先に評価して値を保持しているそうです。

ではでは、新運営委員長の@y_ugrails連携説明している暇ないからLTよろしくと無茶ぶりした@gantawitter、参加者のみなさんおつかれさまでした!

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との連携を紹介します。

参考