9
7

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

Go: JWT の実装

Last updated at Posted at 2021-02-04

こちらの記事の実装を Go で行ってみました。
認証におけるJWTの利用

作成する API

Method URL Parameter Response 説明
POST /register name,password,firstname,lastname Tokenを返す ユーザ登録を行う
POST /login name,password Tokenを返す ログインを行う
GET /user Authorization ユーザ名を返す ユーザ情報を返す

準備

mkdir jwt_work && cd jwt_work
go mod init jwt_work
go get -u github.com/gin-gonic/gin

次のフォルダー構造にします。

$ tree
.
├── go.mod
├── go.sum
├── login
│   └── login.go
├── main.go
├── register
│   ├── generate_token.go
│   └── register.go
├── test_dir
│   ├── go_login.sh
│   ├── go_register.sh
│   └── go_user.sh
└── user
    └── user.go
main.go
// ---------------------------------------------------------------
//
//	main.go
//
//					Feb/04/2021
// ---------------------------------------------------------------
package main

import (
	"os"
	"fmt"
	"strings"
)

import "github.com/gin-gonic/gin"

import "jwt_work/register"
import "jwt_work/login"
import "jwt_work/user"
// ---------------------------------------------------------------
func main() {
	fmt.Fprintf (os.Stderr,"*** 開始 ***\n")

	rr := gin.Default()
	rr.GET("/ping", func(cc *gin.Context) {
		cc.JSON(200, gin.H{
			"message": "pong",
		})
	})

	rr.GET("/user", func(cc *gin.Context) {
		str_aa := cc.Request.Header["Authorization"]
		fmt.Fprintf (os.Stderr, str_aa[0] + "\n")
		slice := strings.Split(str_aa[0], " ")
		fmt.Fprintf (os.Stderr, slice[1] + "\n")
		unit_aa := user.User_proc(slice[1]) 
		cc.JSON(200, gin.H{
			"name": unit_aa["name"],
			"firstname": unit_aa["firstname"],
			"lastname": unit_aa["lastname"],
		})
	})

	rr.POST("/register", func(cc *gin.Context) {
		name := cc.PostForm("name")
		password := cc.PostForm("password")
		firstname := cc.PostForm("firstname")
		lastname := cc.PostForm("lastname")
		str_out := register.Register_proc(name,password,firstname,lastname) 
		cc.JSON(200, gin.H{
			"token": str_out,
		})
	})

	rr.POST("/login", func(cc *gin.Context) {
		name := cc.PostForm("name")
		password := cc.PostForm("password")
		str_out := login.Login_proc(name,password) 
		cc.JSON(200, gin.H{
			"token": str_out,
		})
	})

	rr.Run()
}

// ---------------------------------------------------------------
login/login.go
// ---------------------------------------------------------------
//
//	login/login.go
//
//					Feb/04/2021
// ---------------------------------------------------------------
package login

import (
	"os"
	"fmt"
	"time"
)

import "jwt_work/register"

// ---------------------------------------------------------------
func Login_proc(name string,password string) string {
	fmt.Fprintf (os.Stderr,"*** Login_proc ***\n")

	now := time.Now ()
	str_out,_ := register.Generate_token_proc(name, now)

	return str_out
}

// ---------------------------------------------------------------
register/generate_token.go
// ---------------------------------------------------------------
//
//	register/generate_token.go
//
//					Feb/04/2021
// ---------------------------------------------------------------
package register

import (
	"os"
	"fmt"
	"time"
//	"errors"

//	"encoding/json"

	"github.com/dgrijalva/jwt-go"
)

const (
    // secret は openssl rand -base64 40 コマンドで作成した。
    secret = "2FMd5FNSqS/nW2wWJy5S3ppjSHhUnLt8HuwBkTD6HqfPfBBDlykwLA=="

    // userIDKey はユーザーの ID を表す。
    userIDKey = "user_id"

    // iat と exp は登録済みクレーム名。それぞれの意味は https://tools.ietf.org/html/rfc7519#section-4.1 を参照。{
    iatKey = "iat"
    expKey = "exp"
    // }

    // lifetime は jwt の発行から失効までの期間を表す。
    lifetime = 30 * time.Minute
)

