Go 言語用 Web フレームワーク “Gin” でログイン認証を実装

はじめに

前回までで、Gin フレークワームを使った本当に簡単なプログラムを組むことができました。

今回はログイン認証機構を実装していきます。

JSON Web Token とは?

JSON Web Token ( JWT ) は、RFC 7519 で定義された標準規格です。
JWT は、クライアントとサーバー間で情報を安全にやり取りするための仕組みです。
オブジェクトを使用して、クライアントとサーバー間で情報を暗号化し、署名し、またはその両方を行うことができます。

JWT についてはここまでにして本題に入りたいと思います。

作成する REST API について

3 つのエンドポイントを作成します。

  1. 「サインアップ」エンドポイント
  2. 「ログイン」エンドポイント
  3. 「ログイン後にしか呼び出しできない」エンドポイント

「サインアップ」エンドポイント

当然ながら、ログイン前にユーザーのサインアップ ( ユーザー登録 ) が必要です。
このパスは JWT を不要にし公開したままにします。

  • /api/register

「ログイン」エンドポイント

ログインに利用されるパスです。
「ユーザー名」と「パスワード」を受け取り、JWT を生成して返却します。

  • /api/ogin

「ログイン後にしか呼び出しできない」エンドポイント

このパスは JWT が必須となります。

  • /api/admin/user

作業開始 ( セットアップ )

まずはプロジェクトフォルダを作成します。

$ mkdir jwt-gin

$ cd jwt-gin

お約束の go.mod ファイルを作成します。
ここで作成するモジュール(アプリケーション)名は jwt-gin としました。

$ go mod init jwt-gin

必要なモジュールをインストールします。

# Gin フレームワーク
$ go get -u github.com/gin-gonic/gin

# ORM ライブラリ
$ go get -u github.com/jinzhu/gorm

# JWT の認証と生成に使用するパッケージ
$ go get -u github.com/dgrijalva/jwt-go

# .env ファイルから環境変数を読み込む
$ go get -u github.com/joho/godotenv

# パスワード暗号化
$ go get -u golang.org/x/crypto

必要なモジュールをインストールして準備を完了したら、コーディングを開始します。プログラムの main 関数を作成し、最初のエンドポイントを作成します。

コーディング開始

プログラムの main 関数作成

プロジェクトルートディレクトリ に main.go ファイルを作ります。

$ vi main.go

最初のエンドポイントを作成します。

main.go

package main

import (
    "net/http"

    "github.com/gin-gonic/gin"
)

func main() {
    router := gin.Default()

    public := router.Group("/api")

    public.POST("/register", func(c *gin.Context) {
        c.JSON(http.StatusOK, gin.H{
            "data": "this is the register endpoint.",
        })
    })

    router.Run(":8080")
}

実行してみます。

$ go run main.go

ブラウザや curl でアクセスし、JSON が出力されれば成功です。

$ curl -X POST http://localhost:8080/api/register
{"data":"this is the register endpoint."}%

次にサインアップのメインロジックをコーディングしていきます。

「サインアップ」エンドポイント Controller 作成

登録処理を追加します。

Controller を格納するディレクトリ controllers を作成します。

$ mkdir ./controllers

auth.go というファイルを作成し、Controller の処理を実装します。

$ vi ./controllers/auth.go

auth.go

package controllers

import (
    "net/http"

    "github.com/gin-gonic/gin"
)

func Register(c *gin.Context) {
    c.JSON(http.StatusOK, gin.H{
        "data": "hello from the register endpoint.",
    })
}

作成した Register 関数が /register エンドポイントと結びつくよう main.go を変更します。

main.go

package main

import (
    "jwt-gin/controllers"

    "github.com/gin-gonic/gin"
)

func main() {
    router := gin.Default()

    public := router.Group("/api")

    public.POST("/register", controllers.Register)

    router.Run(":8080")
}

main.go が起動済みの場合は一度停止してから、起動します。

$ go run main.go

ブラウザや curl でアクセスし、JSON が出力されれば成功です。

