こんにちは、KOUKIです。
GolangとReactでMovie Appの開発をしています。
今回は、JWTを使ってSign-in処理を実装します。
尚、Udemyの「Working with React and Go (Golang)」を参考にしているので、よかったら受講してみてください。
<目次>
前回
事前準備
フォルダ/ファイル
1 |
touch backend-app/cmd/api/tokens.go |
モジュール
1 2 3 |
cd backend-app/ go get github.com/pascaldekloe/jwt go get golang.org/x/crypto/bcrypt |
JWTについて
JWT(JSON Web Token)は、JSONデータで表現したトークンの仕様です。
1 2 3 4 5 6 7 8 |
// このデータが { "alg": "HS256", "typ": "JWT" } // こんな感じでトークンへ変換 eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9 |
データの暗号化に用いられるので、Sign-in認証など暗号系の処理を実装する時に大活躍します。
Sign-inの実装
JWTを使ったSign-inの処理を実装します。
シークレットキーの設定
認証時に使用するシークレットキーを設定します。ちなみにシークレットキーは、ここから取得しています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 |
// cmd/api/main.go package main import ( "backend/models" "context" "database/sql" "flag" "fmt" "log" "net/http" "os" "time" _ "github.com/lib/pq" ) const ( version = "1.0.0" dsnString = "postgres://postgres:secret@postgres/go_movies?sslmode=disable" ) type config struct { port int env string db struct { // 追加 dsn string } jwt struct { // 追加 secret string } } ... func main() { var cfg config ... // default flag設定 flag.StringVar(&cfg.jwt.secret, "jwt-secret", "2dce505d96a53c5768052ee90f3df2055657518dad489160df9913f66042e160", "secret") flag.Parse() ... } |
ルートの追加
Sign-inへのルートを追加します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
// cmd/api/routes.go package main import ( "net/http" "github.com/julienschmidt/httprouter" ) func (app *application) routes() http.Handler { ... router.HandlerFunc(http.MethodPost, "/v1/signin", app.Signin) return app.enableCORS(router) } |
モデルの追加
Userモデルを追加しましょう。
1 2 3 4 5 6 7 8 9 |
// models/models.go ... // User is the type for users type User struct { ID int Email string Password string } |
ハンドラーの追加
ルートに指定したSigninハンドラーを実装します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 |
// cmd/api/tokens.go package main import ( "backend/models" "encoding/json" "errors" "fmt" "net/http" "time" "github.com/pascaldekloe/jwt" "golang.org/x/crypto/bcrypt" ) // tmp user var validUser = models.User{ ID: 10, Email: "me@here.com", // https://play.golang.org/p/uKMMCzJWGsW Password: "$2a$12$p3wO8E4CQOrvqcYmFMf.LeyzgxrFcK76jo1DqzEtGK9OHwsjPL1xe", } // 認証情報 type Credentials struct { Username string `json:"email"` Password string `json:"password"` } func (app *application) Signin(w http.ResponseWriter, r *http.Request) { var creds Credentials err := json.NewDecoder(r.Body).Decode(&creds) if err != nil { app.errorJSON(w, errors.New("unauthorized")) return } hashedPassword := validUser.Password // パスワードが合っているかチェック err = bcrypt.CompareHashAndPassword([]byte(hashedPassword), []byte(creds.Password)) if err != nil { app.errorJSON(w, errors.New("unauthorized")) return } // JWT作成 var claims jwt.Claims // Tokenに含めたい情報を設定する claims.Subject = fmt.Sprint(validUser.ID) claims.Issued = jwt.NewNumericTime(time.Now()) claims.NotBefore = jwt.NewNumericTime(time.Now()) claims.Expires = jwt.NewNumericTime(time.Now().Add(24 * time.Hour)) claims.Issuer = "mydomain.com" claims.Audiences = []string{"mydomain.com"} jwtBytes, err := claims.HMACSign(jwt.HS256, []byte(app.config.jwt.secret)) if err != nil { app.errorJSON(w, errors.New("error signing")) return } app.writeJSON(w, http.StatusOK, jwtBytes, "response") } |
リクエストから認証データをパース -> パスワードチェック -> JWTトークンの作成 -> Signinの流れになっています。
検証
Sign-in認証処理ができたので、早速検証をしましょう。
ログイン成功
下記のパラメータで、リクエストを送ってください。
- URL: http://localhost:4000/v1/signin
- 形式: POST
1 2 3 4 |
{ "username": "me@here.com", "password": "password" } |

ログイン失敗
下記のパラメータで、リクエストを送ってください。
- URL: http://localhost:4000/v1/signin
- 形式: POST
1 2 3 4 |
{ "username": "me@here.com", "password": "invalidpassword" } |

