#ECHO
+GORM
でJWT
とGraphQL
の環境を構築する
Go
で簡単なJWT
とGraphQL
環境の環境を作成したところ、思った以上に簡単に作成することができなかったので、備忘録として記載した(更新随時更新予定)。
なお、golang
は、ここからgo1.9.2
をダウンロードし、利用した。
##JWT
の環境を構築する
dgrijalva/jwt-goを利用するのが一番簡単な実装に思えたのでまずはこちらを利用した。
###GORM
を使わないパターン
まずはDBなしで、GORM
を使わないパターンで環境を作成した。
echo
とjwt-go
を下記のようにgo get
する。
$ go get github.com/labstack/echo
$ go get github.com/dgrijalva/jwt-go
なお、ここではサンプル作成にあたり、こちらのGitHub
を参考にした。
###サンプル
####ルーティングの定義
go run
を実行する対象となるファイル。
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
を定義する。
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+"!")
}
}
####実行結果
まずは、ログインをし、トークンを取得する。
$ curl -X POST -d 'username=test' -d 'password=test' localhost:3000/login
{"token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhZG1pbiI6dHJ1ZSwiZXhwIjoxNTEwNzUzNTU1LCJuYW1lIjoiSm9uIFNub3cifQ.LPRv0prfLL1Xpy0Us06E97qPb0Nca6UoDcHYVlSVWwc"}
トークンを利用して、認証の検証をする。
$ curl -X POST localhost:3000/restricted -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhZG1pbiI6dHJ1ZSwiZXhwIjoxNTEwNzUzNTU1LCJuYW1lIjoiSm9uIFNub3cifQ.LPRv0prfLL1Xpy0Us06E97qPb0Nca6UoDcHYVlSVWwc"
Welcome test!
###GORM
を使うパターン
ローカルインストールしているMySQL
を利用した。
gorm
とMySQL
のドライバをgo get
する。
$ go get github.com/jinzhu/gorm
$ go get github.com/jinzhu/gorm/dialects/mysql
####DDL/DML
の実行
下記のように準備する。
ユーザ名はUNIQUE
にし、パスワードは平文のままのため、MD5
等で暗号化する予定。
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?)
というランタイムエラーが発生する。
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
モデルを定義する
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)
を実行する必要がある。
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
する。
$ go get github.com/graphql-go/graphql
サンプルは、こちらのGitHubを参考にした。
###モデルの変更
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"`
}
###スキーマの追加
スキーマを定義する。
package graphql
import (
"github.com/graphql-go/graphql"
"fmt"
"../db"
"../models"
"log"
"strconv"
)
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, err :=strconv.ParseInt(p.Args["id"].(string), 10, 64)
if err == nil {
db := db.ConnectGORM()
db.SingularTable(true)
user := models.User{}
user.Id = idQuery
db.First(&user)
log.Print(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
を以下のように変更する。
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)
return c.JSON(http.StatusOK, result)
}
}
###実行結果
ヘッダにトークンを指定して、クエリを実行する。
$ curl -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhZG1pbiI6dHJ1ZSwiZXhwIjoxNTEwNzk5MDU0LCJuYW1lIjoidGVzdCJ9.0snV1Goej4BtEv1Q8-M3N22aBtwB2BsdxNRvr3uhUFQ" -X POST -d '
{
query: User(id: "1") { id, name, hobby }
}
' http://localhost:3000/restricted
クエリの実行結果はこちら。
{"data":{"query":{"hobby":"games","id":"1","name":"test"}}}
##refresh token
を扱った実装
こちらでdgrijalva/jwt-goを利用し、リフレッシュトークンを生成するサンプルを作成されていたので、それを参考に実装する。
###refresh token
の管理を追加
下記を追加する。
package libs
import (
"../db"
"../models"
"crypto/rand"
"encoding/base64"
)
var refreshTokens map[string]string
func GenerateRandomBytes(n int) ([]byte, error) {
b := make([]byte, n)
_, err := rand.Read(b)
if err != nil {
return nil, err
}
return b, nil
}
func GenerateRandomString(s int) (string, error) {
b, err := GenerateRandomBytes(s)
return base64.URLEncoding.EncodeToString(b), err
}
func InitDB() {
refreshTokens = make(map[string]string)
}
func FetchUser(username string, password string) models.User {
con := db.ConnectGORM()
con.SingularTable(true)
user := models.User{}
con.First(&user, "name=? and password=?", username, password)
return user
}
func StoreRefreshToken() (jti string, err error) {
jti, err = GenerateRandomString(32)
if err != nil {
return jti, err
}
for refreshTokens[jti] != "" {
jti, err = GenerateRandomString(32)
if err != nil {
return jti, err
}
}
refreshTokens[jti] = "valid"
return jti, err
}
func DeleteRefreshToken(jti string) {
delete(refreshTokens, jti)
}
func CheckRefreshToken(jti string) bool {
return refreshTokens[jti] != ""
}
###refresh token
とtoken
の作成機能を用意
package middleware
import (
"time"
"../libs"
jwt "github.com/dgrijalva/jwt-go"
"github.com/labstack/echo"
"fmt"
)
type MyClaim struct {
UserId int64
IsAdmin bool
RefreshJti string
jwt.StandardClaims
}
func createRefreshTokenString(userid int64) (refreshTokenString string, err error) {
refreshJti, err := libs.StoreRefreshToken()
if err != nil {
return "", err
}
if userid != 0 {
// Create token
token := jwt.NewWithClaims(jwt.SigningMethodHS256, &MyClaim{
UserId: userid,
IsAdmin: false,
RefreshJti: refreshJti,
StandardClaims: jwt.StandardClaims{
ExpiresAt: time.Now().Add(time.Hour * 24 * 7).Unix(),
}})
// Generate encoded token and send it as response.
t, err := token.SignedString([]byte("secret2"))
if err != nil {
return "", err
}
return t, err
}
return "", echo.ErrUnauthorized
}
func createAuthTokenString(userid int64) (authTokenString string, err error) {
// Create token
token := jwt.NewWithClaims(jwt.SigningMethodHS256, &MyClaim{
UserId: userid,
IsAdmin: true,
StandardClaims: jwt.StandardClaims{
ExpiresAt: time.Now().Add(time.Hour * 24).Unix(),
}})
// Generate encoded token and send it as response.
t, err := token.SignedString([]byte("secret1"))
if err != nil {
return "", err
}
return t, err
}
func CreateNewTokens(username string, password string) (authTokenString string, refreshTokenString string, err error) {
user := libs.FetchUser(username, password)
refreshTokenString, err = createRefreshTokenString(user.Id)
if err != nil {
return "", "", err
}
authTokenString, err = createAuthTokenString(user.Id)
if err != nil {
return "", "", err
}
return
}
func UpdateRefreshTokenExp(myClaim *MyClaim, oldTokenString string) (newTokenString, newRefreshTokenString string, err error) {
myClaim2 := MyClaim{}
_, err = jwt.ParseWithClaims(oldTokenString, &myClaim2, func(token *jwt.Token) (interface{}, error) {
return []byte("secret1"), nil
})
if err != nil {
return "", "", err
}
if !libs.CheckRefreshToken(myClaim.RefreshJti) || myClaim.UserId != myClaim2.UserId {
return "", "", fmt.Errorf("error: %s", "old token is invalid")
}
libs.DeleteRefreshToken(myClaim2.RefreshJti)
newRefreshTokenString, err = createRefreshTokenString(myClaim2.UserId)
if err != nil {
return "", "", err
}
newTokenString, err = createAuthTokenString(myClaim2.UserId)
if err != nil {
return "", "", err
}
return
}
###コントローラを変更
下記のように変更し、refresh token
とtoken
を認証時に受取り、再認証ができるようにする。
package handler
import (
"net/http"
"github.com/labstack/echo"
"github.com/dgrijalva/jwt-go"
"../graphql"
"../middleware"
"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")
tokenString, refreshTokenString, err := middleware.CreateNewTokens(username, password)
if err == nil {
return c.JSON(http.StatusOK, map[string]string{
"token": tokenString,
"refreshToken": refreshTokenString,
})
}
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)
return c.JSON(http.StatusOK, result)
}
}
func ReAuth() echo.HandlerFunc {
return func(c echo.Context) error {
user := c.Get("user").(*jwt.Token)
claims := user.Claims.(*middleware.MyClaim)
oldToken := c.FormValue("old_token")
tokenString, refreshTokenString, err := middleware.UpdateRefreshTokenExp(claims, oldToken)
if err == nil {
return c.JSON(http.StatusOK, map[string]string{
"token": tokenString,
"refreshToken": refreshTokenString,
})
}
return echo.ErrUnauthorized
}
}
main
も変更する。
package main
import (
"github.com/labstack/echo"
"github.com/labstack/echo/middleware"
"./handler"
"./libs"
ext "./middleware"
)
func main() {
e := echo.New()
e.Use(middleware.Logger())
e.Use(middleware.Recover())
libs.InitDB()
e.GET("/hello", handler.Hello())
e.POST("/login", handler.Login())
r1 := e.Group("/restricted")
r1.Use(middleware.JWT([]byte("secret1")))
r1.POST("", handler.Restricted())
r2 := e.Group("/reauth")
config := middleware.JWTConfig{
Claims: &ext.MyClaim{},
SigningKey: []byte("secret2"),
}
r2.Use(middleware.JWTWithConfig(config))
r2.POST("", handler.ReAuth())
e.Start(":3000")
}
###実行結果
まずはrefresh token
とtoken
を取得する。
$ curl -X POST -d 'username=test' -d 'password=test' localhost:3000/login
{"refreshToken":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJVc2VySWQiOjEsIklzQWRtaW4iOmZhbHNlLCJSZWZyZXNoSnRpIjoiX0RYZC12SFZtNTZ4WE9VMHFfbXUxN053eEVFMDAyZmdqRGRFcmFfeFB2az0iLCJleHAiOjE1MjQ0MDc1MzR9.QVc8kiHn1VIv9A-zR_bXFwcDjcECc_7j1s683kizs8M","token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJVc2VySWQiOjEsIklzQWRtaW4iOnRydWUsIlJlZnJlc2hKdGkiOiIiLCJleHAiOjE1MjM4ODkxMzR9.E_BiLRjCLVv-MUoYGGtqS9oMEzR612bn4ucAA6PFscU"}
再認証する。
ヘッダにはrefresh token
を付与し、古いtoken
を念のためPOST
している。
$ curl -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJVc2VySWQiOjEsIklzQWRtaW4iOmZhbHNlLCJSZWZyZXNoSnRpIjoiX0RYZC12SFZtNTZ4WE9VMHFfbXUxN053eEVFMDAyZmdqRGRFcmFfeFB2az0iLCJleHAiOjE1MjQ0MDc1MzR9.QVc8kiHn1VIv9A-zR_bXFwcDjcECc_7j1s683kizs8M" -X POST -d 'old_token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJVc2VySWQiOjEsIklzQWRtaW4iOnRydWUsIlJlZnJlc2hKdGkiOiIiLCJleHAiOjE1MjM4ODkxMzR9.E_BiLRjCLVv-MUoYGGtqS9oMEzR612bn4ucAA6PFscU' localhost:3000/reauth
{"refreshToken":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJVc2VySWQiOjEsIklzQWRtaW4iOmZhbHNlLCJSZWZyZXNoSnRpIjoicWg0UG5pUEpwUG9Ld19mQ3JVZUttX1Q3c1dUd1VSbTlNOWJQNllHblppVT0iLCJleHAiOjE1MjQ0MDc1ODd9.AZyIEGVMeZ8Rr5gEw21FFM39qeRFg6qF6kvfR9VjU9I","token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJVc2VySWQiOjEsIklzQWRtaW4iOnRydWUsIlJlZnJlc2hKdGkiOiIiLCJleHAiOjE1MjM4ODkxODd9.AT7vIchiphnVuMGb9-TW8xtGxvwpDKN3hm8N2n1eX4I"}
再取得したtoken
でクエリを実行する。
$ curl -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJVc2VySWQiOjEsIklzQWRtaW4iOnRydWUsIlJlZnJlc2hKdGkiOiIiLCJleHAiOjE1MjM4ODkxODd9.AT7vIchiphnVuMGb9-TW8xtGxvwpDKN3hm8N2n1eX4I" -X POST -d '
{
query: User(id: "1") { id, name, hobby }
}
' http://localhost:3000/restricted
{"data":{"query":{"hobby":"games","id":"1","name":"test"}}}