$ curl -X POST http://localhost:8080/api/register
{"data":"hello from the register endpoint."}

Register 関数を更に充実させていきます。

「サインアップ」エンドポイント 入力チェック

サインアップには、ユーザー名とパスワードの情報だけが必要です。

binding と呼ばれる Gin の入力情報のチェック機能を利用します。

auth.go

package controllers

import (
    "net/http"

    "github.com/gin-gonic/gin"
)

type RegisterInput struct {
    Username string `json:"username" binding:"required"`
    Password string `json:"password" binding:"required"`
}

func Register(c *gin.Context) {
    var input RegisterInput

    if err := c.ShouldBindJSON(&input); err != nil {
        c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
        return
    }

    c.JSON(http.StatusOK, gin.H{
        "data": "validated!",
    })
}

go run main.go を起動し直した後、動作確認します。

  1. ユーザー名だけ設定したリクエスト

    $ curl -X POST http://localhost:8080/api/register --data '{"username":"genzouw"}'
    {"error":"Key: 'RegisterInput.Password' Error:Field validation for 'Password' failed on the 'required' tag"}%
  2. パスワードだけ設定したリクエスト

    $ curl -X POST http://localhost:8080/api/register --data '{"password":"passwd"}'
    {"error":"Key: 'RegisterInput.Username' Error:Field validation for 'Username' failed on the 'required' tag"}%
  3. ユーザー名、パスワードの両方を設定したリクエスト

    $ curl -X POST http://localhost:8080/api/register --data '{"username":"genzouw", "password":"passwd"}'
    {"data":"validated!"}%

「サインアップ」エンドポイント DB 初期化 + Model 作成

認証情報を MySQL データベースに保存ようと思います。

既存の MySQL データベースがあればそちらを使っても構いません。
ここでは Docker を使って、MySQL を用意します。

# user=dev, password=dev, database=dev
$ docker run --rm -p 3307:3306 -e MYSQL_ROOT_PASSWORD=dev -e MYSQL_USER=dev -e MYSQL_PASSWORD=dev -e MYSQL_DATABASE=dev -d mysql/mysql-server:5.7

正しく起動すれば mysql コマンドで接続できるはずです。

$ mysql -h 127.0.0.1 -P 3307 -u dev -pdev dev

データベースへの接続処理と Model を格納するためのパッケージとコードを作成します。

$ mkdir -p ./models/

$ vi ./models/setup.go

./models/setup.go に以下のコードを記述します。

setup.go

package models

import (
    "fmt"
    "log"
    "os"

    "github.com/jinzhu/gorm"
    _ "github.com/jinzhu/gorm/dialects/mysql"
    "github.com/joho/godotenv"
)

var DB *gorm.DB

func ConnectDataBase() {
    err := godotenv.Load()

    if err != nil {
        log.Fatalf("Error loading .env file")
    }

    driver := os.Getenv("DB_DRIVER")
    dbUser := os.Getenv("DB_USER")
    dbPass := os.Getenv("DB_PASS")
    dbName := os.Getenv("DB_NAME")
    dbHost := os.Getenv("DB_HOST")
    dbPort := os.Getenv("DB_PORT")

    dbURI := fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?charset=utf8&parseTime=True&loc=Local", dbUser, dbPass, dbHost, dbPort, dbName)

    DB, err := gorm.Open(driver, dbURI)

    if err != nil {
        log.Fatal("Could not connect to the database", err)
    }

    DB.AutoMigrate(&User{})
}

以下の 2 つのものも必要となります。

  • .env
  • User 構造体

引き続き作成していきます。

プロジェクトのルートディレクトリに .env ファイルを作り、 MySQL サーバーへの接続情報を記述します。

$ cat <<EOF >>.env
DB_DRIVER=mysql
DB_USER=dev
DB_PASS=dev
DB_NAME=dev
DB_HOST=localhost
DB_PORT=3307
EOF

User Model を作成します。

$ vi ./models/user.go

user.go

package models

import "github.com/jinzhu/gorm"

