Groovyの不思議なメソッド「call」
概要
通常のクラスであれば、インスタンスをnewで生成して、メソッドを呼び出します。
class Hoge { def test(String message) { "${message} in Hoge#test" } } def hoge = new Hoge() assert hoge.test("test") == "test in Hoge#test"
Groovyのクロージャにも同様のものが有りますが、インスタンスメソッドにもcall
という特殊な名前のものを定義することが出来ます。
このcall
というメソッドを定義すると、メソッド名を省略してcallメソッドを実行できます。
使い方
実際にサンプルを見てみます。先ほどのHogeクラスにcallメソッドを追加したのが以下です。
class Hoge { def test(String message) { "${message} in Hoge#test" } // これを追加 def call(String message) { "${message} in Hoge#call" } } def hoge = new Hoge() assert hoge.test("test") == "test in Hoge#test" // 当然普通に呼べる assert hoge.call("test") == "test in Hoge#call" // しかし!callメソッドはメソッド名を省略できる! assert hoge("test") == "test in Hoge#call"
このようにcall
メソッドを定義することで、インスタンスが格納されている変数自身がまるでメソッドのように振る舞うことが出来ます。
普段のコーディングで使いドコロがあるか、と言われると微妙ですが、DSLを自作する際などには威力を発揮するのではないでしょうか?
以下、自分の確認用に作成したMapで渡された値を元に簡単なHTMLを生成するサンプルです。
class MyHTMLBuilder { def call(Map htmlParts) { """ <html> <head>${htmlParts.head.collect{k,v -> "<${k}>${v}</${k}>"}.join("")}</head> <body>${htmlParts.body.collect{k,v -> "<${k}>${v}</${k}>"}.join("")}</body> </html> """ } } def html = new MyHTMLBuilder() def map = ['head': ['title': 'hogehoge'], 'body':['h1': 'これはTitle','p':'Groovy良いよね!']] println html(map)
実行結果は以下のようになります。(見やすいようにインデントしています)
<html> <head> <title>hogehoge</title> </head> <body> <h1>これはTitle</h1> <p>Groovy良いよね!</p> </body> </html>
GroovyでJSONを生成する(JsonBuilder)
GroovyのコードとしてJSONを生成する
特に難しい内容ではないのですが、自分で使う時に毎回ググっているので備忘録として纏めます。
JsonBuilderというGroovy標準のクラスを利用することで簡単にJSONを生成することが出来ます。
import groovy.json.* def json = new JsonBuilder() json( name: 'koji', age: 32, hobbies: [[id: '1', name:'hobby1'], [id: 2, name: 'hobby2']], address: [country: 'japan', city: 'tokyo'] ) println json.toPrettyString()
実行結果は以下の様になります。
{ "name": "koji", "age": 32, "hobbies": [ { "id": "1", "name": "hobby1" }, { "id": 2, "name": "hobby2" } ], "address": { "country": "japan", "city": "tokyo" } }
ぱっと見で分かるシンプルな例ですが、
name
とage
はそれぞれ普通の値です。
hobbies
は、オブジェクトを格納する配列です。
address
は、オブジェクトを格納するオブジェクトです。
また、name
とage
のように、Groovyが自動的に型を判断してダブルクオーテーションで囲んだりしてくれています。非常に楽ちんです。
公式ドキュメントには、JsonBuilderに対してjson{...}
というサンプルが用意されていますが、それだと上記のようなhobbies配列にオブジェクトを渡すことが出来ません。(出来るとは思うのですが自分はその方法を知りません。。。)
そこで、JsonBuilderをjson(...)
という形で呼び出すようにすれば、JSONの値はすべてMapで記述することが出来ます。
もう少し詳細
json
に続いて書く内容は、すべてcall
メソッドへ渡されて実行されます。
call
メソッドは渡される型によっていくつかオーバロードされています。
json {...}
は、JsonBuilder#call(Closure)
が呼ばれ、json(...)
はJsonBuilder#call(Map)
が呼ばれている形です。
少し混乱しそうですが、Groovyは引数としてに渡すMapの場合、最初の[
]
は省略できます。
そして、引数があるメソッドを実行する際には、引数の(
)
を省略できます。
まとめると、
json(...)
は、json.call([:])
を実行していて、json{}
はjson.call({クロージャ})
を実行している、という事になります。
慣れるまでは、なんだかよく分からん、という状態になりそうですが、基本的にはJSONにしたい値をすべてMapで表現しておいて、それをJsonBuilderに渡してあげれば問題ないと思います。
参考:
JsonBuilder
追記
callにクロージャを渡して、配列も定義できる方法を教えて頂きました。
カッコつけたらイケた!https://t.co/35zzMjwPGq
— こなもの (@gitcommitamend) 2017年7月4日
教えて頂いたリンク先のコードは以下のようになっています。
import groovy.json.* def json = new JsonBuilder() json { name 'koji' age 32 hobbies([id: '1', name:'hobby1'], [id: 2, name: 'hobby2']) address([country: 'japan', city: 'tokyo']) } println json.toPrettyString()
hobbies
のような配列のものも(
)
で囲めばイケるようです。
Grails3の本番環境で/dbconsoleにアクセスできるようにする
いつからか分からないけど、少なくともGrails 3.2.8では、production環境ではデフォルトで/dbconsole
にアクセスできなくなっています。(セキュリティ上の理由で)
閉じた環境だしSpringSecurityCoreとか使ってるから大丈夫!という場合は、application.ymlに以下の設定を追加すれば大丈夫。
environments: production: # これはもともとapplication.ymlにある。この下に以下の3行を追加。 grails: dbconsole: enabled: true
これで再起動すればOK。
Grails 3.2.8と3.2.9でSpring Security CoreとDatabase Migrationを利用する際の注意点
前提条件
Spring Security Coreラグインのバージョンは3.1.2
です。
Database Migrationプラグインのバージョンは3.0.0
です。
また、認証用のドメインを作成するs2-quickstart
コマンドを以下のように実行したと仮定します。
s2-quickstart example User Role
これで、User
とRole
、UserRole
というドメインが生成されます。
Grails 3.2.8の場合
Spring Security Coreプラグイン
デフォルトでは正しく動きません
というのも、Grails3.2.8からドメイン内のプロパティに対してDIがデフォルトで動かないようになったためです。
これはパフォーマンスのための措置とのことです。
通常のアプリケーションであれば特に問題になることはないと思いますが、Spring Security Coreラグインを利用している場合、User
ドメイン(User.groovy
)にSpringSecurityService springSecurityService
というプロパティが設定されています。
今までは、User
ドメインをsave
する際にUser#encodePassword()
が呼ばれて、その中でspringSecurityService
を使って自動的にパスワードを暗号化してくれていました。
しかしGrails 3.2.8からはspringSecurityService
がDIされていないためnullになっており、それが原因でパスワードは暗号化されずに平文のままDBに保存される状態になっています。
ログインページからログインしようとした場合、入力したパスワードは自体は本来の暗号化アルゴリズムで暗号化されてDBに問い合わせされるので、当然一致するパスワードが無いのでログイエラーとなってしまいます。
この問題の解決するためには、GORMのDIを以前と同じように有効化する必要が有ります。
具体的には、application.yml
で、autowire
をtrue
にするだけです。
grails: gorm: autowire: true #これをfalseからtrueへ。もしなければ単に追加すればOK。
Grailsを3.2.7から3.2.8にアップグレードした場合、既存ユーザのログイン自体は問題なく動作して、この問題は新しいユーザを追加したタイミングで初めて発生します。
非常に気付きづらいので、Spring Security Coreプラグインを利用している場合、3.2.8にアップグレードするのであればこの部分は要注意です。
Database Migrationプラグイン
さらに、データベースがH2(デフォルト)で、Database Migrationプラグインを利用する際にも注意が必要です。
問題は単純で、User
ドメインのpassword
というカラム名がDatabase Migrationプラグインからうまく利用できません。
起動時にマイグレーションツールを適用(updateOnStart: true
)しようとすると、以下のようなエラーが発生します。
grails> run-app | Running application... Configuring Spring Security Core ... ... finished configuring Spring Security Core INFO 17/05/24 13:14: liquibase: Can not use class org.grails.plugins.databasemigration.liquibase.GormDatabase as a Liquibase service because it does not have a no-argument constructor 2017-05-24 13:14:23.099 ERROR --- [ main] o.h.engine.jdbc.spi.SqlExceptionHelper : 列 "THIS_.password" が見つかりません Column "THIS_.password" not found; SQL statement: select this_.id as id1_8_0_, this_.version as version2_8_0_, this_.account_expired as account_3_8_0_, this_.account_locked as account_4_8_0_, this_.date_created as date_cre5_8_0_, this_.enabled as enabled6_8_0_, this_.last_updated as last_upd7_8_0_, this_."password" as pa ssword8_8_0_, this_.password_expired as password9_8_0_, this_.username as usernam10_8_0_ from user this_ where this_.username=? [42122-193] 2017-05-24 13:14:23.184 ERROR --- [ main] o.s.boot.SpringApplication : Application startup failed
解決策として、User.groovy
に自動生成されるmappingの部分でDBのカラム名を以下のように変更します。
static mapping = { // これは元々あったやつ。コレがマズイ。(H2) //password column: '`password`' //コレを追加 password column: 'passwd' }
これで、再度dbm-gorm-diff
でGORMから差分適用用のchangelogを生成しなおせばちゃんとマイグレーションが動作します。
Grails 3.2.9の場合
Spring Security Coreプラグイン
Spring Security Coreプラグインを利用すること自体には特に問題はありません。
Database Migrationプラグイン
もしもDatabase Migrationプラグインも利用するなら要注意です。
というよりも、結論としてはDatabase MigrationプラグインとSpring Security Coreラグインを同時に利用するのであれば、Grails 3.2.9は見送ったほうが良いと思います。
Hibernate5との絡みの問題らしくて、既にフィックスしたという情報も有りますが少なくとも私の環境では動作しませんでした。
これはGrails 3.2.9がデフォルトで利用するGORMのバージョンが6.0.10.RELEASE
になったために発生します。
このGORMのバージョンからs2-quickstart
コマンドで生成するUser.goorvy
には、パスワードの暗号化用のメソッドencodePassword()
が記載されなくなっています。
その代わりに、新しいsrc/groovy/example/UserPasswordEncoderListener.groovy
というファイルが生成されます。このファイルの中に暗号化用の処理が移動されたようです。
そして、resources.groovy
にそのUserPasswordEncoderListener
のインスタンスを生成する以下のコードが1行挿入されます。
userPasswordEncoderListener(UserPasswordEncoderListener, ref('hibernateDatastore'))
冒頭にも述べましたが、Spring Security Coreプラグイン自体は正しく動作するので、特に問題ありません。
また、最初のGrails 3.2.8のDI問題を解決するためにこの仕組みが導入されたわけなのでSpring Security Coreプラグインとしては正当な進化となります。
しかし、残念ながらDatabase Migrationプラグインと同時に利用することが出来ません。
この状態でDatabase Migrationプラグインのコマンド(dbm-*
)を実行すると、以下のようなエラーが発生します。
*************************** APPLICATION FAILED TO START *************************** Description: The dependencies of some of the beans in the application context form a cycle: authenticationUserDetailsService ↓ userDetailsService ┌─────┐ | hibernateDatastore ↑ ↓ | userPasswordEncoderListener └─────┘
resources.groovy
に追加したコードからインスタンスの参照なりが循環してしまってエラーになっているようです。
なので、もしもresources.groovy
から件の1行のコードを省けばパット見動作はするようになりますが、既に述べたように本来それは平文のパスワードを暗号化するために利用されるものなので、結果的にGrails 3.2.8の時のように平文でパスワードが保存されるようになり、ログインできなくなります。
こちらに関しては、Grails 3.3以降、対応したSpring Security Coreプラグイン(3.2.0系になる予定)などがリリースされるはずですので、そのタイミングで再度確認したほうがいいかなと思います。
まとめ
今回詳細は述べませんでしたが、どうやらGrails 3.2.9だとAsset-Pipelineを利用するとwarに固めでデプロイするとAssetアクセスできないという問題も有るようです。
なので、個人的にはGrails 3.2.9は使わずに、Grails 3.2.8で、application.yml
のgrails.gorm.autowire
をtrue
にして様子見をするのが良いと思います。
参考
Grails Spring Security Core Plugin
Grails Database Migration Plugin
Error creating ‘hibernateDatastore’ - circular reference when running plugin commands
runCommand detects a dependency cycle for AbstractPersistenceEventListener Bean
23. Tutorials
Grails 3.2.8 Upgrade Notes.md
Grails3.2でのCSRF対策
CSRF対策を導入
標準だとGSPのフォームで利用する2重投稿防止用の<g:form useToken="true" ...>
を使うことでCSRF対策が出来る。
ただしこの場合、当然コントローラ側でトークンが正しいかどうかのチェックをしないといけないので、
withForm { // OK! }.invalidToken { // トークンがおかしい! }
というコードを必要な箇所に全て記述しなければならない。
シンプルなコードとは言え、さすがに全コントローラにこのような記述を毎回記述するのは大変。
ということで、基本的にはSpringSecurityCoreを利用するのが楽。
SpringSecurityCoreでの設定
resource.groovy
に以下を追記。
csrfFilter(CsrfFilter, new HttpSessionCsrfTokenRepository()) { // 振る舞いは自分でクラスを用意することで、以下のようにして変更可能 // accessDeniedHandler = ref('自作AccessDeniedHandler') // requireCsrfProtectionMatcher = ref('自作RequireCsrfProtectionMatcher') }
Bootstrap.groovy
に以下を追加
SpringSecurityUtils.clientRegisterFilter('csrfFilter', SecurityFilterPosition.LAST.order + 10)
基本的な設定はこの2つのみ。
クライアント側の設定
で、基本的なFORMからのPOSTとかの場合は、
<input type="hidden" name="_csrf" value="${_csrf.token}">
を付けるだけでもう完了。
これで後はSpringSecurityCoreが全て全自動でチェックしてくれる。わざわざ自分でコントローラ側にトークンの妥当性チェックを書く必要をない。
ただし、JavaScriptからajaxでデータを投げる場合にはHTTPヘッダーに埋め込む方が良いらしい。
なので、基本的には全ページのheadタグ内に以下の内容を追記。
<meta name="_csrf" content="${_csrf?.token}"/> <meta name="_csrf_header" content="${_csrf?.headerName}"/>
で、実際にサーバにデータを投げるJavaScriptで、上記の2つのデータを元にHTTPヘッダーを生成すればOK。
以下は、自分が今作っているアプリケーションで、jQueryでmetaタグの情報を取得して、axiosで実際のそのヘッダーと共にデータをサーバに投げるサンプル。
// metaタグから値を取得 const csrfHeader = $("meta[name='_csrf_header']").attr("content"); const csrfToken = $("meta[name='_csrf']").attr("content"); const headers = {}; headers[csrfHeader] = csrfToken; axios.post( '/u/r/l', { // サーバに投げるデータを指定 name: 'grails' version: '3.2.9' }, { // コレがそのヘッダー headers: headers } ) .then(function (response) { console.log(response); }) .catch(function (error) { console.log(error); });
(ちなみにヘッダー名はデフォルトではX-CSRF-TOKEN
)
なお、Bootstrapに追加したコードの詳細は現在調査中。
もしこれを省いた場合、別のサイトから未ログイン状態のユーザが悪意を持ってPOSTとかPUT、DELETEで遷移させられてきた場合には、まずログイン画面が表示される。
ユーザがもし疑問に思わずにログインすると、GET扱いでログイン先のページに移ってしまう。
(GETになるのでPOSTとかで送られたパラメタはnullになる為、大抵データを保存するような場面なのでvalidationエラーになるはず。だが、それは本来意図したエラーチェックではない)
上記のBootstrapの1行を追加すればちゃんとSpringSecurityCoreがエラー扱いにしてくれる(Statusコードが403じゃなくて999になるので本当に意図したエラーなのか不安。。。ここの辺りを現在調査中)
あと、Bootstrapの1行を追加するとログインフォームにもTokenが必要になるので、views/login/auth.gpsとdenied.gsp
をプラグインからコピーしてきて修正する。
参考ページ
17 Security
8.1.11 Handling Duplicate Form Submissions
Grails 3 CSRF protection
6.7.2.3. AjaxによるCSRFトークンの送信
Spring Security Core Plugin - Reference Documentation