1. hiroykam
Changes in body
Source | HTML | Preview
@@ -1,437 +1,448 @@
#`ECHO`+`GORM`で`JWT`と`GraphQL`の環境を構築する
`Go`で簡単な`JWT`と`GraphQL`環境の環境を作成したところ、思った以上に簡単に作成することができなかったので、備忘録として記載した(更新随時更新予定)。
なお、`golang`は、[ここ](https://golang.org/dl/)から`go1.9.2`をダウンロードし、利用した。
##`JWT`の環境を構築する
###`GORM`を使わないパターン
まずはDBなしで、`GORM`を使わないパターンで環境を作成した。
`echo`と`jwt-go`を下記のように`go get`する。
```shell-session
$ go get github.com/labstack/echo
$ go get github.com/dgrijalva/jwt-go
```
なお、ここではサンプル作成にあたり、こちらの[`GitHub`](https://github.com/labstack/echox/tree/master/cookbook/jwt)を参考にした。
###サンプル
-####ルーティング
+####ルーティングの定義
`go run`を実行する対象となるファイル。
```go:main.go
package main
import (
"github.com/labstack/echo"
"github.com/labstack/echo/middleware"
"./handler"
)
func main() {
e := echo.New()
e.Use(middleware.Logger())
e.Use(middleware.Recover())
e.GET("/hello", handler.Hello())
e.POST("/login", handler.Login())
r := e.Group("/restricted")
r.Use(middleware.JWT([]byte("secret")))
r.POST("", handler.Restricted())
e.Start(":3000")
}
```
-####コントローラ
+####コントローラの定義
+コントローラの位置づけとなる`handler.go`を定義する。
+
```go:handler/handler.go
package handler
import (
"net/http"
"time"
"github.com/labstack/echo"
"github.com/dgrijalva/jwt-go"
)
func Hello() echo.HandlerFunc {
return func(c echo.Context) error {
return c.String(http.StatusOK, "Hello World")
}
}
func Login() echo.HandlerFunc {
return func(c echo.Context) error {
username := c.FormValue("username")
password := c.FormValue("password")
if username == "test" && password == "test" {
// Create token
token := jwt.New(jwt.SigningMethodHS256)
// Set claims
claims := token.Claims.(jwt.MapClaims)
claims["name"] = "test"
claims["admin"] = true
claims["exp"] = time.Now().Add(time.Hour * 72).Unix()
// Generate encoded token and send it as response.
t, err := token.SignedString([]byte("secret"))
if err != nil {
return err
}
return c.JSON(http.StatusOK, map[string]string{
"token": t,
})
}
return echo.ErrUnauthorized
}
}
func Restricted() echo.HandlerFunc {
return func(c echo.Context) error {
user := c.Get("user").(*jwt.Token)
claims := user.Claims.(jwt.MapClaims)
name := claims["name"].(string)
return c.String(http.StatusOK, "Welcome "+name+"!")
}
}
```
####実行結果
まずは、ログインをし、トークンを取得する。
```shell-session
$ curl -X POST -d 'username=test' -d 'password=test' localhost:3000/login
{"token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhZG1pbiI6dHJ1ZSwiZXhwIjoxNTEwNzUzNTU1LCJuYW1lIjoiSm9uIFNub3cifQ.LPRv0prfLL1Xpy0Us06E97qPb0Nca6UoDcHYVlSVWwc"}
```
トークンを利用して、認証の検証をする。
```shell-session
$ curl -X POST localhost:3000/restricted -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhZG1pbiI6dHJ1ZSwiZXhwIjoxNTEwNzUzNTU1LCJuYW1lIjoiSm9uIFNub3cifQ.LPRv0prfLL1Xpy0Us06E97qPb0Nca6UoDcHYVlSVWwc"
Welcome test!
```
###`GORM`を使うパターン
ローカルインストールしている`MySQL`を利用した。
`gorm`と`MySQL`のドライバを`go get`する。
```shell-session
$ go get github.com/jinzhu/gorm
$ go get github.com/jinzhu/gorm/dialects/mysql
```
####`DDL/DML`の実行
下記のように準備する。
パスワードは平文のままのため、`MD5`等で暗号化する予定。
```sql
CREATE DATABASE `go_test`;
CREATE TABLE IF NOT EXISTS `go_test`.`user` (
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
`name` VARCHAR(60) NOT NULL,
`password` VARCHAR(60) NOT NULL,
`hobby` VARCHAR(60) NOT NULL,
PRIMARY KEY (`id`))
ENGINE = InnoDB;
INSERT INTO `go_test`.`user` (`name`, `password`, `hobby`) VALUES ("test", "test", "games");
```
####`MySQL`との接続
`_ "github.com/go-sql-driver/mysql"`を`import`で指定しないと、`sql: unknown driver \"mysql\" (forgotten import?)`というランタイムエラーが発生する。
```go:db/connect.go
package db
import (
_ "github.com/go-sql-driver/mysql"
"github.com/jinzhu/gorm"
)
func ConnectGORM() *gorm.DB {
DBMS := "mysql"
USER := "root"
PASS := ""
PROTOCOL := "tcp(0.0.0.0:3306)"
DBNAME := "go_test"
CONNECT := USER+":"+PASS+"@"+PROTOCOL+"/"+DBNAME
db,err := gorm.Open(DBMS, CONNECT)
if err != nil {
panic(err.Error())
}
return db
}
```
-####モデル
-`User`モデルを用意する
+####モデルの定義
+`User`モデルを定義する
```go:models/user.go
package models
type User struct {
Id int64
Name string `sql:"size:60"`
Password string `sql:"size:60"`
Hobby string `sql:"size:60"`
}
```
-####コントローラの修正
-`handler.go`を以下のように修正する。テーブル名を`User`と複数形にしていないので、`db.SingularTable(true)`を実行する必要がある。
+####コントローラの変更
+`handler.go`を以下のように変更する。テーブル名を`User`と複数形にしていないので、`db.SingularTable(true)`を実行する必要がある。
```go:handler/handler.go
package handler
import (
"net/http"
"github.com/labstack/echo"
"github.com/dgrijalva/jwt-go"
"../db"
"../models"
"time"
)
func Hello() echo.HandlerFunc {
return func(c echo.Context) error {
return c.String(http.StatusOK, "Hello World")
}
}
func Login() echo.HandlerFunc {
return func(c echo.Context) error {
username := c.FormValue("username")
password := c.FormValue("password")
db := db.ConnectGORM()
db.SingularTable(true)
user := [] models.User{}
db.Find(&user, "name=? and password=?", username, password)
if len(user) > 0 && username == user[0].Name {
// Create token
token := jwt.New(jwt.SigningMethodHS256)
// Set claims
claims := token.Claims.(jwt.MapClaims)
claims["name"] = username
claims["admin"] = true
claims["exp"] = time.Now().Add(time.Hour * 72).Unix()
// Generate encoded token and send it as response.
t, err := token.SignedString([]byte("secret"))
if err != nil {
return err
}
return c.JSON(http.StatusOK, map[string]string{
"token": t,
})
}
return echo.ErrUnauthorized
}
}
func Restricted() echo.HandlerFunc {
return func(c echo.Context) error {
user := c.Get("user").(*jwt.Token)
claims := user.Claims.(jwt.MapClaims)
name := claims["name"].(string)
return c.String(http.StatusOK, "Welcome "+name+"!")
}
}
```
####実行結果
「`GORM`を使わないパターン」と同一のため、省略。
-##`GraphQL`の環境を構築する
+##`GraphQL`の環境を構築
`graphql`を`go get`する。
```shell-session
$ go get github.com/graphql-go/graphql
```
サンプルは、こちらの[GitHub](https://github.com/syossan27/sample-graphql)を参考にした。
-###スキーマ
+###モデルの変更
+`user.go`を下記のように変更する
+
+```go:models/user.go
+package models
+
+type User struct {
+ Id int64 `db:"id" json:"id"`
+ Name string `sql:"size:60" db:"name" json:"name"`
+ Password string `sql:"size:60" db:"password" json:"password"`
+ Hobby string `sql:"size:60" db:"hobby" json:"hobby"`
+}
+```
+
+###スキーマの追加
スキーマを定義する。
```go:graphql/schema.go
package graphql
import (
"github.com/graphql-go/graphql"
"fmt"
"../db"
+ "../models"
"log"
)
-type user struct {
- Id string `db:"id" json:"id"`
- Name string `db:"name" json:"name"`
- Hobby string `db:"Hobby" json:"hobby"`
-}
-
var userType = graphql.NewObject(
graphql.ObjectConfig {
Name: "User",
Fields: graphql.Fields {
"id": &graphql.Field {
Type: graphql.String,
},
"name": &graphql.Field{
Type: graphql.String,
},
"hobby": &graphql.Field{
Type: graphql.String,
},
},
},
)
var queryType = graphql.NewObject(
graphql.ObjectConfig {
Name: "Query",
Fields: graphql.Fields {
"User": &graphql.Field {
Type: userType,
Args: graphql.FieldConfigArgument {
"id": &graphql.ArgumentConfig {
Type: graphql.String,
},
},
Resolve: func(p graphql.ResolveParams) (interface{}, error) {
- idQuery, isOK := p.Args["id"].(string)
+ idQuery, isOK := p.Args["id"].(int64)
if isOK {
db := db.ConnectGORM()
db.SingularTable(true)
- user := user{}
+ user := models.User{}
user.Id = idQuery
db.First(&user)
- log.Printf(idQuery)
+ log.Print(idQuery)
return &user, nil
}
return nil, nil
},
},
},
},
)
func ExecuteQuery(query string) *graphql.Result {
- var schema, _ = graphql.NewSchema(
- graphql.SchemaConfig {
- Query: queryType,
- },
- )
+ var schema, _ = graphql.NewSchema(
+ graphql.SchemaConfig {
+ Query: queryType,
+ },
+ )
result := graphql.Do(graphql.Params {
Schema: schema,
RequestString: query,
})
if len(result.Errors) > 0 {
fmt.Printf("wrong result, unexpected errors: %v", result.Errors)
}
return result
}
```
-###コントローラの修正
-`Restricted`を以下のように修正する。
+###コントローラの変更
+`Restricted`を以下のように変更する。
```go:handler/handler.go
package handler
import (
"net/http"
"github.com/labstack/echo"
"github.com/dgrijalva/jwt-go"
"../db"
"../models"
"../graphql"
"time"
"bytes"
"log"
)
func Hello() echo.HandlerFunc {
return func(c echo.Context) error {
return c.String(http.StatusOK, "Hello World")
}
}
func Login() echo.HandlerFunc {
return func(c echo.Context) error {
username := c.FormValue("username")
password := c.FormValue("password")
db := db.ConnectGORM()
db.SingularTable(true)
user := [] models.User{}
db.Find(&user, "name=? and password=?", username, password)
if len(user) > 0 && username == user[0].Name {
// Create token
token := jwt.New(jwt.SigningMethodHS256)
// Set claims
claims := token.Claims.(jwt.MapClaims)
claims["name"] = username
claims["admin"] = true
claims["exp"] = time.Now().Add(time.Hour * 72).Unix()
// Generate encoded token and send it as response.
t, err := token.SignedString([]byte("secret"))
if err != nil {
return err
}
return c.JSON(http.StatusOK, map[string]string{
"token": t,
})
}
return echo.ErrUnauthorized
}
}
func Restricted() echo.HandlerFunc {
return func(c echo.Context) error {
user := c.Get("user").(*jwt.Token)
_ = user.Claims.(jwt.MapClaims)
bufBody := new(bytes.Buffer)
bufBody.ReadFrom(c.Request().Body)
query := bufBody.String()
log.Printf(query)
- result := graphql.ExecuteQuery(query, graphql.Schema)
+ result := graphql.ExecuteQuery(query)
return c.JSON(http.StatusOK, result)
}
}
```
###実行結果
ヘッダにトークンを指定して、クエリを実行する。
```shell-session
$ curl -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhZG1pbiI6dHJ1ZSwiZXhwIjoxNTEwNzk5MDU0LCJuYW1lIjoidGVzdCJ9.0snV1Goej4BtEv1Q8-M3N22aBtwB2BsdxNRvr3uhUFQ" -X POST -d '
{
query: User(id: "1") { id, name, hobby }
}
-' http://localhost:3001/restricted
+' http://localhost:3000/restricted
```
クエリの実行結果はこちら。
```shell-session
{"data":{"query":{"hobby":"games","id":"1","name":"test"}}}
```