type User struct {
    gorm.Model
    Username string `gorm:"size:255;not null;unique" json:"username"`
    Password string `gorm:"size:255;not null;" json:"password"`
}

main.go にデータベース接続処理を追記します。

main.go

package main

import (
    "jwt-gin/controllers"
    "jwt-gin/models"

    "github.com/gin-gonic/gin"
)

func main() {
    models.ConnectDataBase()

    router := gin.Default()

    public := router.Group("/api")

    public.POST("/register", controllers.Register)

    router.Run(":8080")
}

ようやくデータベース接続処理の動作確認ができるところまで来ました。

main.go を立ち上げ直します。

# 立ち上げの前に、使用するパッケージが増えたため追加処理を行っておきます
$ go mod tidy

# 起動
$ go run main.go

MySQL データベース内のテーブルを一覧表示してみます。

$ mysql -h 127.0.0.1 -P 3307 -u dev -pdev dev -e "show tables;"
mysql: [Warning] Using a password on the command line interface can be insecure.
+---------------+
| Tables_in_dev |
+---------------+
| users         |
+---------------+

データベースのマイグレーション処理が正しく動作し users テーブルが追加されました。

「サインアップ」エンドポイント ユーザー情報登録

サインアップの実装の最終段階です。

users テーブルへのデータ登録ロジックを実装します。

user.go

package models

import (
    "strings"

    "github.com/jinzhu/gorm"
    "golang.org/x/crypto/bcrypt"
)

type User struct {
    gorm.Model
    Username string `gorm:"size:255;not null;unique" json:"username"`
    Password string `gorm:"size:255;not null;" json:"password"`
}

func (u User) Save() (User, error) {
    err := DB.Create(&u).Error
    if err != nil {
        return User{}, err
    }
    return u, nil
}

func (u *User) BeforeSave() error {
    hashedPassword, err := bcrypt.GenerateFromPassword([]byte(u.Password), bcrypt.DefaultCost)
    if err != nil {
        return err
    }

    u.Password = string(hashedPassword)

    u.Username = strings.ToLower(u.Username)

    return nil
}

func (u User) PrepareOutput() User {
    u.Password = ""
    return u
}

User 構造体に追加したメソッドを呼び出し、データを保存します。

auth.go

package controllers

import (
    "net/http"

    "jwt-gin/models"

    "github.com/gin-gonic/gin"
)

type RegisterInput struct {
    Username string `json:"username" binding:"required"`
    Password string `json:"password" binding:"required"`
}

func Register(c *gin.Context) {
    var input RegisterInput

    if err := c.ShouldBindJSON(&input); err != nil {
        c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
        return
    }

    user := models.User{Username: input.Username, Password: input.Password}

    user, err := user.Save()
    if err != nil {
        c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
        return
    }

    c.JSON(http.StatusOK, gin.H{
        "data": user.PrepareOutput(),
    })
}

main.go を立ち上げ直します。

# 起動
$ go run main.go

再度、 curl などでリクエストを投げてみます。

$ curl -X POST http://localhost:8080/api/register --data '{"username":"genzouw", "password":"passwd"}'
{"data":{"ID":7,"CreatedAt":"2023-03-29T17:28:06.88854+09:00","UpdatedAt":"2023-03-29T17:28:06.88854+09:00","DeletedAt":null,"username":"genzouw","password":""}}%

$ mysql -h 127.0.0.1 -P 3307 -u dev -pdev dev -e "select * from users;"
mysql: [Warning] Using a password on the command line interface can be insecure.
+----+---------------------+---------------------+------------+----------+--------------------------------------------------------------+
| id | created_at          | updated_at          | deleted_at | username | password                                                     |
+----+---------------------+---------------------+------------+----------+--------------------------------------------------------------+
|  7 | 2023-03-29 17:28:07 | 2023-03-29 17:28:07 | NULL       | genzouw  | $2a$10$G/ojbw02GTTGntOETLWzfekRmZaGfnVNLHSmnd8FFwTMY8tekqSte |
+----+---------------------+---------------------+------------+----------+--------------------------------------------------------------+

データが登録され、かつパスワードはハッシュ化されています。

