ginでAPIサーバを作る
ディレクトリをほって初期化する。 モジュール名のところはリポジトリ名にするんだよ、とする情報が多くて、正しいのだけれど、公開しないものであれば別にそのフォルダ名とかでも特に支障はない(たぶん)。
mkdir XXXX cd XXXX go mod init モジュール名
DBを立ち上げるdocker-compose.ymlをかく
services: db: image: postgres:latest container_name: postgresql ports: - 5432:5432 environment: POSTGRES_USER: postgres POSTGRES_PASSWORD: postgres volumes: - ./pgdata:/var/lib/postgresql/data - ./pginit:/docker-entrypoint-initdb.d
初期化して初期データをつっこむSQLを書く
DROP TABLE IF EXISTS Users; CREATE TABLE Users ( user_id SERIAL, username VARCHAR(128), account VARCHAR(64), password VARCHAR(64) ); INSERT INTO Users(username, account, password) VALUES('あ', 'user01', 'user01'); INSERT INTO Users(username, account, password) VALUES('い', 'user02', 'user02'); INSERT INTO Users(username, account, password) VALUES('う', 'user03', 'user03'); INSERT INTO Users(username, account, password) VALUES('え', 'user04', 'user04'); INSERT INTO Users(username, account, password) VALUES('お', 'user05', 'user05');
勢いで書く。
package main import ( "database/sql" "fmt" "log" "strconv" "strings" "time" _ "github.com/lib/pq" "github.com/gin-contrib/cors" "github.com/gin-gonic/gin" "github.com/golang-jwt/jwt" ) const SECRETKEY = "secretkey" func main() { // ログにファイル名と行番号を出す設定 log.SetFlags(log.Lshortfile) app := gin.Default() // グループの作成 apiRouter := app.Group("/api/v0") // CORS設定、どこからでもアクセスOKにしてみた apiRouter.Use(cors.New(cors.Config{ AllowOrigins: []string{"*"}, AllowMethods: []string{"POST", "GET", "OPTIONS"}, AllowHeaders: []string{"Authorization", "Content-Type"}})) // このグループ共通のミドル、ログインしていたらユーザ情報を取得する apiRouter.Use(middleware) // ログイン apiRouter.POST("/login", Login) // ログインしているユーザの情報の取得 apiRouter.GET("/me", Me) app.Run() } // // Authorizationヘッダに正しいトークンが乗っていたらコンテキストにそのユーザの情報を乗せる // func middleware(c *gin.Context) { // Authorizationヘッダからトークンを取り出す authorizationHeader := c.Request.Header.Get("Authorization") if authorizationHeader != "" { ary := strings.Split(authorizationHeader, " ") if len(ary) == 2 { if ary[0] == "Bearer" { // ここまででトークンと思われるものを取り出せたので、解析する // ここのロジックは公式サイト参照 // https://pkg.go.dev/github.com/golang-jwt/jwt#example-Parse-Hmac token, err := jwt.Parse(ary[1], func(token *jwt.Token) (interface{}, error) { if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { return nil, fmt.Errorf("Unexpected signing method: %v", token.Header["alg"]) } return []byte(SECRETKEY), nil }) if err == nil { // 解析に成功したら、ユーザIDを取り出す if claims, ok := token.Claims.(jwt.MapClaims); ok && token.Valid { userIDStr := claims["sub"].(string) userID, err := strconv.Atoi(userIDStr) if err == nil { // ユーザIDを使って新しいトークンを取り出す(有効期限切れ対策) newToken, err := generateToken(userID) if err == nil { // ユーザの情報を取り出す conn, err := connectDB() if err == nil { defer conn.Close() var username string err := conn.QueryRow(` SELECT username FROM Users WHERE user_id = $1`, userID).Scan(&username) if err == nil { // 新しいトークンとユーザの情報をコンテキストに保存 c.Set("token", newToken) c.Set("username", username) } } } } } } } } } c.Next() } // // 投入されたログイン情報を格納するための構造体 // type LoginInfo struct { Account string `json:"account" binding:"required"` Password string `json:"password" binding:"required"` } // // ログイン処理 // 正しいアカウントとパスワードが渡されたらトークンを生成して返す // func Login(c *gin.Context) { // 投げつけられた情報を取り出す var loginInfo LoginInfo err := c.ShouldBindJSON(&loginInfo) if err != nil { log.Println(err) c.JSON(400, gin.H{"message": "bad request"}) return } // DBに接続 conn, err := connectDB() if err != nil { log.Println(err) c.JSON(500, gin.H{"message": "server internal error"}) return } defer conn.Close() // 渡された情報に該当するユーザを探す。 // 本当だったらDBに生パスワードは保存しないだろうからこの前にハッシュ化などが必要 var userID int rows, err := conn.Query(` SELECT user_id FROM Users WHERE account = $1 AND password = $2`, loginInfo.Account, loginInfo.Password) for rows.Next() { err := rows.Scan(&userID) if err != nil { log.Println(err) c.JSON(500, gin.H{"message": "server internal error"}) return } } // トークン生成 token, err := generateToken(userID) if err != nil { c.JSON(500, gin.H{"message": "server error"}) return } c.JSON(200, gin.H{"token": token}) } // // ログインしているユーザの自分自身の情報取得 // func Me(c *gin.Context) { // 自作ミドルウェアがコンテキストに保存した情報を取り出す username, exist := c.Get("username") if !exist { c.JSON(403, gin.H{"message": "need to login"}) return } token, exist := c.Get("token") if !exist { c.JSON(403, gin.H{"message": "need to login"}) return } c.JSON(200, gin.H{"username": username, "token": token}) } // // DBへの接続 // func connectDB() (*sql.DB, error) { db, err := sql.Open("postgres", ` host=localhost port=5432 dbname=postgres user=postgres password=postgres sslmode=disable`) if err != nil { log.Println(err) return nil, err } return db, nil } // // トークン生成 // func generateToken(userID int) (string, error) { now := time.Now() token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{ "sub": fmt.Sprintf("%d", userID), "iat": now.Unix(), "exp": now.Add(60 * time.Minute).Unix(), // 一時間で無効になる }) return token.SignedString([]byte(SECRETKEY)) }