目次
GoをAPIサーバーとして、フロントのReactから連携
前回と前々回は、EchoのTemplates機能を使用しましたが、本当にやりたいことはGoはあくまでもAPIサーバーに徹して、フロント側のアプリケーションとの連携です。
そこで今回は、Go(Echo)はTemplatesを使用せず、フロント側とAPIのやり取りに徹します。
フロント側にReactでログインフォームを実装、JWT認証に挑戦します。
今回はフロント側WebアプリケーションとしてReactを使用しますが、Go側とAPIを介しての疎結合にすることで、Webアプリケーションだけでなくスマホアプリなど他の方法とも連携がしやすくなります。
この連載はあくまでGoを主体としているので、Reactについての説明は割愛します。
フロントとバックエンドの構成について
開発環境はAWS(EC2)を使用、一つのインスタンスで構築しているため、今回は下記のような構成になります。
フロント側
Reactで構築、ポート番号は3000
https://ec2-xx-xxx-xxx-xx.ap-northeast-1.compute.amazonaws.com:3000/
バックエンド側
Go(Echo)で構築、ポート番号は8080
https://ec2-xx-xxx-xxx-xx.ap-northeast-1.compute.amazonaws.com:8080/
CORS (Cross-Origin Resource Sharing) オリジン間リソース共有
フロント側とバックエンド側を別々に構築するため、CORSを使用します。
CORSとは、あるオリジンで動いている Web アプリケーションに対して、別のオリジンのサーバーへのアクセスをオリジン間 HTTP リクエストによって許可できる仕組みのことを言います。
クロスオリジンとクロスドメインの違いとしては、プロトコルとポート番号を含んでいるという点です。
今回のAWS(EC2)の一つのインスタンスで2つのポート番号で連携する場合も含みます。
つまり、上の例ではドメインが、
ec2-xx-xxx-xxx-xx.ap-northeast-1.compute.amazonaws.com
オリジンが(プロトコル=httpsと、ポート番号=3000を含む)
https://ec2-xx-xxx-xxx-xx.ap-northeast-1.compute.amazonaws.com:3000/
となります。
EchoでのCORSの設定をする
EchoでCORSを設定する場合は、ルーティングの箇所で下記の公式サイトのサンプルのように記述します。
環境によっては簡易な設定でも動作可能です。完全にドメイン違いなどの場合は、厳密な設定が必要です。
Configurationの書き方も公式サイトをご確認ください。
CORS Middleware
https://echo.labstack.com/middleware/cors
CORS Recipe
https://echo.labstack.com/cookbook/cors
// 簡易な設定(ローカル環境や、同じAWS EC2インスタンス内でポート番号違いなど)
e.Use(middleware.CORS())
// 厳密な設定
e.Use(middleware.CORSWithConfig(middleware.CORSConfig{
AllowOrigins: []string{"https://labstack.com", "https://labstack.net"},
AllowHeaders: []string{echo.HeaderOrigin, echo.HeaderContentType, echo.HeaderAccept},
}))
ディレクトリ構造(Go側の追加変更)
前回と前々回で作成したものをTemplates使用を取りやめてJSON連携に変更、CORS設定を追記したものに改修します。
下記はGo側になります。React側は別でディレクトリを作成して実装となります。
今回のように同じEC2インスタンス内に同居する場合、下記のようにGoとReactを配置しましたが、Reactはこのような配置でなくても問題ありません。Goとバッティングしないようにすることだけ注意したらOKです。
Amazon Linux2 に普通にGoをインストールした場合、
/home/ec2-user/go/
というディレクトリ構造での想定になります。
Templatesの使用をやめた関係でGoフォルダ内にHTMLテンプレートのディレクトリ(public/views/)がごっそりなくなっています。
controllerのフォルダ名とファイル名をapiに変更しました(これは気分的な理由です)。
あとDB設定をconf/config.goに記述するようにしました。
├── Go
│ └── src
│ └── sample
│ ├── api
│ │ └─ user.go
│ ├── conf
│ │ └─ config.go
│ ├── go.mod
│ ├── go.sum
│ ├── main.go
│ ├── model
│ │ ├── db.go
│ │ └── user.go
│ └── router
│ └── router.go
│
└── React
追加変更したソース
main.goは変更ありませんが、他のソースは変更が入っています。
api/user.go
前回のcontroller/user_controller.goに当たるソースです。
ディレクトリ名とファイル名を変えた以外に、HTTPを扱う標準ライブラリとしnet/httpから、より高速に動作するというvalyala/fasthttpに変更しています。
valyala/fasthttp
https://github.com/valyala/fasthttp
package api
import (
"time"
"sample/model"
"github.com/dgrijalva/jwt-go"
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
"github.com/valyala/fasthttp"
"golang.org/x/crypto/bcrypt"
)
// jwtCustomClaims are custom claims extending default ones.
type jwtCustomClaims struct {
UID uint `json:"uid"`
Name string `json:"name"`
jwt.StandardClaims
}
var signingKey = []byte("secret")
var Config = middleware.JWTConfig{
Claims: &jwtCustomClaims{},
SigningKey: signingKey,
}
type User struct {
ID uint `json:"id" form:"id" query:"id"`
Name string `json:"name" form:"name" query:"name"`
Password string `json:"password" form:"password" query:"password"`
}
func MethodOverride(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
if c.Request().Method == "POST" {
method := c.Request().PostFormValue("_method")
if method == "PUT" || method == "PATCH" || method == "DELETE" {
c.Request().Method = method
}
}
return next(c)
}
}
func Login(c echo.Context) error {
username := c.FormValue("name")
password := c.FormValue("password")
user := model.User{}
user.FirstByName(username)
// Throws unauthorized error
err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(password))
if err != nil {
return echo.ErrUnauthorized
}
// Set custom claims
claims := &jwtCustomClaims{
user.ID,
user.Name,
jwt.StandardClaims{
ExpiresAt: time.Now().Add(time.Hour * 72).Unix(),
},
}
// Create token with claims
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
// Generate encoded token and send it as response.
t, err := token.SignedString(signingKey)
if err != nil {
return err
}
return c.JSON(http.StatusOK, echo.Map{
"token": t,
})
}
func Mypage(c echo.Context) error {
return c.Render(http.StatusOK, "mypage.html", map[string]interface{}{
"title": "Mypage",
})
}
func SignUp(c echo.Context) error {
return c.Render(http.StatusOK, "signup.html", map[string]interface{}{
"title": "Login / Signup Form",
})
}
func CreateUser(c echo.Context) error {
name := c.FormValue("name")
p := c.FormValue("password")
hashed, _ := bcrypt.GenerateFromPassword([]byte(p), 12)
password := string(hashed)
user := model.User{
Name: name,
Password: password,
}
user.Create()
return c.Render(http.StatusOK, "signup.html", map[string]interface{}{
"title": "Your account was created successfully",
})
}
func Restricted(c echo.Context) error {
user := c.Get("user").(*jwt.Token)
claims := user.Claims.(*jwtCustomClaims)
name := claims.Name
return c.JSON(http.StatusOK, echo.Map{
"name": name,
})
}
conf/config.go
DB設定はこちらにまとめて記述するように変更しました。
package conf
const (
USER string = "root"
PASSWORD string = "password"
DB string = "db_name"
HOST string = "127.0.0.1"
PORT string = "3306"
)
model/db.go
DB設定をconf/config.goから読み込むように修正しました。
なお、model/user.goは、前回の記事から変更ありません。
package model
import (
"log"
"sample/conf"
"gorm.io/driver/mysql"
"gorm.io/gorm"
)
var DB *gorm.DB
var err error
func init() {
dsn := conf.USER + ":" + conf.PASSWORD + "@tcp(" + conf.HOST + ":" + conf.PORT + ")/" + conf.DB + "?charset=utf8mb4&parseTime=True&loc=Local"
DB, err = gorm.Open(mysql.Open(dsn), &gorm.Config{})
if err != nil {
log.Fatalln(dsn + "database can't connect")
}
DB.AutoMigrate(&User{})
}
router/router.go
Templates機能の使用をやめたことにより、大きく変更が入っています。
また今回のテーマであるCORSの記述を追記しています。
下記のサンプルでは、CORSの簡易な設定で記述していますが、GoとReactを別ドメインのサーバーに設置する等の場合は厳密な設定で記述してください。
package router
import (
"sample/api"
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
)
func Init() {
e := echo.New()
e.Use(middleware.CORS())
e.Pre(api.MethodOverride)
e.GET("/", api.SignUp)
e.POST("/user", api.CreateUser)
e.POST("/login", api.Login)
e.GET("/mypage", api.Mypage)
r := e.Group("/restricted")
r.Use(middleware.JWTWithConfig(api.Config))
r.GET("", api.Restricted)
e.Logger.Fatal(e.Start(":8080"))
}
※なお、毎回記載していますが、本当はユーザー情報の登録やログイン情報の参照に際して、入力内容のバリデーションやサニタイズなどのセキュリティ対策をすべきです。
また、JWT認証のトークンをLocal Storageに保存することに対して賛否両論です。JWT認証がどうとかではなく、どんなものに対してもLocal Storageを使用すべきではないという意見です。
この記事はあくまで実験用なので、このサンプルソースをそのままプロダクトに利用しないでください。
次回は、フロント側のReactについて記載する予定です。