Go言語のWEBフレームワークRevelを使用してセキュアなAPIを作成

# WHY

機械学習のモデルはできたけど、サービスインするには何らかの形でweb側から動作できる形にする必要があります。
Railsだと重すぎるし、Go言語使いたい。
そのようなモチベーションから私はGo言語でAPIを作成しました。

Go言語のWEBフレームワークは多々ありますが、今回紹介する記事は[revel](https://revel.github.io/)を用いています。
コンテナとコードを公開しているのですぐに動作を確認することが可能にしています。

Docker: https://hub.docker.com/r/masayaresearch/go_api/
github: https://github.com/SnowMasaya/go_revel_jwt_mysql

revelを採用した理由は下記です。

<img width="200" alt="DI_IMG_5648_TP_V.jpg" src="https://s3.amazonaws.com/f.cl.ly/items/0P1r2R2t2B1C350L1z0v/Image%202016-02-29%20at%2023.28.21.png">

- testのための機能が揃っている
- deployも簡単
- 必要なサンプルがある

# WHAT

何をすることを想定しているか
- 簡単な認証
- APIエンドポイントからパラメータ取得
- DBアクセス
- DBの結果を返却

MySQLに機械学習で予測した結果が入っていると想定してJWTによる認証からAPIを叩いてMySQLのデータを表示するAPIを作成しました。
必要なものは下記です。

- MySQLのDocker環境
- RevelのDocker環境

revelに必要な機能としては大きく分けると4つあります。

- API提供部分
- JWT認証部分
- MySQLへの接続及びデータをモデルに保存する部分
- MySQLの操作して結果を表示

# HOW

上記に示したWHATの部分をどのように実装しているか解説していきます。

## API提供部分

API提供部分の修正に必要なファイルは２つです。

- confファイル
 - routes
 - app.conf　 
- api_jwt_mysql.go

`routes`ファイルで必要な部分を抜粋します。どのようにエンドポイントを設定してアクセスするか設定します。

```
GET     /                                       App.Index
GET     /get_list                               Api_Jwt_Mysql.List

```

上の場合は２つのアクセスを用意しています。

App.Indexへのアクセス
http://{ipアドレス or localhost}/

Go言語で実装した`Api_Jwt_Mysql.List`のメソッドへのアクセス
http://{ipアドレス or localhost}/get_list

### 設定ファイル

`app.conf`で今回に必要な部分を説明します。

下記でアプリケーション名を設定しています。

```
# Application
app.name=Revel_JWT_MySQL
```

下記でポート番号とipアドレスを設定しています。今回はipアドレスを特別に設定する必要がないので空にしています。

```
# Server
http.addr=
http.port=9000
```

ログは4種類用意してくれています。

- `trace`：デバッグ用の情報
- `info`：情報（取得したい情報）
- `warn`：警告
- `error`：エラー

出力先を全て標準エラーにしていますが標準出力と使い分ける方が良いです。通常のログとエラーのログが混ざると人はエラーのログが見づらくなるので分ける方が良いです。また各ログにつけるプレフィックスも設定可能です。

```
# Logging
log.trace.output = stderr
log.info.output  = stderr
log.warn.output  = stderr
log.error.output = stderr

log.trace.prefix = "TRACE "
log.info.prefix  = "INFO  "
log.warn.prefix  = "WARN  "
log.error.prefix = "ERROR "
```

`prod`（本番用）と`dev`（開発用）で設定を変えることが可能です。例えばローカルで動作するDBと本番で動作しているDBを変えるなどで有用になります。

```
[dev]
mode.dev=true
watch=true
module.testrunner=github.com/revel/modules/testrunner
db.uri  = "172.17.0.1:3306"
db.name = "revel_jwt_mysqlapp"
```

### API部分

```go
func (c Api_Jwt_Mysql) List() revel.Result {
    var res models.Api_Jwt_Mysql
　　：
    c.Params.Bind(&res.Id, "Id")
    c.Params.Bind(&res.Date, "Date")
    c.Params.Bind(&res.Title, "Title")
    c.Params.Bind(&res.Picture, "Picture")
    c.Params.Bind(&res.Apitoken, "Apitoken")
```

`var res models.Api_Jwt_Mysql`の部分はapiから取得した値を設定しておく`model`を用意しておきます。

```go
type Api_Jwt_Mysql struct {
	Id            int
	Date          int
	Title         string
	Picture       string
	Apitoken      string
	All           string
}
```

パラメータの取得です。`c.Params.Bind`は`&`で渡しているパラメータ解析して`Go`のmodelに渡しています。

`c.Params.Bind(&res.Id, "Id")`の場合は下記でアクセスすると

http://{ipアドレス or localhost}/api_jwt_mysql&Id=1

`&res.Id`に1の値が代入されます。他の部分も同様のことをしています。

APIにパラメータをgo言語に渡す方法になります。

## JWT認証部分

JWTトークンを使用している理由は下記です。

- 認証用のトークンの期限を決められる。またRSA方式を利用すれば秘密鍵と公開鍵を用いた認証が可能
- HTTPヘッダーに載せられるため、非常に軽く、必要な情報を全て載せることが可能
- 認証のためにData Baseを必要としない。


apiトークンをエンドポイントのパラメータから取得して評価しています。

```go
            if val, ok := c.Params.Query["Apitoken"]; ok {
                check_token = CheckJWTHandler(c.Params.Query["Apitoken"][0])
                if check_token {
                    fmt.Printf("Success")
                }else {
                    fmt.Printf("Failed")
                }
            }else{
                fmt.Printf("Apitoken no setting. The Title is %#v", val)
            }
```


評価する関数はトークンを引数にしてboolean型で値を返す関数です。

```
func CheckJWTHandler(api_token string) (bool){
```

関数内でトークンが存在するかチェック

```go
    tokenString := api_token
    if tokenString == "" {
        return false
    }

```

ここがややこしいので丁寧に解説します。

```go
    token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
        if _, ok := token.Method.(*jwt.SigningMethodRSA); !ok {
            return nil, fmt.Errorf("Unexpected signing method: %v", token.Header["alg"])
        }
        return LookupPublicKey()
    })
```

下記のように”#_#”に置き換えます

```go
jwt.Parse(tokenString, #_#)
```

そうすると取得したトークンと"#_#"の部分をParseして比較していることが分ります。

"#_#"を展開していきます。

```go
jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {#_#})
```

ある関数から値を取得していることが分ります。
[メソッドレシーバー](https://go-tour-jp.appspot.com/methods/1)として`token *jwt.Token`を使用しています。
戻り値として`interface`と`error`を取得することが目的です。
関数の中身を見てみます。

```go
        if _, ok := token.Method.(*jwt.SigningMethodRSA); !ok {
            return nil, fmt.Errorf("Unexpected signing method: %v", token.Header["alg"])
        }
        return LookupPublicKey()
```

メソッドレシーバーを使用して`token.Method.(*jwt.SigningMethodRSA)`の部分でトークン認証用のメソッドとして正しいかをチェックしています。
正しければ`return LookupPublicKey()`公開鍵を読み込んで、`interface{}, error`に返しています。

```go
    if err != nil || !token.Valid {
        fmt.Println("Failed")
        return false
    }
    return true
```

`err`が`nil`でなくかつ`token`が`Valid`（有効）ならば`true`を返して、それ以外なら`false`を返しています。

```go

func LookupPublicKey() (*rsa.PublicKey, error) {
    prev, err := filepath.Abs(".")
    if err != nil {
        defer fmt.Println("Error")
    }
    key, _ := ioutil.ReadFile(prev + "/demo.rsa.pub")
    parsedKey, err := jwt.ParseRSAPublicKeyFromPEM(key)
    return parsedKey, err
}
```

下記の手順で公開鍵の認証情報を取得しています。

- パスの取得
- 鍵の読み込み
- jwwt認証用に情報を取得

## MySQLへの接続及びデータをモデルに保存する部分

機械学習のプログラムはPythonまたはC++などを使って実装している場合が多いので直接的に使用する場合はシステムコールを使って使用する必要がありますが、それはセキュリティ上問題があり、あまり好まれる使い方ではありません。
この記事では機械学習で予測した結果が何らかのDBに保存されている場合を想定しています。

下記の順で説明していきます。

- DBの設定と接続
- Contraller からDBを叩く部分

### DBの設定と接続

`gorp.go`でDBの初期設定と処理をしています。

```go
func InitDB() {
       	db.Init()
       	// conf ファイルのに記載されているDB名を読み込み処理を行なう
       	// `revel revel_jwt_mysql run`と`revel revel_jwt_mysql run prod`で読み込むDBが異なる`prod`が本番用
       	uri := read_conf("db.uri")
```

DBの初期設定と設定が記述されている`app.conf`から値を読み込んでuriに設定しています。

```go
       	// mysqlへの接続処理
       	db_access_uri := "ユーザー名:パスワード@tcp(" + uri +")/データベース名"
       	db, err := sql.Open("mysql", db_access_uri)
       	if err != nil {
       		panic(err)
       	}
       	Dbm = &gorp.DbMap{Db: db, Dialect: gorp.MySQLDialect{"InnoDB", "UTF8"}}

       	setColumnSizes := func(t *gorp.TableMap, colSizes map[string]int) {
       		for col, size := range colSizes {
       			t.ColMap(col).MaxSize = size
       		}
       	}
```

`db_access_uri`で予め起動しておいたMySQLのユーザー名、パスワード、DB名を設定します。
DBからのORMにはgorpを使用しています。
gorpは使用したいSQLを変更したい場合は下記のSQLドライバーから選択し、下記の部分をそのドライバー用に書き換えれば柔軟にDBを変えることができます。

```go
	_ "github.com/go-sql-driver/mysql"

	Dbm = &gorp.DbMap{Db: db, Dialect: gorp.MySQLDialect{"InnoDB", "UTF8"}}
```

[SQLドライバー一覧](https://github.com/golang/go/wiki/SQLDrivers)


```go
       	// Tableに設定する値の長さを設定している
       	t := Dbm.AddTable(models.Revel_JWT_MySQL{}).SetKeys(true, "Revel_JWT_MySQL_Id")
       	setColumnSizes(t, map[string]int{
       		"Date": 10,
       		"Title": 40,
       		"Picture": 140,
       		"Picture_Link": 140,
       		"Caption": 140,
       	})
```

主キーを設定している場合にその値が必ず設定されていることが確認しているのが`SetKeys(true, "Revel_JWT_MySQL_Id")`です。
`setColumnSizes`でサイズがこれ以上の値になることを禁止しています。

```go
       	Dbm.TraceOn("[gorp]", r.INFO)
       	Dbm.CreateTables()
```

DBのテーブルを作成しています。

```go

       	prev, err := filepath.Abs(".")
       	if err != nil {
       		defer fmt.Println("Error")
       	}

       	// 最初に登録するcsv形式のデータ
       	file, err := os.Open(prev + "/csv_diary_data/revel_jwt_mysql.csv")
       	failOnError(err)
       	defer file.Close()

```

csvから読み込んだ値をDBに初期の値として保存するためcsvの情報を取得します。

```go

       	f_count := 0
       	reader := csv.NewReader(file)
       	var revel_jwt_mysqls []*models.Revel_JWT_MySQL

       	// DBへの登録
       	for {
       		recoad, err := reader.Read()
       		if err == io.EOF {
       			break
       		} else {
       			failOnError(err)
       		}
       		img_data := img_path + recoad[3]
       		revel_jwt_mysqls = append(revel_jwt_mysqls, &models.Revel_JWT_MySQL{f_count, t_value, recoad[0], img_data,
       			                                                          recoad[1], recoad[2]})
       		f_count++
    }
```

`reader := csv.NewReader(file)`でcsv読込用のreaderを作成します。
`recoad, err := reader.Read()`でcsvからデータを読み込みます。

下記のような形式のcsvの場合

```
テスト1,テスト2,テスト3
```

下記のように値が保持されます。

recoad[0]="テスト1"
recoad[1]="テスト2"
recoad[2]="テスト3"

`revel_jwt_mysqls=append(revel_jwt_mysqls,models.Revel_JWT_MySQL{f_count,t_value,recoad[0],img_data,recoad[2],recoad[3]})`で読み込んだ値を配列に保持しています。

```
       	for _, revel_jwt_mysql := range revel_jwt_mysqls {
       		if err := Dbm.Insert(revel_jwt_mysql); err != nil {
       			panic(err)
       		}
       	}
```

読み込んだ値を`Dbm.Insert`でDBに挿入していきます。

## MySQLの操作して結果を表示

MySQLを操作してデータを取得、登録、更新、削除する部分の説明をします。SQLのべた書きなのでもっとイケてる方法があると思います。

JWT認証後に各処理に振り分けています。

```go
            switch choose_parameter{
                case "GET":
                    for _, k := range keys {
                        if _, ok := c.Params.Query[k]; ok {
                            revel.TRACE.Printf("Param OK")
                            no_key = false
                            return c.returnParameter(k, res.Revel_JWT_MySQL_Id)
                        }
                    }
                    if no_key {
                        revel.ERROR.Printf("No settings %#v", val)
                        return c.Forbidden("No setting.")
                    }
                case "GET_ALL":
                    length, err := strconv.Atoi(c.Params.Query["Length"][0])
                    if err != nil {
                        revel.ERROR.Printf("No settings %#v", length)
                        return c.Forbidden("No setting length.")
                    }else{
                        return c.GetRobohonAllMethod(length)
                    }
                case "GET_JWT":
                    limit, err := strconv.Atoi(c.Params.Query["Limit"][0])
                    if err != nil {
                        revel.ERROR.Printf("No settings %#v", limit)
                        return c.Forbidden("No setting length.")
                    }else{
                        return c.GetJWTMethod(limit)
                    }
                case "POST":
                    return c.RegistRobohonMethod(res)
                case "DELETE":
                    return c.DeleteRobohonMethod(res.Revel_JWT_MySQL_Id)
                case "UPDATE" :
                    for _, k := range keys {
                        if _, ok := c.Params.Query[k]; ok {
                            revel.TRACE.Printf("Param OK")
                            no_key = false
                            return c.UpdateRobohonMethod(k, c.Params.Query[k][0], res.Revel_JWT_MySQL_Id, res.Date)
                        }
                    }
                    if no_key {
                        revel.ERROR.Printf("No settings %#v", val)
                        return c.Forbidden("No setting.")
}

```

### データの取得

```go
func (c Revel_JWT_MySQL) returnParameter (key string, revel_jwt_mysql_id int) revel.Result {
```

データの取得処理をしている部分です。パラメータを`key`で取得し、MySQLで取得するデータのidを`revel_jwt_mysql_id`で取得しています。結果を`revel.Result`で返しています。

```go

    var result *models.Revel_JWT_MySQL
    revel_jwt_mysqls, err := c.Txn.Select(models.Revel_JWT_MySQL{},
        `select * from Revel_JWT_MySQL where Revel_JWT_MySQL_Id = ?`,
        revel_jwt_mysql_id)
    if err != nil {
        panic(err)
        return c.Forbidden("SQL Select Error")
    }
    if len(revel_jwt_mysql) == 0 {
        return c.NotFound("Could not find")
    }
    result = revel_jwt_mysqls[0].(*models.Revel_JWT_MySQL)
```

MySQL内を`select`で検索して結果を返しています。返却された値をModelで扱えるように変換して渡しています。

```go

    switch key {
    case "Title":
        return c.RenderJson(result.Title)
    case "Picture":
        return c.RenderJson(result.Picture)
    case "Picture_Link":
        return c.RenderJson(result.Picture_Link)
    case "All":
        return c.RenderJson(result)
    default:
        return c.Forbidden("Failed no setting Parameter.")
    }
}
```

取得された項目からどの値を取得するか選択しています。
選択された結果をJson形式で返しています。

### データの登録

エンドポイントから取得したデータを登録する処理になります。

```go

    res.Validate(c.Validation)

    // Handle errors
    if c.Validation.HasErrors() {
        c.Validation.Keep()
        c.FlashParams()
        return c.Forbidden("URL parameter Validate error")
    }

    t_value := time.Now().Format("2006-01-02")

    // Revel_JWT_MySQL_Idは主キーに指定しているため、自動で設定されるがモデルの構造上設定が必要
    robohon := models.Revel_JWT_MySQL{res.Revel_JWT_MySQL_Id, t_value, res.Title, res.Picture,
                                                        res.Picture_Link}
    // DB にデータを登録
    if err := c.Txn.Insert(&robohon); err != nil {
        panic(err)
        return c.Forbidden("SQL Regist Error")
    }

    r := Response{robohon}
    return c.RenderJson(r)
```

下記の処理ををしています。

- 取得したデータのチェック
- モデルに取得したパラメータを設定
- モデルをDBに登録

登録された処理をJson形式で結果として返しています。

### データの更新

データの更新処理です。MySQLに登録されているデータを更新します。

```go
    switch key {
    case "Title":
        _, err := c.Txn.Exec("update Revel_JWT_MySQL set Title = ? where Revel_JWT_MySQL_Id = ? ",
            value, robohonId)
        if err != nil {
            panic(err)
        }
    case "Picture":
        _, err := c.Txn.Exec("update Revel_JWT_MySQL set Picture = ? where Revel_JWT_MySQL_Id = ?",
            value, robohonId)
        if err != nil {
            panic(err)
        }
    case "Picture_Link":
        _, err := c.Txn.Exec("update Revel_JWT_MySQL set Picture_Link = ? where Revel_JWT_MySQL_Id = ?",
            value, robohonId)
        if err != nil {
            panic(err)
        }
    default:
        return c.Forbidden("Failed no setting Parameter.")
    }

    show_char := "UpdateRobohon " + value
    r := Response{show_char}
　　return c.RenderJson(r)
```

主キーであるIdで更新したい情報を選びその情報を更新しています。
更新された結果をJson形式で返しています。

### データの削除

データの削除処理です。

```go
    _, err := c.Txn.Delete(&models.Revel_JWT_MySQL{Revel_JWT_MySQL_Id: robohonId})
    if err != nil {
        panic(err)
        return c.Forbidden("SQL Deelete Error")
    }

    show_char := "Delete " + string(robohonId)
    r := Response{show_char}
    return c.RenderJson(r)
```

主キーのIDを使用して削除処理を行ないます。
削除された結果をJson形式で返しています。

## 実際に動作させる

動作環境は下記の図のようなイメージです。

Vagrantを立ち上げる前にVagrantfileにipアドレスを設定しておきます。このipアドレスは後で使用するのでメモしておきます。

Vagrantを立ち上げます。

```
vagrant up
```

そのVagrantにansibleで動作環境を設定します。
`hosts`に先ほど設定したVagrantのipアドレスを設定します。

```
ansible-playbook -i hosts site.yml
```

これでDocker環境が設定されたVagrantが完成します。

vagrantに入って今回のRevelの環境が設定されているコンテナをビルドして作成します。

```shell-session

cd /home/vagrant/go_lang/docker
make test-api
```

ビルド後に自動でdockerの環境に入りますが`exit`で一旦出ます。

```shell-session
exit
```

Docker composeでMySQL用のコンテナを立ち上げてと先ほどビルドしたイメージに入ります。

```shell-session
vagrant ssh
sh docker_compose.sh
```

テストが全て通るか試します。

```
revel test revel_jwt_mysql
```

下記で実行します。

```
revel run revel_jwt_mysql
```

そうすると下記のアドレスでアクセスできて動作を確認することができます。

```
http://{vagrantで設定したアドレス（デフォルトは"192.168.33.41"）}/revel_jwt_mysql?Id=1&Date=2016-04-02&Title&JWTtoken=eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.nK6gM51kP4klRVQoJ0w6dQLQcbQ2Jrf0TPYp9hZS_8mNZWQaHD9zq5-mIFam1xwJt_dorgBhZrfQzu7tKKbeyHmfj6TcWsSJ7T2W6mG0uFSHyAMHJRpjhFrwGB6K5dzUOvcYgw1B1L-AD6-37zWt6tXP_9Y8HVy4xL-oCR_979Y
```

結果

![Screen Shot 2016-11-28 at 9.18.02 AM.png](https://qiita-image-store.s3.amazonaws.com/0/10496/371134ec-202c-cea9-1eb9-9ae208c06cbe.png)


もし上手くいかなければ下記のdocker hubから下記のコマンドでイメージを取得してください。

```shell-session
docker pull masayaresearch/go_api
```

# まとめ

この記事を読んで機械学習のモデルを作れてサービスインできる形で提供できるエンジニアが増えると幸いです。

（機械学習について一切触れていませんが・・・）