20220113のGoに関する記事は4件です。

【Go】APIサーバの構築 ~ログイン処理と操作権限の付加編~

はじめに APIサーバ構築の続きです。 JWTトークンを使って、ログインなどの認証まわりの処理を実装してみました。 実装 ログイン処理と特定のリクエストへの操作権限の付与について実装します。 実装にあたって、属性情報(Claim)をJSONデータ構造で表現したトークンの仕様であるJWTを使用します。 JWTはヘッダー(Header).内容(Payload).署名(Signature)で構成されており、Claimは内容(Payload)に使われる情報の一部です。 ログイン処理 クライアント側でログインを行うために、routes.goに以下のルートハンドラーを追加します。 routes.go router.HandlerFunc(http.MethodPost, "/v1/signin", app.Signin) Signinメソッドを定義するファイルtokens.goを新しく作成します。 このメソッドでは以下のような流れで処理を行います。 ログイン時にユーザが入力したパスワードを確認 照合できたらJWTにClaimsを付加 ハッシュ関数と秘密鍵から署名を作成してレスポンスとして返却 tokens.go package main import ( "backend/models" "encoding/json" "errors" "fmt" "net/http" "time" "github.com/pascaldekloe/jwt" "golang.org/x/crypto/bcrypt" ) // ログインユーザ(モック) var validUser = models.User { ID: 10, Email: "me@here.com", Password: "$2a$12$TBZJBBs0TfWdXHeujpGBn.TTwJq5V7Ra4yu.w9VV/Xgp9R3XS2YCq", } type Credentials struct { Username string `json:"email"` Password string `json:"password"` } func (app *application) Signin(w http.ResponseWriter, r *http.Request) { var creds Credentials // リクエストのJSONを構造体credsに変換する 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トークンのclaimsを生成する var claims jwt.Claims claims.Subject = fmt.Sprint(validUser.ID) // JWTのタイトル claims.Issued = jwt.NewNumericTime(time.Now()) // JWTが発行された日時 claims.NotBefore = jwt.NewNumericTime(time.Now()) // JWTが有効になる日時 claims.Expires = jwt.NewNumericTime(time.Now().Add(24 * time.Hour)) // JWTが失効する日時 claims.Issuer = "mydomain.com" // JWTの発行者 claims.Audiences = []string{"mydomain.com"} // JWTの想定利用者 // ハッシュ関数(SHA-256)と秘密鍵から署名を作成する jwtBytes, err := claims.HMACSign(jwt.HS256, []byte(app.config.jwt.secret)) if err != nil { app.errorJSON(w, errors.New("error signing")) return } // 署名(JWTトークン)をレスポンスとして返す app.writeJSON(w, http.StatusOK, string(jwtBytes), "response") } 特定のリクエストへの操作権限の付与 データの更新や削除など、クライアント側ではログインユーザのみに権限を与えたい操作があります。 このとき、クライアント側から送られてきたJWTトークンを確認し、照合できたら次の処理(更新、削除)を行うような実装を行います。 トークンの確認と照合の処理のために、checkTokenというメソッドのミドルウェアを作成します。 Aliceというライブラリで、alice.New(app.checkToken)のようにミドルウェアのチェーンを作成します(今回は一つ)。 secure.ThenFunc(app.editMovie)でapp.checkTokenの後にapp.editMovieが実行されるようにし、app.wrap()でラップすることでミドルウェアハンドラcheckTokenと通常のアプリケーションのハンドラeditMovieのチェーンを構築します。 このようにして、checkTokenを通った場合のみ、editMovieが実行されるようになります。 routes.go func (app *application) wrap(next http.Handler) httprouter.Handle { return func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { ctx := context.WithValue(r.Context(), "params", ps) next.ServeHTTP(w, r.WithContext(ctx)) } } // ルートハンドラーのレシーバ func (app *application) routes() http.Handler { router := httprouter.New() // ミドルウェアチェーンをつくる(今回は一つだけ) secure := alice.New(app.checkToken) ... // checkTokenミドルウェアを通過したときのみリクエストを通す router.POST("/v1/admin/editmovie", app.wrap(secure.ThenFunc(app.editMovie))) router.GET("/v1/admin/deletemovie/:id", app.wrap(secure.ThenFunc(app.deleteMovie))) return app.enableCORS(router) } checkTokenの処理の中身は以下のようになります。 クライアント側のリクエストで"Authorization"ヘッダーに付加されたJWTトークンBearer [JWT]を取得・照合し、トークンに含まれるClaimsの中身なども照合した上でエラーがないことを確かめます。 middleware.go // JWTトークンが正しいかどうかを検証するミドルウェア func (app *application) checkToken(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { // キャッシュを行う際、データを一意に特定するためにURI以外に"Authorization"を利用する w.Header().Add("Vary", "Authorization") // Authorizationヘッダーの値(Bearer ~)を取得する authHeader := r.Header.Get("Authorization") if authHeader == "" { // could set an anonymous user } // ["Bearer", "~"]を返す headerParts := strings.Split(authHeader, " ") if len(headerParts) != 2 { app.errorJSON(w, errors.New("invalid auth header")) return } if headerParts[0] != "Bearer" { app.errorJSON(w, errors.New("unauthorized - no bearer")) return } // ~(JWTトークン)を取得する token := headerParts[1] // 取得したトークンが照合できたらclaimsを返す claims, err := jwt.HMACCheck([]byte(token), []byte(app.config.jwt.secret)) if err != nil { app.errorJSON(w, errors.New("unauthorized - failed hmac check"), http.StatusForbidden) return } // 期限内かどうかを確認する if !claims.Valid(time.Now()) { app.errorJSON(w, errors.New("unauthorized - token expired"), http.StatusForbidden) return } // 想定利用者を確認する if !claims.AcceptAudience("mydomain.com") { app.errorJSON(w, errors.New("unauthorized - invalid audience"), http.StatusForbidden) return } // tokenの発行者を確認する if claims.Issuer != "mydomain.com" { app.errorJSON(w, errors.New("unauthorized - invalid issuer"), http.StatusForbidden) return } // 認証したuserIDを返す userID, err := strconv.ParseInt(claims.Subject, 10, 64) if err != nil { app.errorJSON(w, errors.New("unauthorized"), http.StatusForbidden) return } log.Println("Valid user:", userID) // ここまでエラーにならなければOK next.ServeHTTP(w, r) }) } 参考資料
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Go】APIサーバを構築する③ ~ログイン処理と操作権限の付加~

