himanago

Azure・C#などのMS系技術やLINE関連技術など、好きな技術について書くブログ

Grails3.3でrest-apiプロファイルを試す(Spring Securityでの認証つき)

はじめに

長いこと業務でGrailsを使っているので、Grailsの動向が気になっています。

Grailsとは、GroovyというJavaを強力に進化させたスクリプト言語を用いたフルスタックなWebアプリケーションフレームワークです。

Ruby on Railsのように「CoC(設定より規約)」「DRY(Don’t Repeat Yourself)」を信条とし、煩わしい作業や無駄な記述を一切排除し素早くWebアプリを開発できる、高い生産性が魅力です。

そんなGrailsですが、つい最近、3.3が正式リリースになりました。

社内の認証プロキシがどうにも突破できずうまく開発できないので現在の業務では2.5を使って開発していますが、ビルドシステムがGradleになり大きく構成の変わった3.xはずっと気になっていました。今回3.3が出たのでちょっと試してみたところ、プロキシ突破できたので、3.x系に乗り換えようか悩み中。。
※プロキシ突破できた話はメモとしてQiitaに書きました

qiita.com

とりあえず、いま作ってるアプリはそのまま2.5で作って、そのあと徐々に3.x系に移行していけばいいかな、と思っています。

さて、今回はGrails 3の出番に備え、リリースしたての3.3を使って、認証機能つきのWeb APIを作ってみたいと思います。 業務で作っているシステムは複数システムを連携ありで作る想定なので、Web APIで連携しやすくすることが重要になるため、ここの検証をしておきます。今回作成したものはGithubに上げています。

github.com

以下、Grailsの簡単な説明を交えつつのやったことの流れです。

「rest-api」プロファイルでプロジェクトを作成

Grailsではコマンドを用いて開発を進めます。アプリの新規作成は「create-app」コマンドで行いますが、Grails 3になると、作りたいアプリの構成ごとに「プロファイル」が用意されており、作成時に指定することができます。プロファイルを指定せずに「create-app」コマンドでアプリを新規作成すると、デフォルトのプロファイルである「web」プロファイルが用いられ、通常のWebアプリケーションのプロジェクトが作成されます。
一方、

grails create-app api-sample --profile=rest-api

とオプションで「rest-api」プロファイルを指定すると、RESTで叩けるWeb APIを作るための雛形プロジェクトを作ってくれます。

ドメインクラスとScaffold

GrailsMVCモデルのフレームワークです。
モデル(DBのテーブルに相当し、Grailsではドメインクラスと呼ばれます)、ビュー(WebアプリではHTMLのレンダリングを司る)、コントローラー(双方の仲立ち)により構成されます。

Grailsでは「設定より規約」により、モデル、ビュー、コントローラーのファイル名を規約通りにつけることで、それぞれが連動し複雑な設定なしでWebアプリとして動いてくれます。具体的には、たとえば書籍を管理するWebアプリの場合「Book.groovy」というモデル(ドメインクラス)、「BookController.groovy」というコントローラークラスを作り、「BookController.groovy」には「index」というpublicなメソッドを作ります(このメソッドを「アクション」と呼びます)。そして、アクションと同じ名前のビュー「index.gsp」というファイルをディレクトリ名がドメイン名と同じであるディレクトリに作ります。つまり、以下のような構成です。

domain
└ Book.groovy
controllers
└ BookController.groovy
views
└ book
   └ index.gsp

この状態でWebアプリを起動し、「http://xxxx/sampleapp/book/index」というURLにアクセスすると、URLに書かれたとおりのコントローラークラスのアクションメソッドが実行され、同様にビューが表示されます。このあたりの仕組みはRuby on Railsと同じですね。

そして、このコントローラーとビューは、ドメインクラスをもとに自動生成することが可能で、それが「Scaffold」です。

ドメインクラスを

package rest.s2.sample

class Book {
    // 属性
    String name
    String author

    // 制約
    static constraints = {
        name blank: false
        author blank: false
    }
}

と定義します。そして、 ‘’‘ grails generate-all rest.s2.sample.Book ’‘’ と実行すると、ドメインクラスに定義した通りの「Book」データをDBと連携し一覧表示/登録/編集/削除ができるようなコントローラーとビューを生成してくれます。これがScaffoldです。自動生成されたコードは実際にそのまま本番で使用することは少ないと思いますが、基本的な動作の書き方の参考にしたり、カスタマイズして流用したりすることができ、開発効率向上に一役買ってくれます。

rest-api の場合の Scaffold

さて、こんなScaffoldですが、「rest-api」プロファイルで作成されたプロジェクトで行うと、ちゃんとRESTで呼べるものが自動生成されます。 特に重要なのはビューで、通常は拡張子が「.gsp」であるGSPファイル(Groovy Server Pages。HTMLをレンダリングするためのファイル)が生成されますが、rest-apiの場合に生成されるのが「.gson」のGSONファイルです。これは、その名の通りGSPのJSON版で、リクエストに対しJSONのレスポンスを返すための情報を記載するファイルです。

起動と動作確認