type Auth struct {
    UserID    string
    Iat       int64
}

// ---------------------------------------------------------------
func Generate_token_proc(userID string, now time.Time) (string, error) {
	fmt.Fprintf (os.Stderr,"*** Generate_token_proc ***\n")

    token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
        userIDKey: userID,
        iatKey:    now.Unix(),
        expKey:    now.Add(lifetime).Unix(),
    })

//	secret := "2FMd5FNSqS/nW2wWJy5S3ppjSHhUnLt8HuwBkTD6HqfPfBBDlykwLA=="

    return token.SignedString([]byte(secret))
}

// ---------------------------------------------------------------
func Parse_proc(signedString string) (*Auth, error) {
    token, err := jwt.Parse(signedString, func(token *jwt.Token) (interface{}, error) {
        if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
            return "", fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
//            return "", err.Errorf("unexpected signing method: %v", token.Header["alg"])
        }
        return []byte(secret), nil
    })

    if err != nil {
        if ve, ok := err.(*jwt.ValidationError); ok {
            if ve.Errors&jwt.ValidationErrorExpired != 0 {
//                return nil, err.Wrapf(err, "%s is expired", signedString)
                return nil, fmt.Errorf("%s is expired", signedString,err)
            } else {
                return nil, fmt.Errorf("%s is invalid", signedString, err)
//                return nil, err.Wrapf(err, "%s is invalid", signedString)
            }
        } else {
            return nil, fmt.Errorf("%s is invalid", signedString,err)
//            return nil, err.Wrapf(err, "%s is invalid", signedString)
        }
    }

    if token == nil {
        return nil, fmt.Errorf("not found token in %s:", signedString)
//        return nil, err.Errorf("not found token in %s:", signedString)
    }

    claims, ok := token.Claims.(jwt.MapClaims)
    if !ok {
        return nil, fmt.Errorf("not found claims in %s", signedString)
//        return nil, err.Errorf("not found claims in %s", signedString)
    }
    userID, ok := claims[userIDKey].(string)
    if !ok {
        return nil, fmt.Errorf("not found %s in %s", userIDKey, signedString)
//        return nil, err.Errorf("not found %s in %s", userIDKey, signedString)
    }
    iat, ok := claims[iatKey].(float64)
    if !ok {
        return nil, fmt.Errorf("not found %s in %s", iatKey, signedString)
//        return nil, err.Errorf("not found %s in %s", iatKey, signedString)
    }

    return &Auth{
        UserID:    userID,
        Iat:       int64(iat),
    }, nil
}

// ---------------------------------------------------------------
register/register.go
// ---------------------------------------------------------------
//
//	register/register.go
//
//					Feb/04/2021
// ---------------------------------------------------------------
package register

import (
	"os"
	"fmt"
	"time"
	"github.com/jinzhu/gorm"
	_ "github.com/go-sql-driver/mysql"
)

// ---------------------------------------------------------------
func Register_proc(name string,password string,firstname string,lastname string) string {
	fmt.Fprintf (os.Stderr,"*** Register_proc ***\n")

	now := time.Now ()
	str_out,_ := Generate_token_proc(name, now)

	db, _ := gorm.Open ("mysql","scott:tiger123@/test_db")
	defer db.Close ()

sql_str :="insert into users (name,password,firstname,lastname,created) values (?,?,?,?,?)"
        db.Exec (sql_str,name,password,firstname,lastname,now)

	return str_out
}

// ---------------------------------------------------------------
user/user.go
// ---------------------------------------------------------------
//
//	user/user.go
//
//					Feb/04/2021
// ---------------------------------------------------------------
package user

import (
	"os"
	"fmt"

	"github.com/jinzhu/gorm"
	_ "github.com/go-sql-driver/mysql"
)

import "jwt_work/register"

