saba1024のブログ

どうでも良い思いついた事とかプログラミング関係のメモとか書いていきます。

Apache Groovyで画像処理の基礎

本記事はG* Advent Calendar 2017の21日目の記事です。

ピクセル

Apache Groovyでも、Javaが用意しているBufferedImageImageIOを利用することで、ピクセル単位で画像を取り扱うことが出来ます。
ピクセルの正体は単なるInteger(32bit)です。
そしてその32bitの数値を8bitずつの合計4つに区切って、先頭ビットからそれぞれAlphaRedGreenBlueの値として扱います。

つまり, 11111111 00000000 10101010 11001100
というIntegerは、
11111111(Alpha)
00000000(Red)
10101010(Green)
11001100(Blue)

となります。 ただし、単純にInteger(10進数)の計算では上記の値をそれぞれ取得するのは難しいので、ビット演算(右シフト)で求めてあげます。
ついでに、読み込みと書き込みに必要なリソースを提供してくれるメソッドと保存に利用するメソッドを追加して、以下のようなクラスを用意しました。

import java.awt.image.BufferedImage
import javax.imageio.ImageIO

class ImageUtil {
    static Integer alpha(Integer v) {v >>> 24}
    static Integer red(Integer v) {v >> 16&0xff}
    static Integer green(Integer v) {v >> 8&0xff}
    static Integer blue(Integer v) {v&0xff}
    static Integer rgb(Integer red, Integer green, Integer blue) {
        0xff000000 | red << 16 | green << 8 | blue
    }
    static Integer rgb(Integer alpha, Integer red, Integer green, Integer blue) {
        alpha << 24| red << 16 | green << 8 | blue
    }

    static withStreams(File rawFile, Closure clj) {
        BufferedImage bufReader = ImageIO.read(rawFile)
        BufferedImage bufWriter = new BufferedImage(bufReader.width, bufReader.height, BufferedImage.TYPE_INT_RGB)
        clj(bufReader, bufWriter)
    }

    static save(BufferedImage buf, String fileName) {
        File file = new File("${fileName}")
        ImageIO.write(buf, "jpg", file)
    }
}

上記のコードを利用しつつ、以下の画像を加工してみます。

f:id:saba1024:20170523213245j:plain

ネガポジ変換

String dir = "/home/koji/work/groovy/image-processing"
String fileName = "test.jpg"

File file = new File("${dir}/${fileName}")

// ネガポジ変換
ImageUtil.withStreams(file) { BufferedImage bufReader, BufferedImage bufWriter ->
    Integer width = bufReader.width
    Integer height = bufReader.height

    // for each line(y)
    (0..<height).each {Integer y ->
        // for each columns(x)
        (0..<width).each {Integer x ->
            Integer colorInfo = bufReader.getRGB(x, y)
            Integer red = 255 - ImageUtil.red(colorInfo)
            Integer green = 255 - ImageUtil.green(colorInfo)
            Integer blue = 255 - ImageUtil.blue(colorInfo)
            Integer rgb = ImageUtil.rgb(red, green, blue)
            bufWriter.setRGB(x, y, rgb)
        }
    }
    ImageUtil.save(bufWriter, "${dir}/negaposi.jpg")
}

f:id:saba1024:20171220192320j:plain

左右反転

String dir = "/home/koji/work/groovy/image-processing"
String fileName = "test.jpg"

File file = new File("${dir}/${fileName}")
// 左右反転
ImageUtil.withStreams(file) { BufferedImage bufReader, BufferedImage bufWriter ->
    Integer width = bufReader.width
    Integer height = bufReader.height
    Integer maxIndexX = width - 1
    // for each line(y)
    (0..<height).each {Integer y ->
        // for each columns(x)
        (maxIndexX..0).each {Integer x ->
            Integer colorInfo = bufReader.getRGB(x, y)
            bufWriter.setRGB(maxIndexX - x, y, colorInfo)
        }
    }
    ImageUtil.save(bufWriter, "${dir}/reversal.jpg")
}

f:id:saba1024:20171220192331j:plain

縦に分割

画像を縦に5つに分割して、最後の分割分から順番に左から描画します。

String dir = "/home/koji/work/groovy/image-processing"
String fileName = "test.jpg"

File file = new File("${dir}/${fileName}")

ImageUtil.withStreams(file) { BufferedImage bufReader, BufferedImage bufWriter ->
    Integer width = bufReader.width
    Integer height = bufReader.height
    Integer splitCount = 5
    List <List<Integer>>splited = []
    Integer splitedWidth = width / splitCount
    Integer remainingPixels = width % splitCount

    Closure getIndexes = {Integer rowIndex ->
        Integer startIndex = (splitedWidth - 1) * rowIndex + rowIndex
        Integer endIndex = startIndex + (splitedWidth - 1)
        [startIndex, endIndex]
    }

    splitCount.times {Integer rowIndex ->
        List<Integer> rowPixcels = []
        def (startIndex, endIndex) = getIndexes(rowIndex)
        (0..<height).each {Integer y ->
            (startIndex..endIndex).each {Integer x ->
                rowPixcels << bufReader.getRGB(x, y)
            }
        }
        splited << rowPixcels
    }

    // 分割した時に、余っている部分があれば救済
    // 今回はとりあえず最後の分割画像に余りを単純に追加
    if (remainingPixels > 0) {
        Integer maxIndexX = width - 1
        Integer remainIndexX = remainingPixels -1
       (0..<height).each {Integer y ->
            (0..remainIndexX).each {Integer x ->
                splited.last() << bufReader.getRGB(width -1 - x, y)
            }
        }
    }

    // リスト内容を反転させて描画
    splited.reverse().eachWithIndex {List<Integer> column, rowIndex ->
        def (startIndex, endIndex) = getIndexes(rowIndex)
        (0..<height).each {Integer y ->
            (startIndex..endIndex).eachWithIndex {Integer x, Integer index ->
                def path = index + ((splitedWidth) * y)
                bufWriter.setRGB(x, y, column[path])
            }
        }
     }
    ImageUtil.save(bufWriter, "${dir}/split.jpg")
}