「ログイン」エンドポイント 作成

ログイン用のエンドポイントはとてもシンプルです。

ユーザー名とパスワードを受け取り、データベース内の認証情報 ( users テーブル) と突き合わせを行います。

データが見つからなかった場合は、エラーレスポンスを返します。

main.go を編集します。

package main

import (
    "jwt-gin/controllers"
    "jwt-gin/models"

    "github.com/gin-gonic/gin"
)

func main() {
    models.ConnectDataBase()

    router := gin.Default()

    public := router.Group("/api")

    public.POST("/register", controllers.Register)
    public.POST("/login", controllers.Login)

    router.Run(":8080")
}

controllers.Login メソッドの作成のために、 auth.go も編集します。

auth.go

package controllers

import (
    "net/http"

    "jwt-gin/models"

    "github.com/gin-gonic/gin"
)

type RegisterInput struct {
    Username string `json:"username" binding:"required"`
    Password string `json:"password" binding:"required"`
}

func Register(c *gin.Context) {
    var input RegisterInput

    if err := c.ShouldBindJSON(&input); err != nil {
        c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
        return
    }

    user := models.User{Username: input.Username, Password: input.Password}

    user, err := user.Save()

    if err != nil {
        c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
        return
    }

    c.JSON(http.StatusOK, gin.H{
        "data": user,
    })
}

type LoginInput struct {
    Username string `json:"username" binding:"required"`
    Password string `json:"password" binding:"required"`
}

func Login(c *gin.Context) {
    var input LoginInput

    if err := c.ShouldBindJSON(&input); err != nil {
        c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
        return
    }

    token, err := models.GenerateToken(input.Username, input.Password)

    if err != nil {
        c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
        return
    }

    c.JSON(http.StatusOK, gin.H{
        "token": token,
    })
}

Controller であるLogin 関数から呼び出している GenerateToken 関数を user.go ファイルに追記します。

user.go

package models

import (
    "jwt-gin/utils/token"
    "strings"

    "github.com/jinzhu/gorm"
    "golang.org/x/crypto/bcrypt"
)

type User struct {
    gorm.Model
    Username string `gorm:"size:255;not null;unique" json:"username"`
    Password string `gorm:"size:255;not null;" json:"password"`
}

func (u User) Save() (User, error) {
    err := DB.Create(&u).Error

    if err != nil {
        return User{}, err
    }
    return u, nil
}

func (u *User) BeforeSave() error {
    hashedPassword, err := bcrypt.GenerateFromPassword([]byte(u.Password), bcrypt.DefaultCost)

    if err != nil {
        return err
    }

    u.Password = string(hashedPassword)

    u.Username = strings.ToLower(u.Username)

    return nil
}

func (u User) PrepareOutput() User {
    u.Password = ""
    return u
}

func GenerateToken(username string, password string) (string, error) {
    var user User

    err := DB.Where("username = ?", username).First(&user).Error

    if err != nil {
        return "", err
    }

    err = bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(password))

    if err != nil {
        return "", err
    }

    token, err := token.GenerateToken(user.ID)

    if err != nil {
        return "", err
    }

    return token, nil
}

トークンを操作する関数は token.go というファイルを新規に作成しこちらに含めます。

$ mkdir -p ./utils/token

$ vi ./utils/token/token.go

以下の3つの関数を外部から利用できるようにします。

  • GenerateToken()
  • TokenValid()
  • ExtractTokenId()

ただし、 TokenValid()ExtractTokenId() の 2 つは後ほど利用します。

token.go

package token

import (
    "fmt"
    "os"
    "strconv"
    "strings"
    "time"

    "github.com/dgrijalva/jwt-go"
    "github.com/gin-gonic/gin"
)

func GenerateToken(id uint) (string, error) {
    tokenLifespan, err := strconv.Atoi(os.Getenv("TOKEN_HOUR_LIFESPAN"))

    if err != nil {
        return "", err
    }

    claims := jwt.MapClaims{}
    claims["authorized"] = true
    claims["user_id"] = id
    claims["exp"] = time.Now().Add(time.Hour * time.Duration(tokenLifespan)).Unix()
    token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)

    return token.SignedString([]byte(os.Getenv("API_SECRET")))
}

