LoginSignup
28
11

More than 5 years have passed since last update.

コードリーディングのすヽめ。

Last updated at Posted at 2018-06-11

コードリーディングのすヽめ。

コードリーディングしてますか?コードリーディングは自分のスキルアップに非常に役に立ちますが自分は最初の頃はうまくできなかったです。

「どうしてこんなディレクトリ構成なんだろ?」

「どうしてこんな風に関数を分けるのだろう?」

このような疑問がどんどん出てきてコードを追うことができなくなってしまいます。ですのでこの記事でコードリーディングの流れを掴んでいただけたら幸いです。

gin-jwtをコードリーディング

今回はサンプルとしてgin-jwtを追うことにします。

gin-jwt github.com

なぜこの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)
    }
}

こんな感じになった。

では気を撮り直して実行する。https://www.google.com/search?q=%E6%B0%97%E3%82%92%E6%92%AE%E3%82%8A%E7%9B%B4&ie=utf-8&oe=utf-8&client=firefox-bできたので、リクエストを送ってみる。

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

エラーを全てあらかじめ定義してあり、これらを使うことでエラーの制御を適当にやらないようにしているのはとても参考になった。こうゆうのはどんどん自分のコードに取り入れていきたいと思いました。

この記事がコードリーディングが苦手な方の参考になれば幸いです。ありがとうございました。

28
11
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
28
11