f:id:saba1024:20171220200733j:plain

以上です。
画像を扱うコード自体はなんだかオールドスクールな感じになりました。
AIだ機械学習だという流れの昨今、画像処理に関する知識も求められるようになっていますので、最も基礎的なピクセル単位での画像の取り扱いをまとめてみました。

Rustでcatコマンドを実装

Rustでcatコマンドを実装しました。
ちゃんと標準入力からでも引数で指定されたファイルからでも読み込めます。
ミソになるのが、stdioでもFileでもBufReadというトレイトを実装しているので、そのトレイトの型を受け取って共通の処理が実現出来る点です。

use std::io::BufReader;
use std::io::BufRead;
use std::io::Stdin;
use std::fs::File;
use std::env;

fn main() {
    println!("cat implimented by Rust");
    let args: Vec<String> = env::args().collect::<Vec<String>>();

    // 標準入力から読み込み
    if args.len() <= 1 {
        let stdin: Stdin = std::io::stdin();
        // Readトレイトを実装している構造体であればBufReader::newに渡せる。
        let mut buf_file = BufReader::new(stdin);
        do_cat(&mut buf_file);

    // 引数で指定されたファイル(1つ以上)
   } else {
        args[1..].iter().for_each(|file_name| {
            match File::open(file_name) {
                Ok(file) => {
                    // Readトレイトを実装している構造体であればBufReader::newに渡せる。
                    let mut buf_file = BufReader::new(file);
                    do_cat(&mut buf_file);
                }
                Err(e) => println!("{}: {}", file_name, e),
                }
        })
    }
}

fn do_cat(stream: &mut BufRead) -> () {
    let mut buffer = String::new();
    loop {
        match stream.read_line(&mut buffer) {
            Ok(0) => break, // EOF
            Ok(_) => {
                print!("{}", buffer);
                buffer.clear();
            }
            Err(e) => {
                println!("{}", e);
                break;
            }
        }
    }
}

Apache GroovyでSSL証明書の有効期限を確認する

本記事はG* Advent Calendar 2017の19日目の記事です。

さて、2016年から正式に公開されたLet's Encryptを利用することで、誰でも無料でSSLを導入してセキュアな通信をお手軽に導入できるようになりました。
しかしLet's Encryptの証明書の有効期限は3ヶ月となっており、油断していると有効期限が切れていてエラーが表示されるようになりかねません。
opensslコマンド等を利用すればSSLの有効期限等が取得できるので、Zabbixなどで定期的に実行、確認してあげるようにする必要が有ります。

しかしWindowsだとopensslコマンドが。。。などなど問題が有りますので、今回Apache GroovyでSSLの有効期限を取得するスクリプトを書いてみました。
これであればLinuxでもWindowsApache Groovyが動くようにしておけば問題なく実行することが出来ます。

import java.security.cert.X509Certificate;
import javax.net.ssl.HttpsURLConnection;
import java.security.cert.Certificate;
import java.security.Principal

// SSL情報を確認したいサーバに接続
URL url = new URL("https://www.example.com")
HttpsURLConnection con = url.openConnection()
con.connect()

// サーバの主体(?)を取得する。
// 下記のgetServerCertificates()では複数の証明書が返ってくるので、このPeerPrincipalのhashCodeと比較して実際のSSL証明書情報を取得するために利用する。
Principal peerPrincipal = con.getPeerPrincipal()

// サーバーの証明書チェーンを取得する
List<Certificate> certs = con.getServerCertificates()
Certificate certificate = certs.findAll {
    it instanceof X509Certificate
}.find {
    it.getSubjectDN().hashCode() == peerPrincipal.hashCode()
}

// あとはCertificateクラスのメソッドなどを利用してSSL情報を取得できる。以下が有効期限の取得方法。
Date expirationDate = certificate.getNotAfter()
println expirationDate

con.disconnect()

すこし手こずった部分が、サーバから返ってくる証明書が複数あるので、どの証明書の有効期限を確認すれば良いか、という部分です。
コメントに書いてありますが、実際に利用されるSSL証明書の情報はgetPeerPrincipal()で取得できるので、サーバから返される証明書の一覧から、これと一致するものを取得するようにする必要が有ります。
なお、Principal#getName()で比較することも出来ますが、これだと同じ内容でもgetPeerPrincipal()で取得するPrincipalと証明書一覧からそれぞれ取得するPrincipalで文字列内のスペースがtrimされていたりして上手く比較できるパターンと出来ないパターンが混在しています。
そのため、比較はhashCode()で行うようにしています。

Java標準の組み込みWebサーバをApache Groovyで利用する

本記事はG* Advent Calendar 2017の18日目の記事です。

@tyamaさんのブログで紹介されていた、@glaforgeさんのこのツイートで初めて、Javaに組み込みWebサーバが存在していることを知りました!

