コードリーディングのすヽめ。
コードリーディングしてますか?コードリーディングは自分のスキルアップに非常に役に立ちますが自分は最初の頃はうまくできなかったです。
「どうしてこんなディレクトリ構成なんだろ?」
「どうしてこんな風に関数を分けるのだろう?」
このような疑問がどんどん出てきてコードを追うことができなくなってしまいます。ですのでこの記事でコードリーディングの流れを掴んでいただけたら幸いです。
gin-jwt
をコードリーディング
今回はサンプルとしてgin-jwt
を追うことにします。
なぜこのgin-jwt
を選んだかというと単純に認証周りに興味があるからです。認証周りは知っておくべきでしょうからね。
まずは使ってみよう。
コードリーディングの最初のステップは闇雲にコードを眺めるのではなく、実際に動かしてみることです。
早速gin-jwt
を動かしてみます。
gin-jwt
のリポジトリにはサンプルコードが用意されているのでそれを使います。
package main
import (
"log"
"net/http"
"os"
"time"
"github.com/appleboy/gin-jwt"
"github.com/gin-gonic/gin"
)
func helloHandler(c *gin.Context) {
claims := jwt.ExtractClaims(c)
c.JSON(200, gin.H{
"userID": claims["id"],
"text": "Hello World.",
})
}
// User demo
type User struct {
UserName string
FirstName string
LastName string
}
func main() {
port := os.Getenv("PORT")
r := gin.New()
r.Use(gin.Logger())
r.Use(gin.Recovery())
if port == "" {
port = "8000"
}
// the jwt middleware
authMiddleware := &jwt.GinJWTMiddleware{
Realm: "test zone",
Key: []byte("secret key"),
Timeout: time.Hour,
MaxRefresh: time.Hour,
Authenticator: func(userId string, password string, c *gin.Context) (interface{}, bool) {
if (userId == "admin" && password == "admin") || (userId == "test" && password == "test") {
return &User{
UserName: userId,
LastName: "Bo-Yi",
FirstName: "Wu",
}, true
}
return nil, false
},
Authorizator: func(user interface{}, c *gin.Context) bool {
if v, ok := user.(string); ok && v == "admin" {
return true
}
return false
},
Unauthorized: func(c *gin.Context, code int, message string) {
c.JSON(code, gin.H{
"code": code,
"message": message,
})
},
// TokenLookup is a string in the form of "<source>:<name>" that is used
// to extract token from the request.
// Optional. Default value "header:Authorization".
// Possible values:
// - "header:<name>"
// - "query:<name>"
// - "cookie:<name>"
TokenLookup: "header:Authorization",
// TokenLookup: "query:token",
// TokenLookup: "cookie:token",
// TokenHeadName is a string in the header. Default value is "Bearer"
TokenHeadName: "Bearer",
// TimeFunc provides the current time. You can override it to use another time value. This is useful for testing or if your server uses a different time zone than your tokens.
TimeFunc: time.Now,
}
r.POST("/login", authMiddleware.LoginHandler)
auth := r.Group("/auth")
auth.Use(authMiddleware.MiddlewareFunc())
{
auth.GET("/hello", helloHandler)
auth.GET("/refresh_token", authMiddleware.RefreshHandler)
}
if err := http.ListenAndServe(":"+port, r); err != nil {
log.Fatal(err)
}
}
これを実行してみる。
./app.go:44:16: cannot use func literal (type func(string, string, *gin.Context) (interface {}, bool)) as type func(string, string, *gin.Context) (string, bool) in field value
./app.go:55:15: cannot use func literal (type func(interface {}, *gin.Context) bool) as type func(string, *gin.Context) bool in field value
エラーがでる。(2018/06/11現在)
これを修正すると
package main
import (
"log"
"net/http"
"os"
"time"
"github.com/appleboy/gin-jwt"
"github.com/gin-gonic/gin"
)
func helloHandler(c *gin.Context) {
claims := jwt.ExtractClaims(c)
c.JSON(200, gin.H{
"userID": claims["id"],
"text": "Hello World.",
})
}
// User demo
type User struct {
UserName string
FirstName string
LastName string
}
func main() {
port := os.Getenv("PORT")
r := gin.New()
r.Use(gin.Logger())
r.Use(gin.Recovery())
if port == "" {
port = "8000"
}
// the jwt middleware
authMiddleware := &jwt.GinJWTMiddleware{
Realm: "test zone",
Key: []byte("secret key"),
Timeout: time.Hour,
MaxRefresh: time.Hour,
Authenticator: func(userId string, password string, c *gin.Context) (string, bool) {
if (userId == "admin" && password == "admin") || (userId == "test" && password == "test") {
return userId, true
}
return "", false
},
Authorizator: func(user string, c *gin.Context) bool {
if user == "admin" {
return true
}
return false
},
Unauthorized: func(c *gin.Context, code int, message string) {
c.JSON(code, gin.H{
"code": code,
"message": message,
})
},
// TokenLookup is a string in the form of "<source>:<name>" that is used
// to extract token from the request.
// Optional. Default value "header:Authorization".
// Possible values:
// - "header:<name>"
// - "query:<name>"
// - "cookie:<name>"
TokenLookup: "header:Authorization",
// TokenLookup: "query:token",
// TokenLookup: "cookie:token",
// TokenHeadName is a string in the header. Default value is "Bearer"
TokenHeadName: "Bearer",
// TimeFunc provides the current time. You can override it to use another time value. This is useful for testing or if your server uses a different time zone than your tokens.
TimeFunc: time.Now,
}
r.POST("/login", authMiddleware.LoginHandler)
auth := r.Group("/auth")
auth.Use(authMiddleware.MiddlewareFunc())
{
auth.GET("/hello", helloHandler)
auth.GET("/refresh_token", authMiddleware.RefreshHandler)
}
if err := http.ListenAndServe(":"+port, r); err != nil {
log.Fatal(err)
}
}
こんな感じになった。
POST localhost:8000/login username=admin password=admin
{
"expire": "2018-06-11T21:12:37+09:00",
"token": "xxxxxxxxx"
}
ちゃんと動いた。
ではこのtoken
を使ってauth/hello
にアクセスしてみる。
GET localhost:8000/auth/hello "Authorization:Bearer xxxxxxxxx" "Content-Type: application/json"
{
"text": "Hello World.",
"userID": "admin"
}
動いてる。じゃあ今度はtoken
なしでリクエストを送信してみる。
GET localhost:8000/auth/hello
{
"code": 401,
"message": "auth header empty"
}
うん、ちゃんと弾くようになってる。では早速コードをみてみよう。
実際にコードを読む。
まずどこから手をつけたらわからないので、実際に利用したAPIからみてみることにしよう。
GinJWTMiddleware
これがgin-jwt
の核になってるぽいのは実際に動かしてみたからわかる。では実際のコードをみてみよう。
// GinJWTMiddleware provides a Json-Web-Token authentication implementation. On failure, a 401 HTTP response
// is returned. On success, the wrapped middleware is called, and the userID is made available as
// c.Get("userID").(string).
// Users can get a token by posting a json request to LoginHandler. The token then needs to be passed in
// the Authentication header. Example: Authorization:Bearer XXX_TOKEN_XXX
type GinJWTMiddleware struct {
// Realm name to display to the user. Required.
Realm string
// signing algorithm - possible values are HS256, HS384, HS512
// Optional, default is HS256.
SigningAlgorithm string
// Secret key used for signing. Required.
Key []byte
// Duration that a jwt token is valid. Optional, defaults to one hour.
Timeout time.Duration
// This field allows clients to refresh their token until MaxRefresh has passed.
// Note that clients can refresh their token in the last moment of MaxRefresh.
// This means that the maximum validity timespan for a token is MaxRefresh + Timeout.
// Optional, defaults to 0 meaning not refreshable.
MaxRefresh time.Duration
// Callback function that should perform the authentication of the user based on userID and
// password. Must return true on success, false on failure. Required.
// Option return user data, if so, user data will be stored in Claim Array.
Authenticator func(userID string, password string, c *gin.Context) (interface{}, bool)
// Callback function that should perform the authorization of the authenticated user. Called
// only after an authentication success. Must return true on success, false on failure.
// Optional, default to success.
Authorizator func(data interface{}, c *gin.Context) bool
// Callback function that will be called during login.
// Using this function it is possible to add additional payload data to the webtoken.
// The data is then made available during requests via c.Get("JWT_PAYLOAD").
// Note that the payload is not encrypted.
// The attributes mentioned on jwt.io can't be used as keys for the map.
// Optional, by default no additional data will be set.
PayloadFunc func(data interface{}) MapClaims
// User can define own Unauthorized func.
Unauthorized func(*gin.Context, int, string)
// User can define own LoginResponse func.
LoginResponse func(*gin.Context, int, string, time.Time)
// User can define own RefreshResponse func.
RefreshResponse func(*gin.Context, int, string, time.Time)
// Set the identity handler function
IdentityHandler func(jwt.MapClaims) interface{}
// TokenLookup is a string in the form of "<source>:<name>" that is used
// to extract token from the request.
// Optional. Default value "header:Authorization".
// Possible values:
// - "header:<name>"
// - "query:<name>"
// - "cookie:<name>"
TokenLookup string
// TokenHeadName is a string in the header. Default value is "Bearer"
TokenHeadName string
// TimeFunc provides the current time. You can override it to use another time value. This is useful for testing or if your server uses a different time zone than your tokens.
TimeFunc func() time.Time
// HTTP Status messages for when something in the JWT middleware fails.
// Check error (e) to determine the appropriate error message.
HTTPStatusMessageFunc func(e error, c *gin.Context) string
// Private key file for asymmetric algorithms
PrivKeyFile string
// Public key file for asymmetric algorithms
PubKeyFile string
// Private key
privKey *rsa.PrivateKey
// Public key
pubKey *rsa.PublicKey
// Optionally return the token as a cookie
SendCookie bool
// Allow insecure cookies for development over http
SecureCookie bool
}
golangには、golintというリンターがあり、それを利用すると外部から利用できる関数や変数はコメントをつけるように推奨されるようになっている。なので大体のコードにはコメントが付いているのでそれをまずは読む。
一つ試しに解説してみる。
// Callback function that should perform the authentication of the user based on userID and
// password. Must return true on success, false on failure. Required.
// Option return user data, if so, user data will be stored in Claim Array.
Authenticator func(userID string, password string, c *gin.Context) (interface{}, bool)
ここの部分を一緒に見ていくことにしよう。要約すると以下のようなことが書いてある。
ユーザの認証をする関数はuserIDとpasswordを元にしている。
そしてもし、認証が成功ならばtrueを返し、失敗ならばfalseを返すようにしなさい。これは必須です。
付属の機能としてユーザのデータを一緒に返すことができます。それはClaim Arrayに保存しておきます。
つまり、このAuthenticator
には認証する際の処理を書きなさいと書いてあるわけだ。なのでサンプルのコードでは
Authenticator: func(userId string, password string, c *gin.Context) (string, bool) {
if (userId == "admin" && password == "admin") || (userId == "test" && password == "test") {
return userId, true
}
return "", false
},
このような処理を実装していたことになる。
ではいつこの関数が呼び出されているかがきになる。それを追ってみる。
あった。以下の関数で呼び出されている。
// LoginHandler can be used by clients to get a jwt token.
// Payload needs to be json in the form of {"username": "USERNAME", "password": "PASSWORD"}.
// Reply will be of the form {"token": "TOKEN"}.
func (mw *GinJWTMiddleware) LoginHandler(c *gin.Context) {
// Initial middleware default setting.
if err := mw.MiddlewareInit(); err != nil {
mw.unauthorized(c, http.StatusInternalServerError, mw.HTTPStatusMessageFunc(err, c))
return
}
var loginVals Login
if c.ShouldBind(&loginVals) != nil {
mw.unauthorized(c, http.StatusBadRequest, mw.HTTPStatusMessageFunc(ErrMissingLoginValues, c))
return
}
if mw.Authenticator == nil {
mw.unauthorized(c, http.StatusInternalServerError, mw.HTTPStatusMessageFunc(ErrMissingAuthenticatorFunc, c))
return
}
data, ok := mw.Authenticator(loginVals.Username, loginVals.Password, c)
if !ok {
mw.unauthorized(c, http.StatusUnauthorized, mw.HTTPStatusMessageFunc(ErrFailedAuthentication, c))
return
}
// Create the token
token := jwt.New(jwt.GetSigningMethod(mw.SigningAlgorithm))
claims := token.Claims.(jwt.MapClaims)
if mw.PayloadFunc != nil {
for key, value := range mw.PayloadFunc(data) {
claims[key] = value
}
}
if claims["id"] == nil {
claims["id"] = loginVals.Username
}
expire := mw.TimeFunc().Add(mw.Timeout)
claims["exp"] = expire.Unix()
claims["orig_iat"] = mw.TimeFunc().Unix()
tokenString, err := mw.signedString(token)
if err != nil {
mw.unauthorized(c, http.StatusUnauthorized, mw.HTTPStatusMessageFunc(ErrFailedTokenCreation, c))
return
}
// set cookie
if mw.SendCookie {
maxage := int(expire.Unix() - time.Now().Unix())
c.SetCookie(
"JWTToken",
tokenString,
maxage,
"/",
"",
mw.SecureCookie,
true,
)
}
mw.LoginResponse(c, http.StatusOK, tokenString, expire)
}
この関数の中でさっき見たAuthenticator
が呼ばれている。もしAuthenticator
が失敗したら
mw.unauthorized(c, http.StatusUnauthorized, mw.HTTPStatusMessageFunc(ErrFailedAuthentication, c))
が呼ばれるようになっている。
成功した場合はトークンの作成をし始めるようになっている。
ではこのLoginHandler
はいつ呼ばれているのだろう?最初に書いたサンプルコードにあった。
r.POST("/login", authMiddleware.LoginHandler)
なるほどー。こういう風に繋がっていくのか。
まとめ
コードリーディングを軽くではありますが、やって見ました。コードリーディングをするといろんな発見があります。今回のgin-jwt
であれば
最初の方に
var (
// ErrMissingRealm indicates Realm name is required
ErrMissingRealm = errors.New("realm is missing")
// ErrMissingSecretKey indicates Secret key is required
ErrMissingSecretKey = errors.New("secret key is required")
// ErrForbidden when HTTP status 403 is given
ErrForbidden = errors.New("you don't have permission to access this resource")
// ErrMissingAuthenticatorFunc indicates Authenticator is required
ErrMissingAuthenticatorFunc = errors.New("ginJWTMiddleware.Authenticator func is undefined")
// ErrMissingLoginValues indicates a user tried to authenticate without username or password
ErrMissingLoginValues = errors.New("missing Username or Password")
// ErrFailedAuthentication indicates authentication failed, could be faulty username or password
ErrFailedAuthentication = errors.New("incorrect Username or Password")
// ErrFailedTokenCreation indicates JWT Token failed to create, reason unknown
ErrFailedTokenCreation = errors.New("failed to create JWT Token")
// ErrExpiredToken indicates JWT token has expired. Can't refresh.
ErrExpiredToken = errors.New("token is expired")
// ErrEmptyAuthHeader can be thrown if authing with a HTTP header, the Auth header needs to be set
ErrEmptyAuthHeader = errors.New("auth header is empty")
// ErrInvalidAuthHeader indicates auth header is invalid, could for example have the wrong Realm name
ErrInvalidAuthHeader = errors.New("auth header is invalid")
// ErrEmptyQueryToken can be thrown if authing with URL Query, the query token variable is empty
ErrEmptyQueryToken = errors.New("query token is empty")
// ErrEmptyCookieToken can be thrown if authing with a cookie, the token cokie is empty
ErrEmptyCookieToken = errors.New("cookie token is empty")
// ErrInvalidSigningAlgorithm indicates signing algorithm is invalid, needs to be HS256, HS384, HS512, RS256, RS384 or RS512
ErrInvalidSigningAlgorithm = errors.New("invalid signing algorithm")
// ErrNoPrivKeyFile indicates that the given private key is unreadable
ErrNoPrivKeyFile = errors.New("private key file unreadable")
// ErrNoPubKeyFile indicates that the given public key is unreadable
ErrNoPubKeyFile = errors.New("public key file unreadable")
// ErrInvalidPrivKey indicates that the given private key is invalid
ErrInvalidPrivKey = errors.New("private key invalid")
// ErrInvalidPubKey indicates the the given public key is invalid
ErrInvalidPubKey = errors.New("public key invalid")
)
エラーを全てあらかじめ定義してあり、これらを使うことでエラーの制御を適当にやらないようにしているのはとても参考になった。こうゆうのはどんどん自分のコードに取り入れていきたいと思いました。
この記事がコードリーディングが苦手な方の参考になれば幸いです。ありがとうございました。