はじめに APIサーバ構築の続きです。 JWTトークンを使って、ログインなどの認証まわりの処理を実装してみました。 実装 ログイン処理と特定のリクエストへの操作権限の付与について実装します。 実装にあたって、属性情報(Claim)をJSONデータ構造で表現したトークンの仕様であるJWTを使用します。 JWTはヘッダー(Header).内容(Payload).署名(Signature)で構成されており、Claimは内容(Payload)に使われる情報の一部です。 ログイン処理 クライアント側でログインを行うために、routes.goに以下のルートハンドラーを追加します。 routes.go router.HandlerFunc(http.MethodPost, "/v1/signin", app.Signin) Signinメソッドを定義するファイルtokens.goを新しく作成します。 このメソッドでは以下のような流れで処理を行います。 ログイン時にユーザが入力したパスワードを確認 照合できたらJWTにClaimsを付加 ハッシュ関数と秘密鍵から署名を作成してレスポンスとして返却 tokens.go package main import ( "backend/models" "encoding/json" "errors" "fmt" "net/http" "time" "github.com/pascaldekloe/jwt" "golang.org/x/crypto/bcrypt" ) // ログインユーザ(モック) var validUser = models.User { ID: 10, Email: "me@here.com", Password: "$2a$12$TBZJBBs0TfWdXHeujpGBn.TTwJq5V7Ra4yu.w9VV/Xgp9R3XS2YCq", } type Credentials struct { Username string `json:"email"` Password string `json:"password"` } func (app *application) Signin(w http.ResponseWriter, r *http.Request) { var creds Credentials // リクエストのJSONを構造体credsに変換する 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トークンのclaimsを生成する var claims jwt.Claims claims.Subject = fmt.Sprint(validUser.ID) // JWTのタイトル claims.Issued = jwt.NewNumericTime(time.Now()) // JWTが発行された日時 claims.NotBefore = jwt.NewNumericTime(time.Now()) // JWTが有効になる日時 claims.Expires = jwt.NewNumericTime(time.Now().Add(24 * time.Hour)) // JWTが失効する日時 claims.Issuer = "mydomain.com" // JWTの発行者 claims.Audiences = []string{"mydomain.com"} // JWTの想定利用者 // ハッシュ関数(SHA-256)と秘密鍵から署名を作成する jwtBytes, err := claims.HMACSign(jwt.HS256, []byte(app.config.jwt.secret)) if err != nil { app.errorJSON(w, errors.New("error signing")) return } // 署名(JWTトークン)をレスポンスとして返す app.writeJSON(w, http.StatusOK, string(jwtBytes), "response") } 特定のリクエストへの操作権限の付与 データの更新や削除など、クライアント側ではログインユーザのみに権限を与えたい操作があります。 このとき、クライアント側から送られてきたJWTトークンを確認し、照合できたら次の処理(更新、削除)を行うような実装を行います。 トークンの確認と照合の処理のために、checkTokenというメソッドのミドルウェアを作成します。 Aliceというライブラリで、alice.New(app.checkToken)のようにミドルウェアのチェーンを作成します(今回は一つ)。 secure.ThenFunc(app.editMovie)でapp.checkTokenの後にapp.editMovieが実行されるようにし、app.wrap()でラップすることでミドルウェアハンドラcheckTokenと通常のアプリケーションのハンドラeditMovieのチェーンを構築します。 このようにして、checkTokenを通った場合のみ、editMovieが実行されるようになります。 routes.go func (app *application) wrap(next http.Handler) httprouter.Handle { return func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { ctx := context.WithValue(r.Context(), "params", ps) next.ServeHTTP(w, r.WithContext(ctx)) } } // ルートハンドラーのレシーバ func (app *application) routes() http.Handler { router := httprouter.New() // ミドルウェアチェーンをつくる(今回は一つだけ) secure := alice.New(app.checkToken) ... // checkTokenミドルウェアを通過したときのみリクエストを通す router.POST("/v1/admin/editmovie", app.wrap(secure.ThenFunc(app.editMovie))) router.GET("/v1/admin/deletemovie/:id", app.wrap(secure.ThenFunc(app.deleteMovie))) return app.enableCORS(router) } checkTokenの処理の中身は以下のようになります。 クライアント側のリクエストで"Authorization"ヘッダーに付加されたJWTトークンBearer [JWT]を取得・照合し、トークンに含まれるClaimsの中身なども照合した上でエラーがないことを確かめます。 middleware.go // JWTトークンが正しいかどうかを検証するミドルウェア func (app *application) checkToken(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { // キャッシュを行う際、データを一意に特定するためにURI以外に"Authorization"を利用する w.Header().Add("Vary", "Authorization") // Authorizationヘッダーの値(Bearer ~)を取得する authHeader := r.Header.Get("Authorization") if authHeader == "" { // could set an anonymous user } // ["Bearer", "~"]を返す headerParts := strings.Split(authHeader, " ") if len(headerParts) != 2 { app.errorJSON(w, errors.New("invalid auth header")) return } if headerParts[0] != "Bearer" { app.errorJSON(w, errors.New("unauthorized - no bearer")) return } // ~(JWTトークン)を取得する token := headerParts[1] // 取得したトークンが照合できたらclaimsを返す claims, err := jwt.HMACCheck([]byte(token), []byte(app.config.jwt.secret)) if err != nil { app.errorJSON(w, errors.New("unauthorized - failed hmac check"), http.StatusForbidden) return } // 期限内かどうかを確認する if !claims.Valid(time.Now()) { app.errorJSON(w, errors.New("unauthorized - token expired"), http.StatusForbidden) return } // 想定利用者を確認する if !claims.AcceptAudience("mydomain.com") { app.errorJSON(w, errors.New("unauthorized - invalid audience"), http.StatusForbidden) return } // tokenの発行者を確認する if claims.Issuer != "mydomain.com" { app.errorJSON(w, errors.New("unauthorized - invalid issuer"), http.StatusForbidden) return } // 認証したuserIDを返す userID, err := strconv.ParseInt(claims.Subject, 10, 64) if err != nil { app.errorJSON(w, errors.New("unauthorized"), http.StatusForbidden) return } log.Println("Valid user:", userID) // ここまでエラーにならなければOK next.ServeHTTP(w, r) }) } 参考資料
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