ツイート内のコードだとシングルスレッド(HttpServer#start()を実行したスレッド)で全てのアクセスが同期して処理されるらしいので、事前にExecutorを定義してHttpServer#setExecutor()で渡してあげてマルチスレッドでWebサーバを動かすようにしてみました。

import com.sun.net.httpserver.HttpServer
import com.sun.net.httpserver.HttpExchange
import java.util.concurrent.Executor

class DirectExecutor implements Executor {
    public void execute(Runnable r) {
        // r.run() <--- これを実行すると同期処理にすることも出来る。
        new Thread(r).start()
    }
}

HttpServer.create(new InetSocketAddress(8081), 0).with {
    createContext("/hello") { HttpExchange http ->
    println """
        request method: ${http.requestMethod}
        protocol: ${http.protocol}
        remoteAddress: ${http.remoteAddress}
        headers: ${http.requestHeaders}
        URI: ${http.requestURI}
        body: ${http.requestBody}
    """
        http.responseHeaders.add("Content-type", "text/plain")
        http.sendResponseHeaders(200, 0)
        http.responseBody.withWriter {out ->
            out << "Hello ${http.remoteAddress.hostName}"
        }
    }
    executor = new DirectExecutor()
    start()
}

コレを適当なファイル名で保存しておいて、groovyコマンドで実行するだけでWebサーバの完成です。
これで、

curl -X POST http://localhost:8081/hello?a=b --data "hoge" --data "{a:b, c:d}"

というリクエストを投げると、コンソールには

request method: POST
protocol: HTTP/1.1
remoteAddress: /127.0.0.1:42410
headers: [Accept:[*/*], Host:[localhost:8081], User-agent:[curl/7.35.0], Content-type:[application/x-www-form-urlencoded], Content-length:[15]]
URI: /hello?a=b
body: hoge&{a:b, c:d}

と出力されます。

ちょっとしたツールとかサービスなんかは、古き良き時代(?)のPHPスクリプトのように、Webサーバに結びついていてPHPスクリプトをその場で修正すれば即反映される、というような気軽な感じで作りたいという欲求が合ったので、すでに今回のG* Advent Calendar 2017で投稿したGroovyだけでWebサーバを実装する方法を応用すれば、このJava組み込みWebサーバでGroovyスクリプトを動かせるな〜とちょっとワクワクしています。

Apache Groovy "as" オペレータの正体

これはG* Advent Calendar 17日目の記事です。

ある値を型Aから型Bに変換するといったコードは頻繁に出てきますよね。
キャストで行えるものが最もシンプルですが、キャストだと変換できないパターンというものが大量に存在します。
例えば以下のコードはそれぞれ例外が発生します。

Integer a = "123"
Integer a = (Integer) "123"

例外の内容は共に、キャストできません、というものです。

org.codehaus.groovy.runtime.typehandling.GroovyCastException: Cannot cast object '123' with class 'java.lang.String' to class 'java.lang.Integer'

さて、そこで今回のネタである強制変換オペレータasを使ってみます。

Integer a = "123" as Integer

なんと、こちらは問題なく動作しました。
ということは、どうやらキャストとasオペレータは別物のようです。

asの正体

このasオペレータ、実はレシーバに定義されているasTypeというメソッドを実行しています。
このasType()はクラス自体を引数として取り、その指定されたクラスを元にどんな変換処理をするか、という処理を定義することが出来ます。
なので、上記のコードは

Integer a = "123".asType(Integer)

と等価です。
つまり上記のコードは、"123"というStringクラスのインスタンスなので、Stringクラスに定義されているasTypeメソッドが実行されて、その中でIntegerの値を生成してreturnしてくれている、ということになります。

具体例

class Person {
    String name
    Integer age
    String toString() {
        "Person name:${name}, age:${age}"
    }
    
    // これ!
    def asType(Class convertTo) {
        // 別にクラスは一つだけじゃなくて、複数指定することも出来る。
        switch (convertTo) {
            case Developer:
                return new Developer(person: this, language: "Apache Groovy")
                break;
            case Integer:
                return age
            default:
                throw new ClassCastException("Person cannot be coerced into ${convertTo}")
        }
    }
}

class Developer {
    Person person
    String language
        String toString() {
        "${language} Developer!! ---> ${person}"
    }
}

Person p = new Person(name: "koji", age: 32)

// 継承関係も何も無いクラスへ変換!
Developer d = p as Developer

assert p instanceof Person
assert !(p instanceof Developer)
assert d instanceof Developer
assert !(d instanceof Person)

assert p.toString() == "Person name:koji, age:32"
assert d.toString() == "Apache Groovy Developer!! ---> Person name:koji, age:32"
assert 32 == p as Integer

// 参照を持っているのでメンバ変数を変更すると両方変る。
p.age = 99
assert p.toString() == "Person name:koji, age:99"
assert d.toString() == "Apache Groovy Developer!! ---> Person name:koji, age:99"

// Integerにもasで変換できるようにしている。
assert 99 == p as Integer

Groovyにもtraitが追加された今では、asTypeで実現できることはtraitでも出来るような気がするので、自分のコードでasTypeを定義する機会はあまり無いな気もしますが、普段何気なく使っているasオペレータの動作を知ることで一歩Groovyの深みに近づけた感じです。

Grails/Vue.jsでTodoアプリ(Deploy as war)

これはG* Advent Calendar 2017の11日目の記事です。

この記事は、以下の5つの投稿で成り立っています。

Grails/Vue.jsでTodoアプリ(環境構築)
Grails/Vue.jsでTodoアプリ(CRUD)
Grails/Vue.jsでTodoアプリ(認証) 1/2
Grails/Vue.jsでTodoアプリ(認証) 2/2
Grails/Vue.jsでTodoアプリ(Deploy as war)

初めに

Webサーバ(今時で言うフロントエンド)とApplicationサーバ(Grails)を分けて、それぞれ別々にデプロイするのであれば特に考慮することはありません。
が、小さなWebアプリなどをVue.js&Grailsで開発するのであれば、Grailsのwarで一緒に簡単一発デプロイ出来た方が素敵です。
コレができればもうWebサーバもApplicationサーバも必要なく、javaコマンド一発でVue.jsとGrailsを駆使したWebアプリケーションを公開することが出来ます!
そのための手順をココでご紹介します。

前提条件

Grailsで作成したアプリーションのディレクトリは今後$GRAILS_PROJECTとし、Vue.jsのアプリケーションのディレクトリはその直下なで$VUE_PROJECTと記述します。
私の環境では、$GRAILS_PROJECT/home/koji/work/grails/gvtodoで、$VUE_PROJECT$GRAILS_PROJECT/frontendになっています。
また、上記の4つの記事で作成してきたアプリケーションをベースとして本文を記述しています。

Grails側の設定

さて、Vue.jsをGrailsのwarに含めるために、幾つかGrails側で設定して上げる必要が有ります。
なぜかというと、開発時はGrailsをポート8080、Vue.jsアプリをポート8081で捌いていたので問題ありませんでしたが、Vue.jsもGrailsのwarに含めるということはWebもAppも両方同じポート(今回は8080。変更可)で動くことになります。
そのため、Grailsが提供するAPI以外へのアクセスは全て、SPAであるVue.jsに流して上げる必要が有ります。

Vue.jsにアクセスを流すコントローラの作成

専用のAPI以外へのアクセスをVue.jsで作成したSPAにそのまま流してあげるために、専用のControllerを1個追加します。名前は何でも良いので、今回はIndexとしました。

grails> create-controller index
| Created grails-app/controllers/gvtodo/IndexController.groovy
| Created

IndexController.groovyの中身は以下のようになります。

package gvtodo

class IndexController {

    def index() {
        // webappから読み取り。develop時は問題ないけど、warでのproductionモードだと見つからない。
        // render file: grailsApplication.mainContext.getResource("index.html").file
        // render file: grailsApplication.classLoader.getResourceAsStream('index.html').text
        // render file:
        
        // ということで、Vue.jsのindex.htmlはsrc/main/resourcesに格納しておいてそこから読み込むようにする必要が有る。
        response.contentType = 'text/html'
        response.outputStream << grailsApplication.classLoader.getResourceAsStream('index.html')
        response.outputStream.flush()
    }
}

そして、API以外の全てのアクセスはこのIndexControllerのindexアクションに流すように、UrlMapping.groovyを変更します。

package gvtodo

class UrlMappings {
    // これを追加
    // src/main/webappディレクトリ用に、/staticへのアクセスはこのUrlMappingでは処理しない
    static excludes = ["/static/**"]
    
    static mappings = {
        "/manual/items"(resources: 'item')
        "/$controller/$action?/$id?(.$format)?"{
            constraints {
                // apply constraints here
            }
        }

        // "/"(view:"/index") コメントアウト
        "/**"(controller: "index", action: "index") // これを追加
        "500"(view:'/error')
        "404"(view:'/notFound')
    }
}

Vue.jsのために認証の設定を修正

すでにAPI自体はSpringSecurityCoreSpringSecurityRESTプラグインによってログインしていなければ利用できないようになっています。
APIに該当しないURLの場合は全てVue.jsに流すようにしますので、Vue.jsへのアクセス自体に認証をかけてしまうと誰も使えなくなってしまいます。
ということで、$GRAILS_PROJECT/grails-app/conf/application.groovyを以下のように一行だけ修正します。
修正箇所はinterceptUrlMap内の一番最後のpattern: '/**'の部分のみです。

// Added by the Spring Security Core plugin:
...変更ないので省略...

grails.plugin.springsecurity.interceptUrlMap = [

         // for SpringSecurityREST
         [pattern: '/oauth/**',           access: ['permitAll']],
         [pattern: '/api/**',             access: ['ROLE_USER', 'ROLE_ADMIN']],
         [pattern: '/manual/**',          access: ['ROLE_USER', 'ROLE_ADMIN']],

        // for /dbconsole
        [pattern: '/login/**',           access: ['permitAll']],
        [pattern: '/dbconsole/**',       access: ['ROLE_ADMIN']],

        // この1行だけ修正した。
        // Vue.jsがこれに該当するので、コレ自体は誰でもアクセスOKにする。
        [pattern: '/**',                 access: ['permitAll']]
]

grails.plugin.springsecurity.filterChain.chainMap = [
...変更ないので省略
]

本番用DBの設定(今回の記事用)

warでGrailsを動作させるとデフォルトで本番用モードで起動します。
そうするとDB自体は事前にスキーマやデータ等を用意しておかないといけないので、ちょっと今回はその手間を省くために、productionモードでもdbCreateをcreate-dropにしておきます。

  • $GRAILS_PROJECT/grails-app/conf/application.yml
...省略...
environments:
    development:
        dataSource:
            dbCreate: create-drop
            url: jdbc:h2:mem:devDb;MVCC=TRUE;LOCK_TIMEOUT=10000;DB_CLOSE_ON_EXIT=FALSE
    test:
        dataSource:
            dbCreate: update
            url: jdbc:h2:mem:testDb;MVCC=TRUE;LOCK_TIMEOUT=10000;DB_CLOSE_ON_EXIT=FALSE
    production:
        dataSource:
            # dbCreate: none
            dbCreate: create-drop # これに変更。あくまで今回の確認用なので注意!
            url: jdbc:h2:./prodDb;MVCC=TRUE;LOCK_TIMEOUT=10000;DB_CLOSE_ON_EXIT=FALSE
...省略...

これでGrails側の設定は完了です。

Vue.js側の設定

ビルド設定の修正

続いて、Vue.jsを本番用にビルドする際の設定を修正します。
修正するファイルは、$VUE_PROJECT/config/index.jsです。

'use strict'
// Template version: 1.1.3
// see http://vuejs-templates.github.io/webpack for documentation.

const path = require('path')

module.exports = {
  build: {
    env: require('./prod.env'),
    // index: path.resolve(__dirname, '../dist/index.html'),
    // assetsRoot: path.resolve(__dirname, '../dist'),
    index: path.resolve(__dirname, '../../src/main/resources/index.html'), // 追加
    assetsRoot: path.resolve(__dirname, '../../src/main/webapp'), // 追加
    assetsSubDirectory: 'static',
    // assetsPublicPath: '/',
    assetsPublicPath: '/static/', // 追加
    productionSourceMap: true,
    // Gzip off by default as many popular static hosts such as
    // Surge or Netlify already gzip all static assets for you.
    // Before setting to `true`, make sure to:
    // npm install --save-dev compression-webpack-plugin
    productionGzip: false,
    productionGzipExtensions: ['js', 'css'],
    // Run the build command with an extra argument to
    // View the bundle analyzer report after build finishes:
    // `npm run build --report`
    // Set to `true` or `false` to always turn it on or off
    bundleAnalyzerReport: process.env.npm_config_report
  },
  dev: {
    env: require('./dev.env'),
    port: process.env.PORT || 8081,
    autoOpenBrowser: true,
    assetsSubDirectory: 'static',
    assetsPublicPath: '/',
    proxyTable: {},
    // CSS Sourcemaps off by default because relative paths are "buggy"
    // with this option, according to the CSS-Loader README
    // (https://github.com/webpack/css-loader#sourcemaps)
    // In our experience, they generally work as expected,
    // just be aware of this issue when enabling this option.
    cssSourceMap: false
  }
}

build:の中のindex:assetsRoot:assetsPublicPath:を修正しました。(既存のものをコメントアウトして新規に追加)
ビルドすると、index.htmlGrailsresourceseディレクトリへ、それ以外のファイル(JavaScriptCSS、画像等)はGrailswebappディレクトリ配下にコピーするようにしました。
本当は単純に全てwebappindex.htmlも含め、Vue.js系のファイルを放り込めればいいのですが、そうすると/でアクセスしてきた時に、GrailsのControllerがindex.htmlをrenderしようとしてもファイルが上手く見つからないなどの問題が発生しました。
そのため、細かく全ての問題をメモし忘れてしまったのですが、現状一番確実な方法はこれとなります。

URLの設定(必須ではない)

そして最後に、必須ではないのですが、$VUE_PROJECT/src/router/index.js内の、new Routerの部分を以下のように修正します。

const vr = new Router({
  mode: 'history', // これを追加してURLのハッシュ記号(#)を取る
  routes: [
    ...省略...
    { // 本当にAPI以外の全てのアクセスがVue.jsに流れてきているか確認するために以下を追加
      path: '/a/b/c',
      name: 'ToDoIndex2',
      component: ToDoIndex
    }
    ...省略...

warに固める

いよいよwarに固めてみます。
まず、yarn run buildで、必要なファイルをビルドしてGrailsディレクトリに配置します。(この設定を上記のindex.jsで行いました)

[koji:frontend]$ yarn run build
yarn run v1.3.2
$ node build/build.js
Hash: a013ba3d49397af14f5f
Version: webpack 3.8.1
Time: 11975ms
                                              Asset       Size  Chunks             Chunk Names
           static/js/vendor.077af75171293270864c.js     136 kB       0  [emitted]  vendor
              static/js/app.8ddd9f2f6956595dcdf6.js    15.1 kB       1  [emitted]  app
         static/js/manifest.fffa9c3719c553c5d3cf.js    1.49 kB       2  [emitted]  manifest
static/css/app.0d9dd6962ae03b505f9fdae53bae9d85.css  192 bytes       1  [emitted]  app
       static/js/vendor.077af75171293270864c.js.map     1.1 MB       0  [emitted]  vendor
          static/js/app.8ddd9f2f6956595dcdf6.js.map    65.9 kB       1  [emitted]  app
     static/js/manifest.fffa9c3719c553c5d3cf.js.map    14.3 kB       2  [emitted]  manifest
                            ../resources/index.html  471 bytes          [emitted]  

  Build complete.

  Tip: built files are meant to be served over an HTTP server.
  Opening index.html over file:// won't work.

Done in 16.04s.
[koji:frontend]$ 

そしてGrailsは普通にwarコマンドを実行するだけです。

grails> war
:compileJava UP-TO-DATE
:compileGroovy
:findMainClass
:assetCompile
Processing File 1 of 25 - apple-touch-icon-retina.png
Processing File 2 of 25 - favicon.ico
...省略...
:bootRepackage
:assemble

BUILD SUCCESSFUL

Total time: 12.179 secs
| Built application to build/libs using environment: production
grails> 

Gradleの機能を使えば普通に1コマンドで出来るようになると思いますが、実はGradleはほぼまともに触ったことが無いので今回はスルー. これで完了です。

warを実行する

warファイルは$GRAILS_PROJECT/build/libs/gvtodo-01.warとして作成されています。 実際にこれを適当なディレクトリに持って行って実行してみます。

[koji:gvtodo]$ mv build/libs/gvtodo-0.1.war $HOME/Desktop 
[koji:gvtodo]$ cd $HOME/Desktop 
[koji:Desktop]$ java -jar gvtodo-0.1.war 

Configuring Spring Security Core ...
... finished configuring Spring Security Core


Configuring Spring Security REST 2.0.0.M2...
... finished configuring Spring Security REST

Grails application running at http://localhost:8080 in environment: production

起動しました。 これで、http://localhost:8080/や、http://localhost:8080/a/b/cにアクセスすれば、ちゃんとwarの中でVue.jsが動いていることが確認できるはずです。

どんな時にこんなとするの?

さて、今回態々Grailsのwar自体にVue.jsをまるまる放り込みましたが、そもそもフロントエンドとバックエンドはAPIを通してやり取りする粗結合なものにするのが世の流れなので、今回のようにwarの中にフロントエンドであるVue.jsまで含めてしまうのはあまり良い考えでは無いと思います。
ちょっとしたHTMLの修正もGrailsTomcat)の再起動が必要にもなります。
ではいつ今回の内容が役立つかというと、それは恐らくGrails製アプリケーションをOSSとして公開、配布するタイミングだと思われます。
ユーザには単純にwarファイルを落としてもらって、javaコマンド一発でWebアプリケーションが起動する、という手軽さを提供することが出来ます。JenkinsやGitbucketと同じ手法です。
かく言う私も現在、この方法を用いたGrails製のOSSアプリケーションを作成中です。
本来は今年の夏頃に公開したかったのですが中々思うように時間をとれずに公開までには至っていません。
自分が仕事で使いたいツールだったので、すでに一応個人的に仕事で導入して利用しているので、来年のアドベントカレンダーの頃には公開出来れば良いな〜と思っています。

Grails/Vue.jsでTodoアプリ(認証) 2/2

これはG* Advent Calendar 2017の10日目の記事です。

この記事は、以下の5つの投稿で成り立っています。

Grails/Vue.jsでTodoアプリ(環境構築)
Grails/Vue.jsでTodoアプリ(CRUD)
Grails/Vue.jsでTodoアプリ(認証) 1/2
Grails/Vue.jsでTodoアプリ(認証) 2/2
Grails/Vue.jsでTodoアプリ(Deploy as war)

初めに

昨日の記事でRESTful APIで利用できる認証機構を追加しました。
今日はさらに追加で、アクセストークンの更新方法とログアウト、そしてSpringSecurityCoreとSpringSeucirtyRESTの組合せで発生するちょっとした問題の回避方法ご紹介します。

前提条件

Grailsで作成したアプリーションのディレクトリは今後$GRAILS_PROJECTとし、Vue.jsのアプリケーションのディレクトリはその直下で$VUE_PROJECTと記述します。
私の環境では、$GRAILS_PROJECT/home/koji/work/grails/gvtodoで、$VUE_PROJECT$GRAILS_PROJECT/frontendになっています。

access_tokenのリフレッシュ

なぜ?

SpringSecurityRESTがクライアントに発行してくれるaccess_tokenを使って、認証/認可が出来るようになりました。
しかし、実はこのaccess_token、デフォルトの設定だと3600秒(1時間)しか有効期限がありません。
1時間経過した後に、その有効期限の切れたaccess_tokenを使ってRESTful APIにアクセスすると、HTTPレスポンスコード401が返されます。(つまりログインしていないのと実質同じ)

ではどうすれば良いのか?
単純にトークンの有効期限を伸ばすことも可能です。
その場合、application.groovy内に

grails.plugin.springsecurity.rest.token.storage.jwt.expiration = 秒数

と指定することで可能です。ただ、コレでもやはり根本的な問題の解決ではありません。
SpringSecurityRESTでは、refresh_tokenを使って、新しいaccess_tokenを取得することが可能です。
access_tokenrefresh_tokenも、ログイン成功時にSpringSecurityRESTから渡されています。

なお、すでに述べた通り、access_tokenには制限期間があって、application.groovyでその値を変更することが出来ますが、このrefresh_tokenには制限期間はありません。

リフレッシュ方法

実際にaccess_tokenをリフレッシュするには、/oauth/access_tokenに、POSTメソッドでリクエストを投げます。
送信しなければならないデータは、grant_typeと、ログイン時に渡されたrefresh_tokenの2種類だけです。

const url = `http://localhost:8080/oauth/access_token`
// Content-Type: application/x-www-form-urlencodedで送らないとダメなので、そのためにURLSearchParamsを使って構築。
// Vue.jsではくて、axiosでそれを実現する方法
const params = new URLSearchParams()
params.append('grant_type', 'refresh_token')
params.append('refresh_token', UserInfoRepository.get().getRefreshToken())

axios.post(url, params)
  .then((response) => {
    // 新しいaccess_tokenを含む認証情報が再度得られるので、ココで再保存。
    UserInfoRepository.save(response.data)
})

上記のようコードを実行することで、新しいaccess_tokenが取得できるので、それを利用することでユーザは常にログインしている状態に出来ます。

しかし、もし上記ようにaccess_tokenのリフレッシュをリクエスト時に確認してしまうと、その画面で1時間放置しておいてToDoの追加ボタンを押すと、当然その際にリクエストヘッダーに付与されるaccess_tokenはSpringSecurityREST側ではタイムアウト扱いになり、401が返されます。
その場合は、一旦画面をリフレッシュすれば新しいaccess_token再セットされますが、スマートではないですよね。

ということで、ToDoのRESTful APIを実行する前に必ずこのaccess_tokenをリフレッシュする処理を実行するように、以下のようにコードを変更してみました。

  • $VUE_PROJECT/src/components/todo/Index.vue
<template>
  <div>
    <h1>ToDoリスト</h1>
    <input v-model="title">
    <button @click="add">add</button>
    <ul>
      <li v-for="todo in todoList">
        <input
          class="todo-title"
          v-bind:value="todo.title"
          @keyup.enter="edit($event, todo.id)"
          @blur="edit($event, todo.id)">

        &nbsp;<button @click="remove(todo.id)">del</button>
      </li>
    </ul>
  </div>
</template>
<script>
  import axios from 'axios'
  import UserInfoRepository from '@/UserInfoRepository'

  let errorFunc = (error) => {
    alert(`HTTP Status: ${error.response.status}, [${error.response.data.message}]`)
  }

  // const baseURL = 'http://localhost:8080/todo'
  const baseURL = 'http://localhost:8080/manual/items'

  export default {
    data: function () {
      return {
        title: '',
        todoList: []
      }
    },
    mounted: function () {
      this.list()
    },
    methods: {
      api: function (func) {
        const url = `http://localhost:8080/oauth/access_token`

        // Content-Type: application/x-www-form-urlencodedで送らないとダメなので、そのためにURLSearchParamsを使って構築
        const params = new URLSearchParams()
        params.append('grant_type', 'refresh_token')
        params.append('refresh_token', UserInfoRepository.get().getRefreshToken())

        axios.post(url, params)
          .then((response) => {
            // 新しいaccess_tokenを含む認証情報が再度得られるので、ココで再保存。
            UserInfoRepository.save(response.data)
            func()
          })
      },

      list: function () {
        this.api(() => {
          axios.get(`${baseURL}`, {headers: UserInfoRepository.get().generateAuthHeader()})
            .then((response) => {
              this.todoList = response.data
            }).catch(errorFunc)
        })
      },
      add: function () {
        this.api(() => {
          axios.post(`${baseURL}`, {
            title: this.title
          }, {headers: UserInfoRepository.get().generateAuthHeader()})
            .then((response) => {
              this.title = ''
              this.list()
            })
            .catch(errorFunc)
        })
      },
      edit: function (event, id) {
        this.api(() => {
          axios.put(`${baseURL}/${id}`, {
            title: event.target.value
          }, {headers: UserInfoRepository.get().generateAuthHeader()})
            .then((response) => {
              this.list()
            })
            .catch(errorFunc)
        })
      },
      remove: function (id) {
        this.api(() => {
          axios.delete(`${baseURL}/${id}`, {headers: UserInfoRepository.get().generateAuthHeader()})
            .then((response) => {
              this.list()
            })
            .catch(errorFunc)
        })
      }
    }
  }
</script>
<style>
  .todo-title {
    border: none;
  }
</style>

新たにapi関数を用意して、この中でaccess_tokeknをリフレッシュしています。
そして各RESTful APIにアクセスする処理自体を無名関数化して、それを今回用意したこのapi関数の中で実行するようにしました。
これで常に新しいaccess_tokenが得られるようになりました。

UserInfoRepositoryを使って新しいaccess_tokenを含むレスポンスデータをlocalStorageに再保存していますので、その後実行する、渡された関数内では、UserInfoRepositoryは当然その新しいaccess_tokenを返してくれるので、ToDo用のRESTful APIには常に新しいaccess_tokenが利用できる、という流れになっています。

当然この方式は私がこの記事のために実装しただけなので、要件によって実装方法などは変わります。

これで、ユーザが意図的にlocalStorageを削除しない限り常にログイン状態を保てるようになりました。

ログアウト

永続的にログイン状態に出来るようになりました。
では当然必要になってくるログアウトも実装しましょう。
が、SpringSeurityRESTの公式ドキュメントを読むとデフォルトのJWTはそもそもステートレスなんだからサーバは状態を保持していない、つまりログアウトなんて不可能だよ!とのことです。
しかしどうしてもユーザにログアウトさせてあげたい、というのであれば単純にlocalStorageに保存している情報を削除すれば、access_tokenrefresh_tokenもユーザは参照できなくなり、結果的にログアウトしたことに出来ます。

ということでやることはとっても簡単!
今回は追加箇所のみ抜粋します。

  • $VUE_PROJECT/src/components/todo/Index.vue
<template>
  <div>
    <h1>ToDoリスト</h1>
        ...省略...
        &nbsp;<button @click="remove(todo.id)">del</button>

        <!--この1行をを追加-->
       <hr><a @click="logout">Logout</a>
      </li>
    </ul>
  </div>
</template>
<script>
    ....省略....
    methods: {
      ...省略...
      logout: function () {
        UserInfoRepository.logout()
        this.$router.push({name: 'Login'})
      }
    }
  }
</script>

これだけです!
ToDo一覧の一番下にログアウトリンクを追加したので、それをクリックすると単純にUserInfoRepository.logout()を実行して、ログインページにリダイレクトします。
このUserInfoRepository.logout()は、単純にlocalStorageからデータを削除しているだけです。

static logout () {
  localStorage.removeItem('userInfo')
}

これでユーザは実質ログインしたのと同じことになります。簡単ですね!

Grails側でユーザの情報を更新する際のワークアラウンド

さて、Grails側ではSpringSecurityCoreで各ユーザのステータスを管理できます。
デフォルトでは、SpringSecurityCoreのユーザ用のドメイン(今回のサンプルではAuthUser.groovy)を見ると

boolean enabled = true
boolean accountExpired
boolean accountLocked
boolean passwordExpired

が用意されています。
例えば、あるユーザのenabledfalseにすれば、そのユーザを今後ログイン出来なくすることが出来ます。
太字にした通り、実はそこに大きな落とし穴が有ります。
SpringSecurityCoreプラグインでは、上記の4つのユーザのステータスを更新(直接SQLでデータを更新でも)しても、その時点のログイン状態には影響せず、次回ログイン時にそのステータスをチェックするようになっています。
つまり今回のSpringSecurityRESTベースで、毎回自動的にrefresh_tokenを使ってaccess_tokenを更新する方式だと、BANさせたいユーザが居たとしても、永久にそのユーザがログインした状態になってしまいます。

実際に、GrailsのDBConsoelにアクセスして、以下のSQLを実行してみてください。

UPDATE AUTH_USER SET enabled = false WHERE id = 1;

間違いなくDB上のデータは更新されるのですが、すでにログインしているユーザは引き続き操作を問題なく行えてしまいます。
これは非常に不味いです。恐らくSpringSecurityCoreのinterface等を実装した自前の認証方法などを用意すれば回避することも出来そうですが、自分でそこまではたどり着けませんでした。
ということで、その問題を回避するワークアラウンドをご紹介します。

特に難しいことは全くなくて、単純にGrails3のInterceptorで、毎アクセスごとにそのユーザのステータスをチェックして、ステータスによって403を返したりするだけです。
まずはInterceptorを適当な名前で作成します。

grails> create-interceptor CheckUserStatusInterceptor
| Created grails-app/controllers/gvtodo/CheckUserStatusInterceptor.groovy
| Created src/test/groovy/gvtodo/CheckUserStatusInterceptorSpec.groovy

作成されたInterceptorを以下のようにします。

package gvtodo

import groovy.json.JsonBuilder
import org.springframework.dao.DataAccessResourceFailureException


class CheckUserStatusInterceptor {

    def springSecurityService

    CheckUserStatusInterceptor() {
        matchAll()
                .excludes(uri: '/login/**')
                .excludes(uri: '/oauth/access_token/**')
    }

    boolean before() {
        try {
            AuthUser user = springSecurityService.currentUser
            String errorMessage
            if (user != null && !user.enabled) {
                errorMessage = 'Sorry, your account is disabled.'
            }
            if (user?.accountLocked) {
                errorMessage = 'Sorry, your account is locked.'
            }
            if (user?.passwordExpired) {
                errorMessage = 'Sorry, your password has expired.'
            }
            if (user?.accountExpired) {
                errorMessage = 'Sorry, your account has expired.'
            }

            if (errorMessage) {
                response.status = 403
                JsonBuilder json = new JsonBuilder()
                json([message: errorMessage])
                render json
                return false
            }
            true
        } catch (DataAccessResourceFailureException e) {
            // What is this!!??
            // If user has no Authority, SpringSecurityCore returns 403.(Please check application.groovy)
            // But this interceptor is executed too!
            // This exception is thrown if be used springSecurityService when user has no authority.
            // Grails/SpringSecurityCore already preparation to return a 403 status to client.
            // There fore it return true simply here it is enough.
            // Could not obtain current Hibernate Session; nested exception is org.hibernate.HibernateException: No Session found for current thread
            true
        }
    }

    boolean after() { true }

    void afterView() {
        // no-op
    }
}

以上です!ココで一旦Grailsを再起動しておいてください。
この方法で全てのリクエストでユーザの情報を必ずチェックできるようになったので、ユーザがすでにログインしている状態で、再度GrailsのDBConsoelにアクセスして、以下のSQLで状態を更新します。

UPDATE AUTH_USER SET enabled = false WHERE id = 1;

これでログインしているユーザのToDo一覧画面でページをリロードすると、403が返り、ユーザはもうToDoにアクセスできなくなっています。

注意点

いかがだったでしょうか?個人的にオールドスクールなSpringSecurityCoreのみで認証するモノリシックなシステムを開発運用してきたので、中々今回のRESTful APIベースでの認証には手こずりました。。。

さて、前回も述べましたが、今回実装してはっきりわかると思いますが、トークン(access_tokenかrefresh_token)が第3者に漏れるとアカウントがっ取られます。
ただのHTTPヘッダーにトークンを埋め込むわけですので、SSL環境では絶対に利用するべきではありません!
幸いなことに、現在はLet's Encryptのように無料で利用できるSSLがあります。必ずSSL経由で認証を行うようにしましょう!

参考

Spring Security Core
Spring Security REST