GoでJWT認証するAPI Gatewayを作成する

こんにちは。新規サービス基盤チームで、ギャザリーの開発を担当している高丸です。

ギャザリーで、リクルートライフスタイルのサービス内部でまとめ記事を連携するプロジェクトがあり、既存のAPIを利用すればすぐ行けるじゃん!と思ったのですが、度重なる機能改修でギャザリー内部向けのつくりになっていました。
なんとか面倒くさい認証部分だけでも外出ししたいという思いから、API Gatewayを作成しました。

元々、Amazon API Gateway がそういった役割をしてくれると期待していたのですが、
当初は、いわゆるHTTPプロキシとして機能は物足りなく、特にIPが固定ではないので、期待していたプロキシではありませんでした……。
(一方で、AWS Lambdaと連携することで、EC2レスのアーキテクチャが構築できるようになったことは素晴らしいと思います。)

ギャザリーは、メインでRubyとJavaを使用しているのですが、ミドルウェアにふさわしい言語は何がいいだろうと考えたとき、Goに至りました。
パフォーマンスはさることながら、ミドルウェアやCLIとして使われている例が多いことも理由の一つです。

Goを本番環境で利用するのは初めてだったので、その開発過程をご紹介したいと思います。

ライブラリ探し

プログラミング言語が成長期である時代に、良いライブラリを探すのは、掘り出し物を探すようで、とても楽しく感じます。もちろん、バグもセットで付いてきます……。
「はまって、考えて、直して」の試行錯誤の繰り返しで、その言語に対して理解を深めたり、愛着を持つことができるんじゃないかなと思ってます。

これ以上話すと、プログラマー論になってしまうので、本題に戻ります。

これからGoを始める人は、まず、Awesome Goを見て、お気に入りのライブラリを探しましょう。 awesome-go

こう見ると、だいぶ揃っているなぁという印象です。 私は、次のものを選びました。

Gin

まずは、Webフレームワーク。Revelのようなフルスタックのものもありましたが、私は軽めフレームワークが好きなので、最初はMartiniを選びました。
が、最近になって、メンテナンスされないことが発表されたため、Ginに移行してます。
(JWT認証だけであれば、フレームワークは不要だと思います。)

Testify

GoのデフォルトパッケージにはTest Assertionがないので入れました。RSpecほど、くどくない書き方が良かったので、これを選びました。

toml

YAMLより、TOMLの方がアツい!という記事をどこかで見たので、流行に乗っかって使ってみました。

Logrus

一番カスタマイズができそうなロガーだったので、これを選びました。

jwt-go

今回のメイン機能となるJWT認証のためのライブラリです。

他にもまだありますが、このような組み合わせで開発をしました。

JWT認証

JWTは、JSON Web Tokensの略で、今までOAuth2で送っていたアクセストークンをJSONで置き換えるやり方です。有効期限や発行者などの情報をJSONに含め、そのJSONと改ざん防止の署名をBase64でエンコードした形で連結します。

Header

1
{ "alg": "HS256", "typ": "JWT" }

Payload

1
{ "sub": "1234567890", "name": "John Doe", "admin": true }

Base64エンコードしたHeaderとPayloadとSignatureをドットで連結

1
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ

ポイントとしては、JSONなのでその場でパースしやすい改ざんを検知できる暗号化ではないというところでしょうか。

詳しくは、上記リンクを見ていただくとして割愛しますが、Google APIsでも使用されており、
今後のAPI認証は、JWTが主流になっていくと思います。

JWT認証サーバの構築の仕方もいろいろあると思います。
私は、Goのサーバで処理を行わせましたが、この記事ではApacheモジュールを使った例も挙げられています。mrubyやLuaを使って処理してしまうことも可能ですね。

実際のコードはというと、

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
        if _, ok := token.Method.(*jwt.SigningMethodRSA); !ok {
            return nil, fmt.Errorf("Unexpected signing method: %v", token.Header["alg"])
        }
        return LookupPublicKey()
})
if err != nil || !token.Valid {
        // Invalid token
}
func LookupPublicKey() (*rsa.PublicKey, error) {
        key, _ := ioutil.ReadFile("/PATH/TO/PUBLIC_KEY")
        parsedKey, err := jwt.ParseRSAPublicKeyFromPEM(key)
        return parsedKey, err
}

こんな感じで、LookupPublickey で、署名復号用の公開鍵を渡してあげるようにすれば、Parseがチェックをしてくれます。

リバースプロキシ

Goのデフォルトパッケージは種類が豊富で、リバースプロキシを作成するためのパッケージnet/http/httputilがあります。 これを使うと、簡単にリバースプロキシを作成することができます。
以下は、Ginのrouterと合わせた例です。middlewareとして登録して、認証が必要なものと不要なものに分けることができます。

Ginと合わせた例

1
2
3
4
5
6
7
8
9
10
11
router := gin.Default()
v1 := router.Group("/api/v1")
{
        // Without Authorization
        v1.GET("/healthcheck", ReverseProxyHandler)
        // With Authorization
        authorized := v1.Group("/", CheckJWTHandler)
        authorized.GET("/users/:userId", ReverseProxyHandler)
}

Reverse Proxy

1
2
3
4
5
func ReverseProxyHandler(req *http.Request, res http.ResponseWriter) {
        remoteUrl := "API ORIGIN URL"
        proxy := httputil.NewSingleHostReverseProxy(remoteUrl)
        proxy.ServeHTTP(res, req)
}

まとめ

Gin側でAPIのルーティングを設定し、認証が必要なものはMiddlewareでJWTのチェックを行い、認証が通れば、リバースプロキシとしてバックエンドの実APIに流すAPI Gatewayの仕組みができました。

サンプルコードをGitHubに置いておきましたので、興味ある方は見てみてください。

脚注

gopher-side_path.{ai,svg,png} was created by Takuya Ueda.
Licensed under the Creative Commons 3.0 Attributions license.

cc_attribution