こちらの記事の実装を 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