// ---------------------------------------------------------------
func User_proc(token string) map[string]interface{}{
	fmt.Fprintf (os.Stderr,"*** User_proc ***\n")
	fmt.Fprintf (os.Stderr,"token = " + token + "\n")

	auth,_ := register.Parse_proc(token)

	fmt.Fprintf (os.Stderr,"*** User_proc *** bbb ***\n")
	fmt.Fprintf (os.Stderr,  auth.UserID)
	fmt.Fprintf (os.Stderr,"*** User_proc *** ccc ***\n")

	name := auth.UserID
	db, _ := gorm.Open ("mysql","scott:tiger123@/test_db")
	defer db.Close ()

	firstname := ""
	lastname := ""
	row := db.Table("users").Where("name = ?", name).Select("firstname, lastname").Row() // (*sql.Row)
	row.Scan(&firstname, &lastname)
	fmt.Fprintf (os.Stderr,"firstname = " + firstname + "\n")
	fmt.Fprintf (os.Stderr,"lastname = " + lastname + "\n")

	unit_aa := make (map[string]interface{})
	unit_aa["name"] = name
	unit_aa["firstname"] = firstname
	unit_aa["lastname"] = lastname

	return unit_aa
}

// ---------------------------------------------------------------

MariaDB の用意

User: scott
Password: tiger123
Database: test_db

create_table.sql
drop table if exists users;
create table users (name varchar(16) primary key, password text, firstname text, lastname text, created date);
show columns from users;
quit

テーブルの作成

mysql -u scott -ptiger123 test_db < create_table.sql

サーバーの起動

go run main.go

起動時に出るメッセージ

$ go run main.go 
*** 開始 ***
[GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached.

[GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production.
 - using env:	export GIN_MODE=release
 - using code:	gin.SetMode(gin.ReleaseMode)

[GIN-debug] GET    /ping                     --> main.main.func1 (3 handlers)
[GIN-debug] GET    /user                     --> main.main.func2 (3 handlers)
[GIN-debug] POST   /register                 --> main.main.func3 (3 handlers)
[GIN-debug] POST   /login                    --> main.main.func4 (3 handlers)
[GIN-debug] Environment variable PORT is undefined. Using port :8080 by default
[GIN-debug] Listening and serving HTTP on :8080

テストスクリプト

Register データの登録

go_register.sh
#
URL=http://localhost:8080/register
#
curl -X POST -d name=lion -d password=tiger123 \
	-d firstname=Scott -d lastname=White \
	$URL
#
echo
#
curl -X POST -d name=panda -d password=tiger123 \
	-d firstname=John -d lastname=Clinton \
	$URL
#
echo
#
curl -X POST -d name=coala -d password=tiger123 \
	-d firstname=Mary -d lastname=Smith \
	$URL
#
echo
#

Login トークンの取得

go_login.sh
#
URL="http://localhost:8080/login"
#
curl -X POST -d name=lion -d password=tiger123 $URL > lion.json
#
jq . lion.json
echo
#
curl -X POST -d name=panda -d password=tiger123 $URL > panda.json
#
jq . panda.json
echo
#
curl -X POST -d name=coala -d password=tiger123 $URL > coala.json
#
jq . coala.json
echo
#

User トークンを渡して、ユーザー情報を得る

go_user.sh
#
URL="localhost:8080/user"
#
token=`jq .token lion.json | sed 's/"//g'`
curl -X GET $URL \
	-H "Authorization:Bearer ${token}" | jq .
#
token=`jq .token panda.json | sed 's/"//g'`
curl -X GET $URL \
	-H "Authorization:Bearer ${token}" | jq .
#
token=`jq .token coala.json | sed 's/"//g'`
curl -X GET $URL \
	-H "Authorization:Bearer ${token}" | jq .
#

実行結果

$ ./go_user.sh 

{
  "firstname": "Scott",
  "lastname": "White",
  "name": "lion"
}

{
  "firstname": "John",
  "lastname": "Clinton",
  "name": "panda"
}

{
  "firstname": "Mary",
  "lastname": "Smith",
  "name": "coala"
}

Httpie を使ったスクリプト

http_user.sh
#
URL="localhost:8080/user"
#
token=`jq .token lion.json | sed 's/"//g'`
http $URL "Authorization:Bearer ${token}"
#
token=`jq .token panda.json | sed 's/"//g'`
http $URL "Authorization:Bearer ${token}"
#
token=`jq .token coala.json | sed 's/"//g'`
http $URL "Authorization:Bearer ${token}"
#

確認したバージョン

$ go version
go version go1.20.3 linux/amd64
9
7
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
9
7

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?