func extractTokenString(c *gin.Context) string {
    bearToken := c.Request.Header.Get("Authorization")
    strArr := strings.Split(bearToken, " ")
    if len(strArr) == 2 {
        return strArr[1]
    }

    return ""
}

func parseToken(tokenString string) (*jwt.Token, error) {
    token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
        if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
            return nil, fmt.Errorf("There was an error")
        }
        return []byte(os.Getenv("API_SECRET")), nil
    })

    if err != nil {
        return nil, err
    }

    return token, nil
}

func TokenValid(c *gin.Context) error {
    tokenString := extractTokenString(c)

    token, err := parseToken(tokenString)

    if err != nil {
        return err
    }

    return nil
}

func ExtractTokenId(c *gin.Context) (uint, error) {
    tokenString := extractTokenString(c)

    token, err := parseToken(tokenString)

    if err != nil {
        return 0, err
    }

    claims, ok := token.Claims.(jwt.MapClaims)

    if ok && token.Valid {
        userId, ok := claims["user_id"].(float64)

        if !ok {
            return 0, nil
        }

        return uint(userId), nil
    }

    return 0, nil
}

.env にコードに登場した環境変数を 2 つ追加します。

$ cat <<EOF >>.env
TOKEN_HOUR_LIFESPAN=1
API_SECRET=jwt-gin-dev
EOF

main.go を立ち上げ直します。

# 立ち上げの前に、使用するパッケージが増えたため追加処理を行っておきます
$ go mod tidy

# 起動
$ go run main.go

curl でいくつかリクエストを投げて動作を確認してみます。

  1. ユーザー名だけ正しい

    $ curl -X POST http://localhost:8080/api/login --data '{"username":"taro", "password":"passwd"}'
    {"error":"record not found"}%
  2. パスワードだけ正しい

    $ curl -X POST http://localhost:8080/api/login --data '{"username":"genzouw", "password":"pass"}'
    {"error":"crypto/bcrypt: hashedPassword is not the hash of the given password"}%
  3. ユーザー名、パスワードが正しい

    $ curl -X POST http://localhost:8080/api/login --data '{"username":"genzouw", "password":"passwd"}'
    {"token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdXRob3JpemVkIjp0cnVlLCJleHAiOjE2ODAwODgwNTIsInVzZXJfaWQiOjd9.lvqdlWCVxiCp0aPvQRnKUg0aKZFNbCfwR88r8v2vrec"}%

ログインに成功したときにトークンが生成されるようになりました。

認証用ミドルウェア ( Filter ) 作成

Gin にはミドルウェアと呼ばれる機能があります。
Controller にリクエストが到達する前に処理を行うことができます。

middlewares.go ファイルを新規に作成します。

$ mkdir -p ./middlewares/

$ vi ./middlewares/middlewares.go

middlewares.go

package middlewares

import (
    "jwt-gin/utils/token"
    "net/http"

    "github.com/gin-gonic/gin"
)

func JwtAuthMiddleware() gin.HandlerFunc {
    return func(c *gin.Context) {
        err := token.TokenValid(c)

        if err != nil {
            c.JSON(http.StatusUnauthorized, gin.H{"error": err.Error()})
            c.Abort()
            return
        }

        c.Next()
    }
}

main.go に新しいエンドポイント /api/admin/user の追加とこれに対するミドルウェアの設定を行います。

main.go

package main

import (
    "jwt-gin/controllers"
    "jwt-gin/middlewares"
    "jwt-gin/models"

    "github.com/gin-gonic/gin"
)

func main() {
    models.ConnectDataBase()

    router := gin.Default()

    public := router.Group("/api")

    public.POST("/register", controllers.Register)
    public.POST("/login", controllers.Login)

    protected := router.Group("/api/admin")
    protected.Use(middlewares.JwtAuthMiddleware())
    protected.GET("/user", controllers.CurrentUser)

    router.Run(":8080")
}

