TL;DR
公式ドキュメントも間違ってるときだってある。
公式ドキュメント通りにやってんのに動かねぇ!!
ある日私は、JWTを使った認証をGolangとechoで実装しようと考えました。
とりあえず何も分からなかったので、echoの公式ドキュメントにあるCookbookから以下のコードをコピペしてGoに突っ込んでみました。
コピーしたコード
https://echo.labstack.com/cookbook/jwt/ より引用
package main
import (
"github.com/golang-jwt/jwt/v4"
echojwt "github.com/labstack/echo-jwt/v4"
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
"net/http"
"time"
)
// jwtCustomClaims are custom claims extending default ones.
// See https://github.com/golang-jwt/jwt for more examples
type jwtCustomClaims struct {
Name string `json:"name"`
Admin bool `json:"admin"`
jwt.RegisteredClaims
}
func login(c echo.Context) error {
username := c.FormValue("username")
password := c.FormValue("password")
// Throws unauthorized error
if username != "jon" || password != "shhh!" {
return echo.ErrUnauthorized
}
// Set custom claims
claims := &jwtCustomClaims{
"Jon Snow",
true,
jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Hour * 72)),
},
}
// Create token with claims
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
// Generate encoded token and send it as response.
t, err := token.SignedString([]byte("secret"))
if err != nil {
return err
}
return c.JSON(http.StatusOK, echo.Map{
"token": t,
})
}
func accessible(c echo.Context) error {
return c.String(http.StatusOK, "Accessible")
}
func restricted(c echo.Context) error {
user := c.Get("user").(*jwt.Token)
claims := user.Claims.(*jwtCustomClaims)
name := claims.Name
return c.String(http.StatusOK, "Welcome "+name+"!")
}
func main() {
e := echo.New()
// Middleware
e.Use(middleware.Logger())
e.Use(middleware.Recover())
// Login route
e.POST("/login", login)
// Unauthenticated route
e.GET("/", accessible)
// Restricted group
r := e.Group("/restricted")
// Configure middleware with the custom claims type
config := echojwt.Config{
NewClaimsFunc: func(c echo.Context) jwt.Claims {
return new(jwtCustomClaims)
},
SigningKey: []byte("secret"),
}
r.Use(echojwt.WithConfig(config))
r.GET("", restricted)
e.Logger.Fatal(e.Start(":1323"))
}
コードをコピーして...
依存関係をgetして...
コンパイルして......
動かねえ!!
エラーとの格闘
/login
は動作するのに、/restricted
を使おうとするとgo側が長文エラーを吐き始めます。
長文エラー(ユーザー名など一部改変してあります)
{"time":"2023-05-22T00:24:04.737973449+09:00","level":"-","prefix":"echo","file":"recover.go","line":"113","message":"[PANIC RECOVER] interface conversion: interface {} is *jwt.Token, not *jwt.Token (types from different packages) goroutine 19 [running]:\ngithub.com/labstack/echo/v4/middleware.RecoverWithConfig.func1.1.1()\n\t/home/user/go/pkg/mod/github.com/labstack/echo/v4@v4.10.2/middleware/recover.go:93 +0x14e\npanic({0x6ee9e0, 0xc00011bef0})\n\t/usr/lib/go-1.20/src/runtime/panic.go:884 +0x213\ntest/accessible({0x7c0558, 0xc0000abcc0})\n\t/projectpath/main.go:57 +0xd9\ngithub.com/labstack/echo-jwt/v4.Config.ToMiddleware.func2.1({0x7c0558, 0xc0000abcc0})\n\t/home/user/go/pkg/mod/github.com/labstack/echo-jwt/v4@v4.2.0/jwt.go:237 +0x41d\ngithub.com/labstack/echo/v4.(*Echo).add.func1({0x7c0558, 0xc0000abcc0})\n\t/home/user/go/pkg/mod/github.com/labstack/echo/v4@v4.10.2/echo.go:575 +0x51\ngithub.com/labstack/echo/v4/middleware.RecoverWithConfig.func1.1({0x7c0558, 0xc0000abcc0})\n\t/home/user/go/pkg/mod/github.com/labstack/echo/v4@v4.10.2/middleware/recover.go:119 +0xfe\ngithub.com/labstack/echo/v4/middleware.LoggerWithConfig.func2.1({0x7c0558, 0xc0000abcc0})\n\t/home/user/go/pkg/mod/github.com/labstack/echo/v4@v4.10.2/middleware/logger.go:126 +0xe2\ngithub.com/labstack/echo/v4.(*Echo).ServeHTTP(0xc0000c66c0, {0x7bcf70?, 0xc00014a000}, 0xc000100200)\n\t/home/user/go/pkg/mod/github.com/labstack/echo/v4@v4.10.2/echo.go:662 +0x3d1\nnet/http.serverHandler.ServeHTTP({0xc00011bc20?}, {0x7bcf70, 0xc00014a000}, 0xc000100200)\n\t/usr/lib/go-1.20/src/net/http/server.go:2936 +0x316\nnet/http.(*conn).serve(0xc0000fc2d0, {0x7bd198, 0xc00011bb30})\n\t/usr/lib/go-1.20/src/net/http/server.go:1995 +0x612\ncreated by net/http.(*Server).Serve\n\t/usr/lib/go-1.20/src/net/http/server.go:3089 +0x5ed\n\ngoroutine 1 [IO wait]:\ninternal/poll.runtime_pollWait(0x7f7d96c1dff8, 0x72)\n\t/usr/lib/go-1.20/src/runtime/netpoll.go:306 +0x89\ninternal/poll.(*pollDesc).wait(0xc0000fea00?, 0x4?, 0x0)\n\t/usr/lib/go-1.20/src/internal/poll/fd_poll_runtime.go:84 +0x32\ninternal/poll.(*pollDesc).waitRead(...)\n\t/usr/lib/go-1.20/src/internal/poll/fd_poll_runtime.go:89\ninternal/poll.(*FD).Accept(0xc0000fea00)\n\t/usr/lib/go-1.20/src/internal/poll/fd_unix.go:614 +0x2bd\nnet.(*netFD).accept(0xc0000fea00)\n\t/usr/lib/go-1.20/src/net/fd_unix.go:172 +0x35\nnet.(*TCPListener).accept(0xc0000a8810)\n\t/usr/lib/go-1.20/src/net/tcpsock_posix.go:148 +0x25\nnet.(*TCPListener).AcceptTCP(0xc0000a8810)\n\t/usr/lib/go-1.20/src/net/tcpsock.go:284 +0x3d\ngithub.com/labstack/echo/v4.tcpKeepAliveListener.Accept({0x4410a0?})\n\t/home/user/go/pkg/mod/github.com/labstack/echo/v4@v4.10.2/echo.go:982 +0x1d\nnet/http.(*Server).Serve(0xc000130000, {0x7bcd60, 0xc0000b4180})\n\t/usr/lib/go-1.20/src/net/http/server.go:3059 +0x385\ngithub.com/labstack/echo/v4.(*Echo).Start(0xc0000c66c0, {0xc00009eb9a, 0x5})\n\t/home/user/go/pkg/mod/github.com/labstack/echo/v4@v4.10.2/echo.go:679 +0xce\nmain.main()\n\t/projectpath/main.go:87 +0x5a6\n\ngoroutine 20 [runnable]:\nsyscall.Syscall(0x0?, 0x0?, 0x4ef052?, 0x7ffff800000?)\n\t/usr/lib/go-1.20/src/syscall/syscall_linux.go:69 +0x27\nsyscall.read(0xc0000fea80?, {0xc00011bc31?, 0x0?, 0x0?})\n\t/usr/lib/go-1.20/src/syscall/zsyscall_linux_amd64.go:711 +0x45\nsyscall.Read(...)\n\t/usr/lib/go-1.20/src/syscall/syscall_unix.go:178\ninternal/poll.ignoringEINTRIO(...)\n\t/usr/lib/go-1.20/src/internal/poll/fd_unix.go:794\ninternal/poll.(*FD).Read(0xc0000fea80?, {0xc00011bc31?, 0x1?, 0x1?})\n\t/usr/lib/go-1.20/src/internal/poll/fd_unix.go:163 +0x2ce\nnet.(*netFD).Read(0xc0000fea80, {0xc00011bc31?, 0x0?, 0x0?})\n\t/usr/lib/go-1.20/src/net/fd_posix.go:55 +0x29\nnet.(*conn).Read(0xc0000b41a0, {0xc00011bc31?, 0x0?, 0x0?})\n\t/usr/lib/go-1.20/src/net/net.go:183 +0x45\nnet/http.(*connReader).backgroundRead(0xc00011bc20)\n\t/usr/lib/go-1.20/src/net/http/server.go:674 +0x3f\ncreated by net/http.(*connReader).startBackgroundRead\n\t/usr/lib/go-1.20/src/net/http/server.go:670 +0xca\n\n"}
{"time":"2023-05-22T00:24:04.738101663+09:00","id":"","remote_ip":"127.0.0.1","host":"localhost:1323","method":"GET","uri":"/restricted","user_agent":"curl/7.81.0","status":500,"error":"","latency":367498,"latency_human":"367.498µs","bytes_in":0,"bytes_out":36}
う~ん、わからんw!
長文エラーが一瞬にして吐かれたので、頭の使用率は100%です。理解できません。
とりあえず最初の方に[PANIC RECOVER] interface conversion: interface {} is *jwt.Token, not *jwt.Token
と表示されてるので、型アサーション系のエラーということだけはわかりました。
どう見ても*jwt.Token
と*jwt.Token
って同じなのに何が違うんや??と思い色々調べました。
(今見たらnot *jwt.Token~
の後に(types from different packages)
って書いてありますね...これに気づいてたらもっと早く解決できてたかもしれません。)
エラーについて調べてgo mod tidy
をしてみたり、コードの順番を入れ替えてみたり、OSを変えてみたり...
あれこれ試しているうちに一日が過ぎました。
そして、このissueにたどり着きます。
このissueについてるコメントに、
echo
で使ってるjwtと、import
で使うjwtのバージョンを合わせてくれよな!(超絶意訳)
と書いてあり、まさかとは思いながらecho-jwtのREADMEを読んでみました。
するとどうやら、最近echo-jwt
が使うjwt
のバージョンがv4
からv5
に変わっているようでした。
そこでコード内のimport
で使っているjwtのバージョンをv5
にすると
動いた!!!
どうやらバージョン違いの問題で一日を潰したようです。
同じ作成者なら依存関係に破壊的なアップデートをしたときにドキュメントも更新してくれ!!
ということで問題になっていたドキュメントを修正して、GithubでPull Requestを出しました。
教訓
- 公式ドキュメントも万能じゃない。エラーが起きたらバージョン違いなども視野に入れて解決しよう。
- エラー文はちゃんと読もう。
ここまで読んでいただいてありがとうございました!