Scaffoldまでできたら、「run-app」コマンドでアプリを起動します。 APサーバー、DBがあらかじめ含まれているのですぐ動作確認できます(DRYな特徴のひとつ)。 今回はrest-apiなのでブラウザではなくコマンドから確認します。Windows環境なので、PowerShellInvoke-RestMethodを使ってみます。この方法だと、送信するデータをスクリプトで組み立てられるため、RESTの動作確認におすすめです。

RESTということなので、今回、URLにアクションは含めません。UrlMappingsという設定ファイルで、HTTPメソッドとアクションメソッドの対応が定義されており、Grailsのデフォルトの規約を上書きしています。rest-apiでは、規約より設定、ということでしょうか。

package grails3.rest.api.s2.sample

class UrlMappings {

    static mappings = {
        delete "/$controller/$id(.$format)?"(action:"delete")
        get "/$controller(.$format)?"(action:"index")
        get "/$controller/$id(.$format)?"(action:"show")
        post "/$controller(.$format)?"(action:"save")
        put "/$controller/$id(.$format)?"(action:"update")
        patch "/$controller/$id(.$format)?"(action:"patch")

        "/"(controller: 'application', action:'index')
        "500"(view: '/error')
        "404"(view: '/notFound')
    }[f:id:himanago:20170803094920p:plain]
}

実行結果は以下のようになりました。まず登録。

PS C:\> Invoke-RestMethod -Uri "http://localhost:8080/book" -Method POST -Body @{name="Sample Book";author="Sample Author"}

id name        author
-- ----        ------
 1 Sample Book Sample Author

そして一覧表示です。

PS C:\> Invoke-RestMethod -Uri "http://localhost:8080/book" -Method GET

id name        author
-- ----        ------
 1 Sample Book Sample Author

たったこれだけで、RESTなWeb APIを作成することができました。

認証機能をつけるプラグイン

では、次にこのAPIに認証機能を付けてみたいと思います。Grailsには、認証機能をつけるための「Spring Security」がプラグインとして用意されています。プラグインは、Grailsの大きな利点のひとつで、さまざまな機能を追加したり、またよく使う機能を自前のプラグインとして外部化することもできます。

それでは、Spring Securityプラグインを使って認証機能を追加します。 rest-apiの場合、使うのはSpring Security CoreSpring Security RESTの2つ。

プラグインは、build.gradle内のdependenciesに以下の行を追加するだけで導入できます。

    compile 'org.grails.plugins:spring-security-core:3.2.0.M1'
    compile "org.grails.plugins:spring-security-rest:2.0.0.M2"

ちなみに認証の流れはSpring Security RESTのUser guideに載っているこの図がわかりやすいです。 rest

s2-quickstart

Spring Security Coreには、コマンド一発で認証に必要なユーザーとロールのドメインを作ってくれる「s2-quickstart」コマンドが用意されています。

grails s2-quickstart rest.s2.sample.auth User Role

次に、できあがったクラス(User、Roleとその関連を管理するUserRole)に対してgenerate-allでコントローラー/ビューを生成しておきます。

grails generate-all rest.s2.sample.auth.User
grails generate-all rest.s2.sample.auth.Role
grails generate-all rest.s2.sample.auth.UserRole

権限設定

Spring Securityでは、コントローラーのクラス単位/アクション単位でSecuredアノテーションを付けることできめ細かく権限制御をすることができます。ログイン有無や、Userに紐付くRoleごとの制御ができます。今回は、BookControllerのアクションを「ROLE_ADMIN」というRoleを持つUserでログインしていないと実行できないようにしてみます。

記述はクラス名の上に「@Secured(‘ROLE_ADMIN’)」をつけるだけです。もちろん、importは必要になります(この参照がうまくいかない場合はGradleプロジェクトのリフレッシュを行います)。

package rest.s2.sample

import grails.plugin.springsecurity.annotation.Secured
...

@Secured('ROLE_ADMIN')
class BookController {

}

各種設定

初期ユーザー登録

init/Bootstrap.groovyに、起動時処理として初期ユーザーの登録処理を書いておきます。今回は、このユーザーで認証を行います。

package grails3.rest.api.s2.sample

import rest.s2.sample.auth.Role
import rest.s2.sample.auth.User
import rest.s2.sample.auth.UserRole

class BootStrap {

    def init = { servletContext ->
        def user = new User(username: 'admin', password: 'admin')
        user.save(flush: true)
        def role = new Role(authority: 'ROLE_ADMIN')
        role.save(flush: true)
        new UserRole(user: user, role: role).save(flush: true)
    }
    def destroy = {
    }
}

設定ファイルの変更

今回はconf/application.ymlに設定を書きます。

grails:
    ....

    plugin.springsecurity:
        userLookup.userDomainClassName: 'rest.s2.sample.auth.User'
        userLookup.authorityJoinClassName: 'rest.s2.sample.auth.UserRole'
        authority.className: 'rest.s2.sample.auth.Role'
        filterChain.chainMap:
            #Stateless chain
            - {pattern: '/**', filters: 'JOINED_FILTERS,-anonymousAuthenticationFilter,-exceptionTranslationFilter,-authenticationProcessingFilter,-securityContextPersistenceFilter,-rememberMeAuthenticationFilter'}
        rest.token:
                validation:
                    useBearerToken: false
                    headerName: 'X-Auth-Token'

