使用Golang(ECHO + GORM)搭建JWT和GraphQL环境

使用ECHO和GORM来构建JWT和GraphQL环境。

当我尝试创建一个简单的JWT和GraphQL环境时,我发现其实并没有想象中那么容易,所以我决定将此过程记录下来作为备忘录(将随时更新)。
顺便提一下,我下载并使用了golang1.9.2版本。

搭建JWT环境

由于使用dgrijalva/jwt-go看起来是最简单的实现,所以首先选择了它进行利用。

不使用GORM的模式。

首先,我使用不带数据库的方式,通过不使用GORM来创建环境。
请按照以下方式使用go get获取echo和jwt-go库。

$ 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进行操作。
运行go get命令获取gorm和MySQL驱动。

$ go get github.com/jinzhu/gorm
$ go get github.com/jinzhu/gorm/dialects/mysql

执行DDL/DML

请按照以下方式进行准备。
为用户名选择唯一的值,并且计划保留密码的明文状态,使用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的连接

如果不使用import指定”github.com/go-sql-driver/mysql”,就会出现运行时错误,提示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
}

模型的定义 de

定义用户模型

package models

type User struct {
    Id int64
    Name string `sql:"size:60"`
    Password string `sql:"size:60"`
    Hobby string `sql:"size:60"`
}

控制器的更改

需要将handler.go文件做如下更改:由于表名未使用复数形式的”Users”,因此需要执行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 环境

使用go get安装GraphQL。

$ 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"}}}

处理刷新令牌的实现

我在这里使用dgrijalva/jwt-go库创建一个生成刷新令牌的示例,因此我将参考这个示例进行实现。

添加对刷新令牌的管理

请将以下内容添加进去。

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] != ""
}

提供了重新获取令牌和创建令牌的功能。

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
    }
}

主要的部分也需要进行更改。

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"}

重新验证。
在标头中添加刷新令牌,并额外进行了一次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"}

使用刚刚获得的令牌执行查询操作。

$ 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"}}}
广告
将在 10 秒后关闭
bannerAds