VSCodeで複数のmain.goを開くとgoplsのwarningが出る

問題 1つのルートディレクトリ(プロジェクト)下に複数のgo module、つまりmain.goが存在すると、goplsがwarningを出してしまいます。 個人的に1ルートディレクトリ=複数のサンプルコードの構成を崩したくないので調べてみました。 解決法 以下の設定を追加するだけで無事解消しました? settings.json "gopls": { "experimentalWorkspaceModule": true } ※ 試験的なオプションなので動作しなくなる可能性があります 詳細 を読むとわかります。 他の解決方法として、workspaceに複数のルートディレクトリを紐付けるというのがあります。 https://qiita.com/y_tochukaso/items/da426190a4563c1dbebd ただ、main.goを追加する度にworkdspaceの設定が必要なので今回は避けました。 experimentalWorkspaceModuleが動作しなくなったら、頑張ってmoduleごとにworkspace folderとやらを作成する必要がありそうです。 参考 https://github.com/golang/tools/blob/master/gopls/doc/settings.md#experimentalworkspacemodule-bool https://stackoverflow.com/a/66014474 (検索用)warningの中身 gopls requires a module at the root of your workspace. You can work with multiple modules by opening each one as a workspace folder. Improvements to this workflow will be coming soon, and you can learn more here: https://github.com/golang/tools/blob/master/gopls/doc/workspace.md.
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Goのデバッガーdelveを使えるようにするまでの苦労・プロセス】