auth.go に Controller となる CurrentUser 関数を追加します。

package controllers

import (
    "net/http"

    "jwt-gin/models"
    "jwt-gin/utils/token"

    "github.com/gin-gonic/gin"
)

type RegisterInput struct {
    Username string `json:"username" binding:"required"`
    Password string `json:"password" binding:"required"`
}

func Register(c *gin.Context) {
    var input RegisterInput

    if err := c.ShouldBindJSON(&input); err != nil {
        c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
        return
    }

    user := models.User{Username: input.Username, Password: input.Password}

    user, err := user.Save()

    if err != nil {
        c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
        return
    }

    c.JSON(http.StatusOK, gin.H{
        "data": user.PrepareOutput(),
    })
}

type LoginInput struct {
    Username string `json:"username" binding:"required"`
    Password string `json:"password" binding:"required"`
}

func Login(c *gin.Context) {
    var input LoginInput

    if err := c.ShouldBindJSON(&input); err != nil {
        c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
        return
    }

    token, err := models.GenerateToken(input.Username, input.Password)

    if err != nil {
        c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
        return
    }

    c.JSON(http.StatusOK, gin.H{
        "token": token,
    })
}

func CurrentUser(c *gin.Context) {
    userId, err := token.ExtractTokenId(c)

    if err != nil {
        c.JSON(http.StatusUnauthorized, gin.H{"error": err.Error()})
        return
    }

    var user models.User

    err = models.DB.First(&user, userId).Error

    if err != nil {
        c.JSON(http.StatusUnauthorized, gin.H{"error": err.Error()})
        return
    }

    c.JSON(http.StatusOK, gin.H{
        "data": user.PrepareOutput(),
    })
}

main.go を立ち上げ直します。

# 起動
$ go run main.go

動作確認してみます。

まずは /api/login エンドポイントに対してログインを行い、トークンを取得します。

$ curl -X POST http://localhost:8080/api/login --data '{"username":"genzouw", "password":"passwd"}'
{"token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdXRob3JpemVkIjp0cnVlLCJleHAiOjE2ODAwOTA2MzQsInVzZXJfaWQiOjd9.y1k0b3YqWjohqvuVyrQ7Bev5VQKiGMDkWP-K21YTstc"}

この操作でトークンが取得できたので、改めて jq コマンドを使ってパースしてみます。

$ curl --silent -X POST http://localhost:8080/api/login --data '{"username":"genzouw", "password":"passwd"}' | jq -rc .token
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdXRob3JpemVkIjp0cnVlLCJleHAiOjE2ODAwOTA3NDksInVzZXJfaWQiOjd9.H6I_KkmFvSGZRhPfxZa3FpKsvS-UPEH8jdYtB2zUA2g

結果シェル変数に保存しておきます。

$ TOKEN=$(curl --silent -X POST http://localhost:8080/api/login --data '{"username":"genzouw", "password":"passwd"}' | jq -rc .token)

$ echo $TOKEN
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdXRob3JpemVkIjp0cnVlLCJleHAiOjE2ODAwOTA3ODUsInVzZXJfaWQiOjd9.0ptmj2BgGH54wIhJh9wvMQsMfyq5gWBwVRDWW8BN-WY

次に、/api/admin/user エンドポイントにリクエストしてみます。

まずはトークンを使わない場合。

$ curl --silent -X GET http://localhost:8080/api/admin/user
{"error":"token contains an invalid number of segments"}%

エラーとなりました。

今度は正しいトークンをヘッダーにセットしてリクエストしてみます。

$ curl --silent -X GET http://localhost:8080/api/admin/user -H "Authorization: Bearer ${TOKEN}"
{"data":{"ID":7,"CreatedAt":"2023-03-29T17:28:07+09:00","UpdatedAt":"2023-03-29T17:28:07+09:00","DeletedAt":null,"username":"genzouw","password":""}}

ひとこと

Controller をどういう単位でファイルに分割するとチーム開発しやすいのでしょうか?


参考ページ

Posted by genzouw