OKですね。
次回
次回は、React側でログインコンポーネントの実装を行います。
記事まとめ
参考書籍
ソースコード
main.go
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 |
// cmd/api/main.go package main import ( "backend/models" "context" "database/sql" "flag" "fmt" "log" "net/http" "os" "time" _ "github.com/lib/pq" ) const ( version = "1.0.0" dsnString = "postgres://postgres:secret@postgres/go_movies?sslmode=disable" ) type config struct { port int env string db struct { // 追加 dsn string } jwt struct { secret string } } type AppStatus struct { Status string `json:"status"` Environment string `json:"environment"` Version string `json:"version"` } type application struct { config config logger *log.Logger models models.Models } func main() { var cfg config flag.IntVar(&cfg.port, "port", 4000, "Server port to listen on") flag.StringVar(&cfg.env, "env", "development", "Application environment (development|production)") flag.StringVar(&cfg.db.dsn, "dsn", dsnString, "Postgres connection string") flag.StringVar(&cfg.jwt.secret, "jwt-secret", "2dce505d96a53c5768052ee90f3df2055657518dad489160df9913f66042e160", "secret") flag.Parse() logger := log.New(os.Stdout, "", log.Ldate|log.Ltime) // db接続 db, err := openDB(cfg) if err != nil { logger.Fatal(err) } defer db.Close() app := &application{ config: cfg, logger: logger, models: models.NewModels(db), } srv := &http.Server{ Addr: fmt.Sprintf(":%d", cfg.port), Handler: app.routes(), IdleTimeout: time.Minute, ReadTimeout: 10 * time.Second, WriteTimeout: 30 * time.Second, } logger.Println("Starting server on port", cfg.port) err = srv.ListenAndServe() if err != nil { log.Println(err) } } func openDB(cfg config) (*sql.DB, error) { db, err := sql.Open("postgres", cfg.db.dsn) if err != nil { return nil, err } ctx, cancle := context.WithTimeout(context.Background(), 5*time.Second) defer cancle() err = db.PingContext(ctx) if err != nil { return nil, err } return db, nil } |
models.go
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 |
// models/models.go package models import ( "database/sql" "time" ) // Models is the wrapper for database type Models struct { DB DBModel } // NewModels returns models with db pool func NewModels(db *sql.DB) Models { return Models{ DB: DBModel{DB: db}, } } // Movie is the type for movies type Movie struct { ID int `json:"id"` Title string `json:"title"` Description string `json:"description"` Year int `json:"year"` ReleaseDate time.Time `json:"release_date"` Runtime int `json:"runtime"` Rating int `json:"rating"` MPAARating string `json:"mpaa_rating"` CreatedAt time.Time `json:"-"` UpdatedAt time.Time `json:"-"` MovieGenre map[int]string `json:"genres"` } // Genre is the type for genre type Genre struct { // ID int `json:"-"` ID int `json:"id"` GenreName string `json:"genre_name"` CreatedAt time.Time `json:"-"` UpdatedAt time.Time `json:"-"` } // MovieGenre is thee type for movie genre type MovieGenre struct { ID int `json:"-"` MovieID string `json:"-"` GenreID string `json:"-` Genre Genre `json:"genre"` CreatedAt time.Time `json:"-"` UpdateAt time.Time `json:"-"` } // User is the type for users type User struct { ID int Email string Password string } |
routes.go
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
// cmd/api/routes.go package main import ( "net/http" "github.com/julienschmidt/httprouter" ) func (app *application) routes() http.Handler { router := httprouter.New() router.HandlerFunc(http.MethodGet, "/status", app.statusHandler) router.HandlerFunc(http.MethodPost, "/v1/signin", app.Signin) router.HandlerFunc(http.MethodGet, "/v1/movie/:id", app.getOneMovie) router.HandlerFunc(http.MethodGet, "/v1/movies", app.getAllMovie) router.HandlerFunc(http.MethodGet, "/v1/movies/:genre_id", app.getAllMoviesByGenres) router.HandlerFunc(http.MethodGet, "/v1/genres", app.getAllGenres) router.HandlerFunc(http.MethodPost, "/v1/admin/editmovie", app.editMovie) router.HandlerFunc(http.MethodGet, "/v1/admin/deletemovie/:id", app.deleteMovie) return app.enableCORS(router) } |
tokens.go
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 |
// cmd/api/tokens.go package main import ( "backend/models" "encoding/json" "errors" "fmt" "net/http" "time" "github.com/pascaldekloe/jwt" "golang.org/x/crypto/bcrypt" ) // tmp user var validUser = models.User{ ID: 10, Email: "me@here.com", // https://play.golang.org/p/uKMMCzJWGsW Password: "$2a$12$p3wO8E4CQOrvqcYmFMf.LeyzgxrFcK76jo1DqzEtGK9OHwsjPL1xe", } // 認証情報 type Credentials struct { Username string `json:"email"` Password string `json:"password"` } func (app *application) Signin(w http.ResponseWriter, r *http.Request) { var creds Credentials err := json.NewDecoder(r.Body).Decode(&creds) if err != nil { app.errorJSON(w, errors.New("unauthorized")) return } hashedPassword := validUser.Password // パスワードが合っているかチェック err = bcrypt.CompareHashAndPassword([]byte(hashedPassword), []byte(creds.Password)) if err != nil { app.errorJSON(w, errors.New("unauthorized")) return } // JWT作成 var claims jwt.Claims // Tokenに含めたい情報を設定する claims.Subject = fmt.Sprint(validUser.ID) claims.Issued = jwt.NewNumericTime(time.Now()) claims.NotBefore = jwt.NewNumericTime(time.Now()) claims.Expires = jwt.NewNumericTime(time.Now().Add(24 * time.Hour)) claims.Issuer = "mydomain.com" claims.Audiences = []string{"mydomain.com"} jwtBytes, err := claims.HMACSign(jwt.HS256, []byte(app.config.jwt.secret)) if err != nil { app.errorJSON(w, errors.New("error signing")) return } app.writeJSON(w, http.StatusOK, jwtBytes, "response") } |
コメントを残す
コメントを投稿するにはログインしてください。