デバッガを扱う前にGoのmodule関連でつまづいた go get で dlvをinstallしたらビルド時にエラーが出た。 go: inconsistent vendoring in /go/src/app/xxxx: github.com/go-delve/delve@v1.8.0: is explicitly required in go.mod, but not marked as explicit in vendor/modules.txt golang.org/x/sys@v0.0.0-xxxxxxxx: is marked as explicit in vendor/modules.txt, but not explicitly required in go.mod To ignore the vendor directory, use -mod=readonly or -mod=mod. To sync the vendor directory, run: go mod vendor go: /go/src/app/xxxx でベンダリングに矛盾があります。github.com/go-delve/delve@v1.8.0: は go.mod で明示的に要求されていますが、 vendor/modules.txt では明示的にマークされていません。 golang.org/x/sys@v0.0.0-xxxxxx vendor/modules.txt では明示的にマークされていますが、go.mod では明示的に必要ではありません。 ベンダーディレクトリを無視するには、-mod=readonly または -mod=mod を使ってください。vendor ディレクトリを同期させるには、以下を実行します。 go mod vendor vendor ディレクトリとは? 対応する概念として、module cacheがある。 vendor が使われるかどうかは、go.mod ファイルのバージョン記述に依存し、Go 1.14 以降は vendor が優先して使用される。1.14 未満の場合は module cache が優先される。 つまり、エラー内容としては、go getによってdelve moduleが$GOPATH/binにinstallされ、go.modファイルにも明示的に示されているが、go.modファイルのversionは1.16となっているためデフォルトでvenderディレクトリ内のmoduleを参照しに行った結果、venderディレクトリには、「delve moduleはないよ」と言われてしまっている。 解決策としては、$GOPATH/binにinstallされてしまったdelve moduleをvenderディレクトリに移動させるようなことをすればいい。 go mod vendorは何をするか go mod vendor すると外部の依存モジュールを vendor ディレクトリにダウンロードする。 おそらく、$GOPATH/binをきれいにしてくれたりするわけではない。 go mod vendor を実行するとエラーはなくなった。 想定通り、$GOPATH/binにinstallしてしまったものは消えてはくれない。 ようやくエラーがなくなったので、デバッガの取り扱いに戻る。 golangアプリケーションはdocker上で動作しているため、リモートデバッグという方法になるらしい。(2021/11/27の記事) 仕組みとしては、コンテナで動くdelveに対して、DAPというデバッグ用のプロトコルで、エディターからのデバッグ命令を受けて実行する。といったアーキテクチャのようです。 vimではcliでのデバッグが前提とされていますが、プラグインを導入することで解決できるようなので、今回は対応しないことにします。 デバッガを動かせるコンテナを作る ゼロから上記の記事に従ってコンテナを構築してみる。 上記記事の go-remote-debug → delve_sample に変えて試しにコンテナを起動してみる。 すると以下のようなエラーや他にもいろいろエラーが出た。 Build Error: go build -o /var/folders/wh/xxxx/T/__debug_binxxxxxxx -gcflags all=-N -l . go: cannot find main module, but found .git/config in /Users/xxxxx to create a module there, run: cd ../.. && go mod init (exit status 1) 最終構成 最終的には、3つの手順で接続してデバッグまでできるようになった。 ①右のコマンドでコンテナを起動 DEBUG=dap DEBUG_PORT=9998 docker compose up -d ②以下の構成のlaunch.jsonでデバッグ実行を試みるとvsCodeの下がオレンジになる。  (しかしデバッガは動かず、リロードすると接続が切れるので③の手順を追加) { "version": "0.2.0", "configurations": [ { "name": "Debug API Container", "type": "go", "debugAdapter": "dlv-dap", "request": "launch", "port": 9998, "host": "localhost", "mode": "exec", "program": "/dev/delve_sample/app", "substitutePath": [ { "from": "${workspaceFolder}", "to": "/dev/delve_sample" } ] } ] } ③下記の構成で再度接続を試みる。 { "version": "0.2.0", "configurations": [ { "name": "Launch Package", "type": "go", "request": "launch", "mode": "auto", "program": "${fileDirname}" } ] } ③の手順を追加した理由 本来の想定では、手順②までで完了すると思っていたが、できたりできなかったりして、いろいろ触っているうちに③の手順を追加することで接続を再現できるようになった。 しかし、③の手順だけを②を飛ばして行ってもうまくいきませんでした。 どういった仕組みでこのようなことになっているかまではわかりませんが、もし誰かの参考になればと思います。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む