application.groovyにあるSpring Security関連の設定はすべてコメントアウトします。

実行結果を確認

では、実行確認です。run-appして、Invoke-RestMethodコマンドを実行していきます。

認証情報のない状態で、先ほどと同じようにbookの登録を行おうとすると、403エラーになります。

PS C:\> Invoke-RestMethod -Uri "http://localhost:8080/book" -Method POST -Body @{name="Sample Book";author="Sample Author"}
Invoke-RestMethod : リモート サーバーがエラーを返しました: (403) 使用不可能
発生場所 行:1 文字:1
+ Invoke-RestMethod -Uri "http://localhost:8080/book" -Method POST -Bod ...
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : InvalidOperation: (System.Net.HttpWebRequest:HttpWebRequest) [Invoke-RestMethod]、WebException
    + FullyQualifiedErrorId : WebCmdletWebResponseException,Microsoft.PowerShell.Commands.InvokeRestMethodCommand

なのでまずは認証をし、アクセストークンを得ます。実行したスクリプト

$user = @{
    username='admin'
    password='admin'
}
$json = $user | ConvertTo-Json
$response = Invoke-RestMethod 'http://localhost:8080/api/login' -Method POST -Body $json -ContentType 'application/json'
echo $response.access_token

<実行結果>

PS C:\> $user = @{
>>     username='admin'
>>     password='admin'
>> }
PS C:\> $json = $user | ConvertTo-Json
PS C:\> $response = Invoke-RestMethod 'http://localhost:8080/api/login' -Method POST -Body $json -ContentType 'application/json'
PS C:\> echo $response.access_token
eyJhbGciOiJIUzI1NiJ9.eyJwcmluY2lwYWwiOiJINHNJQUFBQUFBQUFBSlZTdlVcL2JRQlJcL1RoTlJnVlNnRWtnZFlBRTI1RWgwek1SbjFjb05xR2tXa0VBWCsrRWVu
5cGFKRXFkZTNJZjlJdVwvUU5RTzNSbDd0cDNodURBZ25xVFwvZTduMzlmenhSVlVqSWJuc1daY0dEOFZXY3lsYjFMTlpXd3d6RFMzSFQ4enFDTzBPZUpGRG16U0JLNlBW
4cXdvbTQrcG02d0JEVzJ0cldGSTZ2bUhjMXl6QkU2VVBcL1Z2dVVHbThJMUJRZTk5S01MSU5reXdNVlNadFhjbjFkc28xUnRzd1Vjd0NGUjY2MFZSSU55Z3RaOElNUTBk
sUTVHZ3ZqMTJZenkwVzFnYllXd09PVUdVUHU3aVZwV0dmZDNUdWJraEljd1hzb3QxT1BEblczNEtDKzRcL0ZYbFJDVW1pdHA1cHN5VVJIZjUwNmMrTHN6WnpcLzdYN3ZO
wMHYrXC8rbmMyTDlrSUwwMFBXQzFpdG5aS2J5WUw1clVhbmZQbGw2OVA1MVllZFI2VHNFQnZcL3Y0XC81NVp2bU9xc3FTWmxtVmczdGlHaFB5dTZaeUZjZUpoOXNvZU0z
waWlsdldTZ3o2dGpENlpqTlkzMXRlZVwvMnk3bDRyTEVxNEpOVW5lV2kzTFQ5UXRLdmVuNDhcLytuT1wvaU9FVlZJNlp5SkE2bnloQTlTeHBvVDY5T0o4WitcL3k3bHlj
SQXdBQSIsInN1YiI6ImFkbWluIiwicm9sZXMiOlsiUk9MRV9BRE1JTiJdLCJleHAiOjE1MDE2NTI5ODcsImlhdCI6MTUwMTY0OTM4N30.P2n42_q6WSNY9weZad23aRSP

次にBookの登録です。

$headers = New-Object "System.Collections.Generic.Dictionary[[String],[String]]"
$headers.Add("X-Auth-Token", $response.access_token)
Invoke-RestMethod -Uri "http://localhost:8080/book" -Method POST -Body @{name="Sample Book";author="Sample Author"} -Headers $headers

<実行結果>

PS C:\> $headers = New-Object "System.Collections.Generic.Dictionary[[String],[String]]"
PS C:\> $headers.Add("X-Auth-Token", $response.access_token)
PS C:\> Invoke-RestMethod -Uri "http://localhost:8080/book" -Method POST -Body @{name="Sample Book";author="Sample Author"} -Headers $headers

id name        author
-- ----        ------
 1 Sample Book Sample Author

ちなみに、アクセストークンが誤っていた場合は、401エラーになります。

ということで無事、認証つきでRESTのAPIをたたくことができました。
これだけ手軽に認証機能つきのWeb APIが作れるようになり、Grailsはとても便利に進化していると思います。