1. hiroykam

    Posted

    hiroykam
Changes in title
+ECHO+GORMでJWTとGraphQLの環境を構築
Changes in tags
Changes in body
Source | HTML | Preview
@@ -0,0 +1,437 @@
+#`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")
+}
+```
+
+####コントローラ
+```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`モデルを用意する
+
+```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)`を実行する必要がある。
+
+```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`を`go get`する。
+
+```shell-session
+$ go get github.com/graphql-go/graphql
+```
+
+サンプルは、こちらの[GitHub](https://github.com/syossan27/sample-graphql)を参考にした。
+
+###スキーマ
+スキーマを定義する。
+
+```go:graphql/schema.go
+package graphql
+
+import (
+ "github.com/graphql-go/graphql"
+ "fmt"
+ "../db"
+ "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)
+ if isOK {
+ db := db.ConnectGORM()
+ db.SingularTable(true)
+ user := user{}
+ user.Id = idQuery
+ db.First(&user)
+ log.Printf(idQuery)
+ return &user, nil
+ }
+
+ return nil, nil
+ },
+ },
+ },
+ },
+)
+
+func ExecuteQuery(query string) *graphql.Result {
+ 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`を以下のように修正する。
+
+```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)
+ 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
+```
+
+クエリの実行結果はこちら。
+
+```shell-session
+{"data":{"query":{"hobby":"games","id":"1","name":"test"}}}
+```
+