検証
完成形
まずは完成形です。
完成形
go
package main
import (
"github.com/labstack/echo-contrib/session"
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
"github.com/srinathgs/mysqlstore"
"github.com/traPtitech/naro-template-backend/handler"
"log"
"os"
"time"
"github.com/go-sql-driver/mysql"
"github.com/jmoiron/sqlx"
"github.com/joho/godotenv"
)
func main() {
// .envファイルから環境変数を読み込み
err := godotenv.Load(".env")
if err != nil {
log.Fatal(err)
}
// データーベースの設定
jst, err := time.LoadLocation("Asia/Tokyo")
if err != nil {
log.Fatal(err)
}
conf := mysql.Config{
User: os.Getenv("DB_USERNAME"),
Passwd: os.Getenv("DB_PASSWORD"),
Net: "tcp",
Addr: os.Getenv("DB_HOSTNAME") + ":" + os.Getenv("DB_PORT"),
DBName: os.Getenv("DB_DATABASE"),
ParseTime: true,
Collation: "utf8mb4_unicode_ci",
Loc: jst,
}
// データベースに接続
db, err := sqlx.Open("mysql", conf.FormatDSN())
if err != nil {
log.Fatal(err)
}
// usersテーブルが存在しなかったら、usersテーブルを作成する
_, err = db.Exec("CREATE TABLE IF NOT EXISTS users (Username VARCHAR(255) PRIMARY KEY, HashedPass VARCHAR(255))")
if err != nil {
log.Fatal(err)
}
// セッションの情報を記憶するための場所をデータベース上に設定
store, err := mysqlstore.NewMySQLStoreFromConnection(db.DB, "sessions", "/", 60*60*24*14, []byte("secret-token"))
if err != nil {
log.Fatal(err)
}
h := handler.NewHandler(db)
e := echo.New()
e.Use(middleware.Logger()) // ログを取るミドルウェアを追加
e.Use(session.Middleware(store)) // セッション管理のためのミドルウェアを追加
e.POST("/signup", h.SignUpHandler)
e.POST("/login", h.LoginHandler)
withAuth := e.Group("")
withAuth.Use(handler.UserAuthMiddleware)
withAuth.GET("/me", handler.GetMeHandler)
withAuth.GET("/cities/:cityName", h.GetCityInfoHandler)
withAuth.POST("/cities", h.PostCityHandler)
err = e.Start(":8080")
if err != nil {
log.Fatal(err)
}
}
go
package handler
import (
"database/sql"
"errors"
"github.com/jmoiron/sqlx"
"github.com/labstack/echo-contrib/session"
"github.com/labstack/echo/v4"
"golang.org/x/crypto/bcrypt"
"log"
"net/http"
)
type Handler struct {
db *sqlx.DB
}
func NewHandler(db *sqlx.DB) *Handler {
return &Handler{db: db}
}
type City struct {
ID int `json:"id,omitempty" db:"ID"`
Name sql.NullString `json:"name,omitempty" db:"Name"`
CountryCode sql.NullString `json:"countryCode,omitempty" db:"CountryCode"`
District sql.NullString `json:"district,omitempty" db:"District"`
Population sql.NullInt64 `json:"population,omitempty" db:"Population"`
}
type LoginRequestBody struct {
Username string `json:"username,omitempty" form:"username"`
Password string `json:"password,omitempty" form:"password"`
}
type User struct {
Username string `json:"username,omitempty" db:"Username"`
HashedPass string `json:"-" db:"HashedPass"`
}
type Me struct {
Username string `json:"username,omitempty" db:"username"`
}
func (h *Handler) SignUpHandler(c echo.Context) error {
// リクエストを受け取り、reqに格納する
req := LoginRequestBody{}
err := c.Bind(&req)
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, "bad request body")
}
// バリデーションする(PasswordかUsernameが空文字列の場合は400 BadRequestを返す)
if req.Password == "" || req.Username == "" {
return c.String(http.StatusBadRequest, "Username or Password is empty")
}
// 登録しようとしているユーザーが既にデータベース内に存在するかチェック
var count int
err = h.db.Get(&count, "SELECT COUNT(*) FROM users WHERE Username=?", req.Username)
if err != nil {
log.Println(err)
return c.NoContent(http.StatusInternalServerError)
}
// 存在したら409 Conflictを返す
if count > 0 {
return c.String(http.StatusConflict, "Username is already used")
}
// パスワードをハッシュ化する
hashedPass, err := bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost)
// ハッシュ化に失敗したら500 InternalServerErrorを返す
if err != nil {
log.Println(err)
return c.NoContent(http.StatusInternalServerError)
}
// ユーザーを登録する
_, err = h.db.Exec("INSERT INTO users (Username, HashedPass) VALUES (?, ?)", req.Username, hashedPass)
// 登録に失敗したら500 InternalServerErrorを返す
if err != nil {
log.Println(err)
return c.NoContent(http.StatusInternalServerError)
}
// 登録に成功したら201 Createdを返す
return c.NoContent(http.StatusCreated)
}
func (h *Handler) LoginHandler(c echo.Context) error {
// リクエストを受け取り、reqに格納する
var req LoginRequestBody
err := c.Bind(&req)
if err != nil {
return c.String(http.StatusBadRequest, "bad request body")
}
// バリデーションする(PasswordかUsernameが空文字列の場合は400 BadRequestを返す)
if req.Password == "" || req.Username == "" {
return c.String(http.StatusBadRequest, "Username or Password is empty")
}
// データベースからユーザーを取得する
user := User{}
err = h.db.Get(&user, "SELECT * FROM users WHERE username=?", req.Username)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return c.NoContent(http.StatusUnauthorized)
} else {
log.Println(err)
return c.NoContent(http.StatusInternalServerError)
}
}
// パスワードが一致しているかを確かめる
err = bcrypt.CompareHashAndPassword([]byte(user.HashedPass), []byte(req.Password))
if err != nil {
if errors.Is(err, bcrypt.ErrMismatchedHashAndPassword) {
return c.NoContent(http.StatusUnauthorized)
} else {
return c.NoContent(http.StatusInternalServerError)
}
}
// セッションストアに登録する
sess, err := session.Get("sessions", c)
if err != nil {
log.Println(err)
return c.String(http.StatusInternalServerError, "something wrong in getting session")
}
sess.Values["userName"] = req.Username
sess.Save(c.Request(), c.Response())
return c.NoContent(http.StatusOK)
}
func UserAuthMiddleware(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
sess, err := session.Get("sessions", c)
if err != nil {
log.Println(err)
return c.String(http.StatusInternalServerError, "something wrong in getting session")
}
if sess.Values["userName"] == nil {
return c.String(http.StatusUnauthorized, "please login")
}
c.Set("userName", sess.Values["userName"].(string))
return next(c)
}
}
func GetMeHandler(c echo.Context) error {
return c.JSON(http.StatusOK, Me{
Username: c.Get("userName").(string),
})
}
func (h *Handler) GetCityInfoHandler(c echo.Context) error {
cityName := c.Param("cityName")
var city City
err := h.db.Get(&city, "SELECT * FROM city WHERE Name=?", cityName)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return c.NoContent(http.StatusNotFound)
}
log.Printf("failed to get city data: %s\n", err)
return c.NoContent(http.StatusInternalServerError)
}
return c.JSON(http.StatusOK, city)
}
func (h *Handler) PostCityHandler(c echo.Context) error {
var city City
err := c.Bind(&city)
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, "bad request body")
}
result, err := h.db.Exec("INSERT INTO city (Name, CountryCode, District, Population) VALUES (?, ?, ?, ?)", city.Name, city.CountryCode, city.District, city.Population)
if err != nil {
log.Printf("failed to insert city data: %s\n", err)
return c.NoContent(http.StatusInternalServerError)
}
id, err := result.LastInsertId()
if err != nil {
log.Printf("failed to get last insert id: %s\n", err)
return c.NoContent(http.StatusInternalServerError)
}
city.ID = int(id)
return c.JSON(http.StatusCreated, city)
}
検証
自分の実装が正しく動くか検証しましょう。
WARNING
全て Postman での検証です。go run main.go
でサーバーを起動した状態で行ってください。
また、GET
とPOST
を間違えないようにして下さい。
localhost:8080/cities/Tokyoにアクセスすると、ログインしていないため401 Unauthorized
が返ってきます。そのため、情報を入手できません。
ユーザーを作成します。 上手く作成できれば Status 201 が返ってくるはずです。
(注意:POST
です)
そのままパスを変えてログインリクエストを送ります。
ログインに成功したら、レスポンスの方の Cookies を開いて value の中身をコピーします
リクエストの方の Headers で Cookie をセットします。
Key にCookie
を Value にsessions={コピーした値};
をセットします(既に自動で入っている場合もあります、その場合は追加しなくて大丈夫です)。
もう一度 localhost:8080/cities/Tokyo にアクセスすると正常に API が取れるようになりました。
(注意:GET
です)
ここで、作成されたユーザーがデータベースに保存されていることを確認してみましょう。 mysql > SELECT * FROM users;
ユーザー名とハッシュ化されたパスワードが確認できますね。
ちょっと分かりにくい表示ですが、セッションもしっかり確認できます。