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

Go APIでのAuth0 JWT認証ハンズオン

こちらは GAOGAO Advent Calendar 2021 ことしもGAOGAOまつりです の2日目の記事です。昨日の記事は ますみんさん の リバースプロキシをDocker Compose環境で実現する でした。 こんにちは、GAOGAO の案件に携わらさせていただいている こうりん と申します。 よろしくお願いいたします。 この記事では、GoのAPIでAuth0のJWT認証をする方法をハンズオン形式で説明します。今回実装するコードは以下のリポジトリに公開します。 実行環境 maxOS Big Sur version 11.6 Node.js: v16.13.0 npm: 8.1.0 go: 1.16.3 Auth0とは Auth0 は IDaaS (Identity as a Service) に分類されるもので、クラウドでユーザー認証基盤を提供しているサービスです。ユーザーID/パスワード認証だけでなく Google や Facebook などのソーシャルアカウントを使ったサインイン/サインアップやMFA (Multi-Factor Authentication)の設定も可能です。Auth0では多くのプログラミング言語向けSDKを提供しており、Webだけでなくスマートフォンアプリでも使用できます。 Auth0を使ったユーザー認証とAPIアクセスのフロー WebアプリでAuth0を使う場合、以下のような関係になります。 ブラウザ側でAuth0にアクセスしログインします。この時、結果としてブラウザへJWT(JSON Web Token)が渡されます。そして、このJWTをブラウザがAPIを呼ぶ時にトークンとして渡します。APIではJWTを検証を行い正しいユーザーからのアクセスであることを確認するとともに、リクエストがあったユーザーを識別します。 JWTとは RFC7519 で標準化されたトークンの仕様です。JWTの中身は以下のように、ヘッダ・ペイロード・署名の3つがピリオド区切りの構成になっています。 <ヘッダ> . <ペイロード> . <署名> ヘッダ はJWT作成・検証で必要なトークンタイプや署名アルゴリズムの種類などを格納します。この値をBase64エンコードしてJWTの一番目に配置します。 { "typ": "JWT", "alg": "RS256" } ペイロード は具体的な認証情報を格納します。ヘッダと同様Base64エンコードしたものがJWTに配置されます。 Auth0では以下のような情報が含まれています。 iss: トークンの発行者 azp: クライアントID (Auth0で使用しているサービスの識別子) sub: Auth0でのユーザーの識別子 aud: トークンを使用する対象 (APIなど) gty: トークンを取得した方法 iat: トークンの発行日時 exp: トークンの失効日時 scope: 利用できるユーザーのリソースのリスト permissions: ユーザーが属するロールが持つ許可のリスト { "iss": "https://example.us.auth0.com", "azp": "xxxxxxxxx", "sub": "auth0|....", "aud": [ "http://localhost:8080", "https://example.us.auth0.com" ], "gty": "password", "iat": 1637954372, "exp": 1638040772, "scope": "openid profile email", "permissions": [ "sys:admin" ] } 署名 は<Base64エンコードしたヘッダ>,<Base64エンコードしたペイロード>を署名アルゴリズムで署名したものです。 JWTを検証する際は、JWTに含まれるヘッダとペイロードを元にダイジェストを作成します。そして、署名を復号した結果に含まれるダイジェストと一致するか確認して、JWTが改ざんされていないかチェックします。 Auth0の初期設定 まず、Auth0を使用するための初期設定を行います。Auth0.com へアクセスし、サインインをすると以下のようなコンソール画面に移動します。 左のメニューから Applications > APIs を選択します。 APIs で右上の Create API を押すと、次のようなモーダルが表示されます。 Name は好きな名前を設定します。Identifier はAPIの識別子として使われますが、通常はAPIのURLを指定します。 Signing Algorithm では RS256, HS256 のどちらかを選択できます。RS256 が公開鍵/秘密鍵を使った署名方法、HS256 が共通鍵を使った署名方法です。特に問題が無ければ、RS256で良いと思います。 Create を押して作成が完了すると、以下のようなAPI設定画面へ遷移します。 次にフロントエンドからAuth0へアクセスするためにApplicationを作成します。同様に左メニューから Applications > Applications を選択します。 APIに対するApplicationが作られていますが、Create Application を押して新しく作成します。Name には任意の名前を入力してください。Application Type はフロントエンドにReactを使うので、Single Page Web Application を選択します。 Applicationの作成が完了すると設定画面に遷移します。Settingsタブをクリックします。Domain と ClientID は後ほどフロントエンドで使用するのでメモしておきます。 Settingsで Allowed Callback URLs, Allowed Logout URLs, Allowed Web Origins, Allowed Origins (CORS) にフロントエンドのURLを設定します。とりあえず、ローカルで動かすのでそれぞれ http://localhost:3000 を設定します。 Save Changesを押して作成すると、同様に設定画面へ遷移します。ひとまずAuth0のコンソールで必要な作業は以上です。 APIのコード実装 プロジェクト構築 GoでAPIのコードを書いていきます。API用のプロジェクトのディレクトリを作り、go mod initでGoのプロジェクトをセットアップします。(GOPATHの外でgo mod initをする場合は、プロジェクトのルートパスを指定する必要があります) $ mkdir server # server以下に移動する $ cd server # Goプロジェクトの初期化 $ go mod init # GOPATHの外で実行する場合 (例) $ go mod init github.com/Kourin1996/go-auth0-example/server プロジェクトの構成は以下のようにします。handlers以下にAPIのハンドラ、middlewares以下にAuth0のJWT検証部分をそれぞれ実装します。 server ├── handlers │ └── v1 │ ├── v1.go (/v1へのリクエストのハンドラ定義) │ └── users │ └── me (/v1/users/meへのリクエストのハンドラ定義) │ └── me.go ├── middlewares │ └── auth0 (Auth0向けの処理を定義) ├── main.go (エントリポイント) └── go.mod まず、APIの動作確認のためにシンプルなエンドポイントを実装します。server/main.go と server/handlers/v1/v1.go を作成し、それぞれ以下を記述します。 // server/main.go // APIのエントリポイント package main import ( "fmt" "log" "net/http" v1 "github.com/Kourin1996/go-auth0-example/server/handlers/v1" ) const ( port = 8000 ) func main() { mux := http.NewServeMux() // /v1へのリクエストが来た場合のハンドラを追加 mux.HandleFunc("/v1", v1.HandleIndex) addr := fmt.Sprintf(":%d", port) // localhost:8000 でサーバーを立ち上げる log.Printf("Listening on %s", addr) if err := http.ListenAndServe(addr, mux); err != nil { log.Fatal(err) } } // server/handlers/v1/v1.go // /v1向けのハンドラ package v1 import ( "net/http" ) func HandleIndex(w http.ResponseWriter, r *http.Request) { // "Hello!" とだけ返します w.Write([]byte("Hello!")) } ファイルを保存しgo run main.goでサーバーを起動します。 $ go run main.go 2021/12/02 12:47:27 Listening on :8000 別のシェルからcurlを使ってAPIにリクエストを投げると、正しくメッセージが返ってくることを確認できます。 $ curl http://localhost:8000/v1 Hello! JWT認証用の処理を追加 APIでAuth0向けのJWT認証を行うには大きく分けて3つのステップがあります 署名を検証するための公開鍵をAPI起動時に取得する auth0/go-jwt-middlewareを初期化する 認証が必要なリクエストが来た場合にJWTの検証をする それぞれ順番に実装していきます 公開鍵の取得 まず公開鍵を取得します。JWTは秘密鍵を使って署名を生成しているので、検証には対になる公開鍵が必要となります。これはhttps://<DOMAIN>/.well-known/jwks.jsonから取得できます。ドメインはAuth0のApplicationの設定画面などに記載されています。 ドメインを元に公開鍵を取得する関数を実装します。server/middlewares/auth0/key.goを作成し以下を記述します。取得するデータは、JWKS (JSON Web Key Set)というJSONに公開鍵などが入ってる形式なので、取得したJSONをGoの構造体にマッピングします。 // server/middlewares/auth0/key.go package auth0 import ( "encoding/json" "fmt" "net/http" "net/url" ) // JKWS向けの構造体定義 type JSONWebKeys struct { Kty string `json:"kty"` Kid string `json:"kid"` Use string `json:"use"` N string `json:"n"` E string `json:"e"` X5c []string `json:"x5c"` } type JWKS struct { Keys []JSONWebKeys `json:"keys"` } func FetchJWKS(auth0Domain string) (*JWKS, error) { // ドメインを指定して公開鍵が入ったJWKSを取得する resp, err := http.Get(fmt.Sprintf("https://%s/.well-known/jwks.json", auth0Domain)) if err != nil { return nil, err } defer resp.Body.Close() // 取得したJSONデータを構造体にマッピングする jwks := &JWKS{} err = json.NewDecoder(resp.Body).Decode(jwks) return jwks, err } auth0/go-jwt-middlewareの初期化 auth0/go-jwt-middlewareはAuth0向けのJWTを検証するためのパッケージです。まず、パッケージをgo getで追加します。 $ go get github.com/auth0/go-jwt-middleware@v1.0.1 次にserver/middlewares/auth0/auth0.goを作成し、以下を記述します。 // server/middlewares/auth0/auth0.go package auth0 import ( "errors" "fmt" "net/http" jwtmiddleware "github.com/auth0/go-jwt-middleware" "github.com/form3tech-oss/jwt-go" ) func NewMiddleware(domain, clientID string, jwks *JWKS) (*jwtmiddleware.JWTMiddleware, error) { return jwtmiddleware.New(jwtmiddleware.Options{ ValidationKeyGetter: newValidationKeyGetter(domain, clientID, jwks), // JWTで使われている署名アルゴリズムを指定する SigningMethod: jwt.SigningMethodRS256, ErrorHandler: func(w http.ResponseWriter, r *http.Request, err string) {}, }), nil } func newValidationKeyGetter(domain, clientID string, jwks *JWKS) func(*jwt.Token) (interface{}, error) { return func(token *jwt.Token) (interface{}, error) { claims, ok := token.Claims.(jwt.MapClaims) if !ok { return token, errors.New("invalid claims type") } // azpフィールドを見て、適切なClientIDのJWTかチェックする azp, ok := claims["azp"].(string) if !ok { return nil, errors.New("authorized parties are required") } if azp != clientID { return nil, errors.New("invalid authorized parties") } // issフィールドを見て、正しいトークン発行者か確認する iss := fmt.Sprintf("https://%s/", domain) ok = token.Claims.(jwt.MapClaims).VerifyIssuer(iss, true) if !checkIss { return nil, errors.New("invalid issuer") } // JWTの検証に必要な鍵を生成する cert, err := getPemCert(jwks, token) if err != nil { return nil, err } return jwt.ParseRSAPublicKeyFromPEM([]byte(cert)) } } // JWKSからJWTで使われているキーをPEM形式で返す func getPemCert(jwks *JWKS, token *jwt.Token) (string, error) { cert := "" for k := range jwks.Keys { if token.Header["kid"] == jwks.Keys[k].Kid { cert = "-----BEGIN CERTIFICATE-----\n" + jwks.Keys[k].X5c[0] + "\n-----END CERTIFICATE-----" } } if cert == "" { return "", errors.New("unable to find appropriate key") } return cert, nil } Middlewareを作成 任意のハンドラでJWT認証を行えるように、Middlewareの形で関数を作成します。server/middlewares/auth0/middleware.goを作成し以下を記述します。 // server/middlewares/auth0/middleware.go package auth0 import ( "context" "net/http" jwtmiddleware "github.com/auth0/go-jwt-middleware" "github.com/form3tech-oss/jwt-go" ) // jwtmiddleware.JWTMiddlewareをContextに格納するためのキー type JWTMiddlewareKey struct{} // JWTをContextに保存するためのキー type JWTKey struct{} // jwtmiddleware.JWTMiddlewareをリクエストのContextに格納するためのMiddleware func WithJWTMiddleware(m *jwtmiddleware.JWTMiddleware) func(http.Handler) http.Handler { return func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { // リクエストのContextにJWTMiddlewareを格納する ctx := context.WithValue(r.Context(), JWTMiddlewareKey{}, m) // 新しいContextを入れて次の処理に渡す next.ServeHTTP(w, r.WithContext(ctx)) }) } } // JWT検証を行うためのmiddleware func UseJWT(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { // ContextからJWTMiddlewareを取得 jwtm := r.Context().Value(JWTMiddlewareKey{}).(*jwtmiddleware.JWTMiddleware) // リクエスト中のJWTを検証 if err := jwtm.CheckJWT(w, r); err != nil { http.Error(w, err.Error(), http.StatusUnauthorized) return } // JWT検証後に、Contextのjwtm.Options.UserPropertyからパース済みのトークンを取得する if val := r.Context().Value(jwtm.Options.UserProperty); val != nil { token, ok := val.(*jwt.Token) if ok { // リクエストのContextにJWTを保存する ctx := context.WithValue(r.Context(), JWTKey{}, token) // 新しいContextを入れて次の処理に渡す next.ServeHTTP(w, r.WithContext(ctx)) return } } next.ServeHTTP(w, r) }) } // Contextに埋め込まれたJWTを取得する func GetJWT(ctx context.Context) *jwt.Token { rawJWT, ok := ctx.Value(JWTKey{}).(*jwt.Token) if !ok { return nil } return rawJWT } Middlewareとはリクエストのハンドラを呼ぶ前後に任意の処理を差し込む機能です。ここでは2つのMiddlewareを実装しています。 2番のMiddlewareがJWTMiddlewareを参照できるようにContext中に埋め込む リクエストに対応したハンドラの処理が呼ばれる前に、リクエスト中に含まれるJWTを検証する 1番のWithJWTMiddlewareでは、JWTMiddlewareを受け取り新しいMiddlewareを作成します。このMiddlewareでは、リクエストのContextにJWTMiddlewareを埋め込み、そのContextとともに次の処理へ渡します。 2番のUseJWTでは、Contextの中にあるJWTMiddlewareを使ってリクエスト中のJWTを検証します。もし検証に失敗したらエラーを書き込んで返します。エラーの場合はnext.ServeHTTP(w, r)が呼ばれないため、後続の処理は呼ばれずハンドラの処理は実行されません。JWTの検証に成功した場合は、パースしたJWTをContextに埋め込んで次の処理へ渡します。 GetJWTはContextにあるJWTを取得するヘルパー関数です。他のMiddlewareやハンドラでJWTを参照したい場合に使用します。 JWT認証が必要なエンドポイントの実装 JWT認証に必要なコードは実装できたので、実際にJWT認証が必要なエンドポイントを実装していきます。/v1/users/meにアクセスした時にJWT認証をするようにします。 まず、server/handlers/v1/users/me/me.goを作成します。ここでは/v1/users/meへのリクエストに対するハンドラを実装し、JWTに応じてユーザーの情報を返します。 このハンドラは事前にJWT認証をしているため、Contextに検証済みのJWTが含まれています。JWTのClaimsにはペイロード情報が入っており、たとえばsubはAuth0におけるユーザーの識別子が入っています。このsubに紐づくユーザーを返します。ここではインメモリのデータを参照していますが、実際のAPIではDBにクエリを投げてsubが一致するユーザーレコードを取得するなどします。 そして、取得したユーザーをレスポンスとして返します。 // server/handlers/v1/users/me/me.go package me import ( "encoding/json" "fmt" "net/http" "github.com/Kourin1996/go-auth0-example/server/middlewares/auth0" "github.com/form3tech-oss/jwt-go" ) type User struct { Name string `json:"name"` Age int `json:"age"` } var ( subToUsers = map[string]User{ "auth0|61a8178b21127500715968e2": { Name: "kourin", Age: 15, }, } ) // subを元にUserを取得する関数 // 実際のAPIではDBなどに照会し、subに紐づくUserを取得するなどをする func getUser(sub string) *User { user, ok := subToUsers[sub] if !ok { return nil } return &user } // /v1/users/me のハンドラ func HandleIndex(w http.ResponseWriter, r *http.Request) { token := auth0.GetJWT(r.Context()) fmt.Printf("jwt %+v\n", token) // token.Claimsをjwt.MapClaimsへ変換 claims := token.Claims.(jwt.MapClaims) // claimsの中にペイロードの情報が入っている sub := claims["sub"].(string) // userを取得する user := getUser(sub) if user == nil { http.Error(w, "user not found", http.StatusNotFound) return } // レスポンスを返す res, err := json.Marshal(user) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } w.Write(res) } 初期化とMiddlewareの設定 // server/main.go package main import ( "fmt" "log" "net/http" v1 "github.com/Kourin1996/go-auth0-example/server/handlers/v1" "github.com/Kourin1996/go-auth0-example/server/handlers/v1/users/me" "github.com/Kourin1996/go-auth0-example/server/middlewares/auth0" "github.com/rs/cors" ) const ( port = 8000 domain = "<AUTH0_DOMAIN>" clientID = "<AUTH0_CLIENT_ID>" ) func main() { // 公開鍵を取得する jwks, err := auth0.FetchJWKS(domain) if err != nil { log.Fatal(err) } // domain, clientID, 公開鍵を元にJWTMiddlewareを作成する jwtMiddleware, err := auth0.NewMiddleware(domain, clientID, jwks) if err != nil { log.Fatal(err) } mux := http.NewServeMux() // /v1へのリクエストの場合のハンドラを登録 mux.HandleFunc("/v1", v1.HandleIndex) // /v1/users/meへのリクエストの場合のハンドラを登録 // auth0.UseJWTでラップし、ハンドラを呼ぶ前にJWT認証を行う mux.Handle("/v1/users/me", auth0.UseJWT(http.HandlerFunc(me.HandleIndex))) // フロントエンドからアクセスできるようにCORSの設定をする c := cors.New(cors.Options{ AllowedOrigins: []string{"http://localhost:3000"}, AllowedHeaders: []string{"Authorization", "Content-Type"}, AllowCredentials: true, Debug: true, }) // リクエスト前にJWTMiddlewareをContextに埋め込むためのMiddlewareを追加 wrappedMux := auth0.WithJWTMiddleware(jwtMiddleware)(mux) wrappedMux = c.Handler(wrappedMux) addr := fmt.Sprintf(":%d", port) log.Printf("Listening on %s", addr) if err := http.ListenAndServe(addr, wrappedMux); err != nil { log.Fatal(err) } } これで一通りのAPIのコードは実装が完了しました。go run main.goでAPIを起動して、サーバーが立ち上がったら完了です。 $ go run main.go 2021/12/02 14:50:50 Listening on :8000 フロントエンドサンプルアプリ作成 ログイン部分実装 最後にフロントエンドからアクセスするための簡単なReactアプリを作成します。今回はCreate React Appを使い、Reactアプリの雛形を作成します。プロジェクト構築が完了した後、Auth0認証に必要な @auth0/auth0-react を追加でインストールします。 # client以下にReactのプロジェクトを作成する $ npx create-react-app client # プロジェクトの中に移動する $ cd client # Auth0認証に必要なパッケージをインストールする $ npm install --save @auth0/auth0-react まず、src/index.jsを開き、Auth0ProviderをAppコンポーネントの上にラップします。この時, domain, audience, clientId, redirectUriを渡します。audienceにはAPIのIdentifierを指定します。 // src/index.js ... import App from "./App"; import { Auth0Provider } from "@auth0/auth0-react"; ReactDOM.render( <React.StrictMode> <Auth0Provider domain="<DOMAIN>" audience="<API_IDENTIFIER>" clientId="<CLIENT_ID>" redirectUri="http://localhost:3000" > <App /> </Auth0Provider> </React.StrictMode>, document.getElementById("root") ); ... 次に, src/App.jsを開き、ここにログインボタンを設置します。ボタンが押されたら、useAuth0フックの中にあるloginWithRedirect関数を呼びます。 // src/App.js // 分かりやすい用にオリジナルのコードは全て消してあります function App() { const { loginWithRedirect, isAuthenticated } = useAuth0(); const onClickLogin = () => { loginWithRedirect(); }; return ( <div className="App"> <div style={{ display: "flex", justifyContent: "center", marginTop: "64px" }} > <button onClick={onClickLogin} disabled={isAuthenticated}> {isAuthenticated ? "ログイン済み" : "ログイン"} </button> </div> </div> ); } export default App; npm run startで開発サーバーを立ち上げて、ブラウザからアクセスします。 $ npm run start ブラウザからアクセスしログインボタンを押すと、Auth0のログインページにリダイレクトをします。 サインインまたはサインアップをすると、元のページに戻ります。そして、useAuth0()フックに含まれる isAuthenticated が true になりログイン済みであることを取得できます。 API呼び出し部分実装 API呼び出し部分を実装します。まず、JWTを取得する必要があります。useAuth0()フックの中にgetAccessTokenSilentlyという関数があり、これを呼ぶとJWTを取得できます。そして、APIを呼ぶ際にJWTをヘッダーのAuthorizationにセットします。 // client/src/App.js import { useState, useEffect } from "react"; import "./App.css"; import { useAuth0 } from "@auth0/auth0-react"; const API_URL = "http://localhost:8000"; // Auth0のJWTを取得するフック const useAuth0Token = () => { const { isAuthenticated, user, getAccessTokenSilently } = useAuth0(); const [accessToken, setAccessToken] = useState(null); useEffect(() => { const fetchToken = async () => { // JWTを取得して状態に保存する setAccessToken(await getAccessTokenSilently()); }; // ログイン済みの場合のみJWTを取得する if (isAuthenticated) { fetchToken(); } }, [isAuthenticated, user?.sub]); return accessToken; }; function App() { const { loginWithRedirect, isAuthenticated } = useAuth0(); const token = useAuth0Token(); // ユーザー情報を保持する状態 const [me, setMe] = useState(null); // APIコールのエラーを保持する状態 const [error, setError] = useState(null); const onClickLogin = () => { loginWithRedirect(); }; const onClickCall = async () => { try { // APIを呼ぶ const res = await fetch(`${API_URL}/v1/users/me`, { method: "GET", mode: "cors", headers: { // JWTをAuthorizationヘッダにセットする Authorization: `Bearer ${token}`, "Content-Type": "application/json", }, }); if (!res.ok) { throw new Error(res.statusText); } const me = await res.json(); setError(null); setMe(me); } catch (error) { console.log("error", error); setError(error); } }; return ( <div className="App"> <div style={{ display: "flex", justifyContent: "center", marginTop: "64px" }} > <button onClick={onClickLogin} disabled={isAuthenticated}> {isAuthenticated ? "ログイン済み" : "ログイン"} </button> </div> <div style={{ display: "flex", justifyContent: "center", marginTop: "64px" }} > <div style={{ display: "flex", alignItems: "center", marginRight: "32px", }} > <button onClick={onClickCall}>ユーザー情報を取得</button> </div> <div style={{ width: "300px" }}> <p>ユーザー: {JSON.stringify(me)}</p> <p>エラー: {error ? error.toString() : ""}</p> </div> </div> </div> ); } export default App; ログインしていない時の呼び出し ログイン済みの場合の呼び出し まとめ この記事ではSPA+Go APIの構成で、ユーザーのJWTをAPIから認証する方法について紹介しました。auth0/go-jwt-middlewareのレポジトリを覗いたらver2.0 betaが上がってたので、時間があれば試してみます。 参考記事 Auth0の機能を調べてみた Auth0 Go SDK Quickstarts RFC7519 Introduction to JSON Web Tokens Making and Using HTTP Middleware auth0/go-jwt-middleware
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

2021年,Go言語でお世話になったライブラリ/検討したけど導入しなかったライブラリ

この記事はWanoグループ Advent Calendar 2021 2日目の記事になります 今年、Go言語で新たにお世話になったライブラリや使用頻度が上がったライブラリを振り返ります。 けっこう今年は個人的にはGo周りは小ネタが多かったですね。。 2021年、お世話になったライブラリ github.com/deepmap/oapi-codegen Open API 定義からGo言語のサーバーとかRequest/Response/構造体やモデル定義とかをGenerateするやつです。 今年は一部プロジェクトで新たにOpenAPIでのスキーマ駆動開発をとりいれましたので、その際に導入しました。 選定の理由は Open Api 3.0- に対応していた中で当時の言及数が多かったからです。 基本的にはモデル生成のみ使っています。 oapi-codegen --package=api_v2_schema -generate types api/v2/api-v2.yml > api/v2/schema/openapi_schema.go 特に大きな不満はないのですが、けっこう愚直な構造なので、OpenAPIで宣言したネストしたモデルがGoのinline struct になったりして気持ち悪かったり、ハマりどころがないわけでもない...という感じです。 CIでの自動テストで毎回モデルを go generate し、git diff があればエラーにしてテストを落とす、という感じでなるべく実装漏れを防いでいます。 github.com/guregu/dynamo AWS DynamoDBを操作するためのSDKのラッパーです。 冪等性チェックツールやイベントドリブンに向けたイベントストアをdynamoDBで作る機会があり、そこでお世話になりました。 Go言語でdynamoといえばこれ!という感じで使用している方も多いのではないでしょうか。 とにかくインターフェースがわかりやすいのでおすすめです。 シンプルなKVSとして使うだけならDynamoあまり知らなくてもテーブル定義すらこのツールだけで使えてしまうと思います。 github.com/360EntSecGroup-Skylar/excelize GoでExcel操作するためのライブラリです。 現行、excel系では一番Starが多いんじゃないかな?と思います。 Excelを自動処理しなければならない要件があり気が重かったのですが、これが仕事をとてもシンプルにしてくれました。 Goの埋め込みとの相性も良く、テンプレートとなるExcelファイル自体をgo:embed で埋め込んでおき、embed.FS で読み込み、データを書き込んでExport... みたいなユースケースで使用していました。 package excel_task import ( "embed" "golang.org/x/xerrors" "io/fs" ) //go:embed template.xlsx var static embed.FS func LoadTemplate() (fs.File, error) { f, err := static.Open("template.xlsx") if err != nil { return nil, xerrors.Errorf(`%v`, err) } return f, nil } ... // 読み込み f, err := LoadTemplate() if err != nil { return "", xerrors.Errorf(`%+v`, err) } excelFile, err := excelize.OpenReader(f) if err != nil { log.Error(err) return "", err } // 加工処理... セルの色付けや複数シート操作なども楽にできたのでおすすめです。 github.com/iancoleman/strcase 文字列のスネークケースやキャメルケースを相互変換するやつです。 すごい地味に見えますが、 go generate で構造体を出力するようなツールを作るときに地味に何度もお世話になりました。 mysqlのテーブル定義構造体を出力するようなライブラリなんかでもよく使われているようです。 github.com/gabriel-vasile/mimetype 元々のmimeパッケージと違い、実際にファイルのヘッダを見て出力してくれるライブラリです。 アップロードされてきたファイルのContent-TypeをDBに入れたり、AWS S3オブジェクトのContentTypeを任意で設定したいときに使いました。 github.com/Masterminds/sprig Go template の便利なFunctions集です。 環境変数の読み込みやbase64 のエンコード/デコード 関数なんかも同梱しています。 text/teemplate , html/template 両方で使えます。 どんな便利そうなものがあるかはこちらを参照 2021年、あんまり使わなくなった/検討したけど使用に至らなかったライブラリ github.com/rclone/rclone rcloneはマルチクラウド/ファイルサーバ対応のファイル転送ツールです。 cli自体は google drive の アップロード/ダウンロードにすごーくお世話になりました。 このツール自体がGo言語製だったので、Goのプログラムから直接ライブラリとして読みこもうと画策しましたが、設定がグルーバル変数だったりしてインスタンス化できず、あまりすっきりしなかった感じです。 (...結局はGoからexecでrcloneコマンド自体を利用することになりました) github.com/rakyll/statik 外部ファイルをGoのコードとしてgenerateしてくれるやつです。 言語仕様自体に go:embed が導入されてから新しく使うことはなくなりました。 一部のGoのバージョンが古い共通リポジトリで残っているかな?というくらい。 2022年 今年は小ネタが多かったですが、   来年はとにかくGoのジェネリクス導入に伴うライブラリの発展にすごく期待しています。 Generateとかなしでコレクション操作や地味すぎる型変換がシンプルにできるようになるといいですね...
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Vonage】最新CPaaSのユーザ認証APIをSDKを独自実装しながら理解する【Go】

この記事は最新CPaaSコミュニケーションAPIを比べる or 使ってみよう! 【PR】Vonage Japanの3日目の記事です。 この記事の内容 Communication Platform as a ServiceのVonageさんのVerify APIを触ってみて得た開発知見を共有します。Verify APIは簡単にSMSやTELを使ったユーザ認証を実現できるAPIです。 ただAPIを触るだけじゃつまらない! & 私はGoで使いたいんや!という個人的な思いから、公式さんから正式提供されていないGo版のSDKを作るところまでやりました。その実装についての紹介です。 作成したSDKは下記で公開しています。様々なAPIが提供されているのですが、まずはユーザ認証用のVerify APIの基本操作を行う部分だけ完成しています…! そしてこの記事を書いている最中に気が付きましたが、まだリリースしていないだけで、当然公式さんもGoのSDKを開発中でしたね!いいんです、フルスクラッチで実装することで私の理解が深まったことでしょう(白目 今後は下記のrepoにcontributeしてみたいな、と思います 以下では公式のSDKの存在を意識の外に置いて進めていきます Vonageの機能概要を見てみよう 何はともあれ、公式サイト・公式ドキュメントからスタートするべきですね。ということで早速お邪魔してみましょう。ディペロッパーセンターというページが用意されているので、そちらに進んでみます。ふむふむ、Video APIとそれ以外のVONAGE APIに大別されるようです。 今回は、私が個人的に運営しているサービスでも使えるかな?という期待を込めて、SMSへのPINコード送信とコード認証が行えるVerify APIを使ってみようと思います。(VONAGE APIの一部です) Verify APIの仕様を確認しよう 詳細なAPIドキュメントが用意されています。ここまではユーザ登録などしなくても確認することが可能でした。 APIエンドポイントに対してリクエストを飛ばしてあげるだけで、PINコード送信(Verify)、コード認証(Check)、その他オペレーション(ワークフローの中止など)を行えることが分かります。curlなどでも使えますが、複数の言語向けにSDKも提供されています。そちらを使うことでより気軽に活用することが可能に思えます。対応している言語はこの記事の公開時点で以下の通りですね。 さて、せっかくなので認証を試してみたいと思います。VONAGE APIディペロッパーセンターからサインアップしアカウントを作成すると、次のようなダッシュボードを使うことができるようになりました。リクエストに必要なAPI_KEYやAPI_SECRETはこのダッシュボードから取得可能ですし、GUIを使って簡単にコード送信⇒認証を試すこともできます。親切ですね。 ためしに自分の携帯電話にコードを送信し認証ワークフローを実行してみると…、おお、SMSが送られてきました! curlでもリクエストを送ってみて動作を確認できました。 認証ワークフローについては公式ドキュメントを読めば十分理解可能ではありますが、分かりやすい図をVonageの @ak7787 様が掲載されていたので引用させて頂きます。記事リンク 今回は上図で言うところのAppサーバーの処理を実装していく、ということですね。 余談ですが、PINコードを受け取ってしばらく放置しておいたら、突如アメリカから電話がかかってきました 後でドキュメントを確認して分かりましたが、いくつかのワークフローがあり、デフォルトのフローではSMS⇒電話⇒電話という形でリマインダーを送ってくれるんですね。びっくりしました笑 さて、一通りAPIの動作確認ができたので、いよいよ使いこなしに向けて活動してみましょう。 SDK開発スタート 上述したようにSDKが充実しているのですが、私はGoで書きたい!GoのSDKはリリースされていない!よし作ろう!となりました 基本的にやるべきことはAPIリファレンスを参照すれば分かりそうですが、実装に踏み込んで仕様を理解しておきたい気持ちもあるので、公開されている他言語のSDKも確認しておくことにします。私が経験があるのはPythonなのでそちらのSDKを参照するのが近道ですが、いかんせんソースコード中で型が明示されていないので解読が辛いですね…。ということで雰囲気は分かるJavaも並行して参照してみましょう。 ソースコードを確認しての所感 JavaとPythonで圧倒的にコード量が違うのは何なんだろうとなりました(小並感) Java Python 一つのメソッドに対しOptionalなパラメータが結構な数存在するため、それに対応するための処置を多く含んでいそうです。 設計要件・設計指針 まずは機能ミニマムで入っていきたいので、VerifyAPIの最小実装を確認してみます。こちらにサンプルがありましたので参照します。 上記の例を見つつ、各メソッドの実装を確認すると最小機能は次のようです POSTリクエストを送る リクエストボディはrequiredなAPI_KEY, API_SECRET、PINを送る相手のPHONE_NUMBER、SMSに記載するBRAND_NAMEをjsonで作る 電話番号はE.164形式にするのを忘れずに(例:日本国内の番号090123456だったら、国コード81をつけて8190123456) リクエストヘッダにBASIC認証をつける(ここはAPIリファレンスからは読み取れず。SDKの実装から判断した) verify, check, cancelの3通りのメソッドを持っておけば実用には耐えそう 次に実装に向けて設計指針を決めておきます。 パラメータの与え方が複数パターンあるメソッドは、拡張性を考えてfunctional option patternで実装する。 functional option pattern はこちらを参照 …ひとまずこれだけでしょうかね 以上を加味して、目指すふるまいは次のようになります。(PINコードを送信するverifyメソッドのみ抜粋) import "github.com/tenkoh/vonage-go-sdk" func main(){ client, _ := vonage.NewClient( vonage.ApiKey("YOUR_API_KEY"), vonage.ApiSecret("YOUR_API_SECRET"), ) resp, _ := client.GenerateVerifyClient().Verify( vonage.VerifyNumber("PHONE_NUMBER"), vonage.VerifyBrand("YOUR_BRAND_NAME"), ) } 実装自体は上記ふるまいに合致するようにガリガリ書くだけなので、特に難しいことはありません。(API叩くだけですしね) 自分で作ったSDKで行うリクエストは格別ですね! PINコードの送信⇨受信したコードを使った認証処理までを行うサンプルアプリケーションも自作repoに加えてありますので、そちらも含めて参照いただけると使用イメージが掴みやすいと思います。 おわりに SDKを自作するつもりで各種ドキュメント・ソースコードを参照したので、なかなかにVONAGE APIのことを理解することができました。いわんやリリース済みのSDKを活用すれば、とんでもなく簡単にSMSを使ったユーザ認証を実現することができて良いですね! 本記事を作成するにあたりVonage公式さんに相談したところ、追加のトライアルクレジットを頂くことができました。手厚いサポートを受けることもできるんだなぁと実感できて、とても良い開発体験を積むことができました。ありがとうございます。 さて、冒頭で宣言したように公式repoへcontributeすべく、まずは仕様理解に取り組みますか…!おあとがよろしいようで。 参照のまとめ 公式情報 その他
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

AxumでGoのチュートリアル「Writing Web Application」をやってみた

フューチャーアドベントカレンダーの22日目の記事です はじめに 私はプログラミング初心者ですが、ITエンジニアリングスキルを高めて手に職をつけたい!という気持ちがあります。 そこで、まずは社会で比較的需要の多い(のか?)Webバックエンド開発の基礎を習得したいと思いました。Webバックエンドフレームワークは星の数ほどあれど、それらに共通した要素は多いはずで、どれか一つにある程度習熟してしまえば、新たなフレームワークを学ぶのは容易になるはずです。 と考えると何でもいいような気がしたので、自分の好きな言語であるRustのWebフレームワークAxumを選びました。ただ、Axumはかなり新しいフレームワークで、チュートリアルなどが見当たらないので、Goのnet/httpのチュートリアルWriting Web ApplicationをAxumで実装してみました。実装は以下にあります。 Axumとは Axumは非常にスリムなフレームワークです。AxumはGitHubのREADMEに書かれているようにErgonomicsとModularityにフォーカスしています。 Modularity Axumは色々なクレートがレゴブロックのように組み合わされています。 例えば、非同期ランタイムはtokio、サーバーはhyperが使われています。 また、AxumはServiceという「Requestを受け取りResponseを返すもの」を抽象化したトレイト(Javaでいうインターフェイスのようなもの)を土台としています。Serviceはtowerで定義されています。 AxumはServiceのルーティングに特化しています。後述するハンドラはServiceに変換できます。 Ergonomics Ergonomicsは人間工学という意味だそうです。使いやすさを重視しているということでしょう。 以下のような特徴があるようです。 マクロフリーなルーティング Webフレームワークの中にはアノテーション(マクロ)でルーティングするWebフレームワークも多いですが、Axumは現在のところ、その方式は採用していません(Axum v0.4時点)。 マクロは非常に高い表現力ゆえに、何をしているのか分かりづらいという側面があるので、なるべく使用を避けようとしているのでしょう。 宣言的なリクエストとレスポンスのパース net/httpのハンドラは、要求されたインターフェイスを実装した構造体に対して、操作を施し、リクエストからデータを抽出したりレスポンスを返したりしたりする方式を採用していますが、Axumではハンドラはリクエストは0個以上のFromRequestを実装したもの(Extractorと呼ばれます)で、レスポンスはIntoResponseを実装したものです。 言葉だと分かりづらいので具体例で比較してみます。 net/httpのハンドラの定義の一例 func handler(w http.ResponseWriter, r *http.Request) { ... } Axumのハンドラの定義の一例 async fn hander(uri: Uri, method: Method, headers: HeaderMap, body: Bytes) -> (StatusCode, HeaderMap, &'static str) { ... } net/httpではリクエストとレスポンスを処理したければ脳死でhttp.ResoponseWriterと*http.Requestを使いますが、Axumの場合、リクエストやレスポンスにおいて具体的に欲しい物、返したい物を引数、返り値に書いていきます。 ハンドラの引数や戻り値で使うためにはFromRequestやIntoResponseが実装されていなければなりませんが、よく使われるものには、FromRequestやIntoResponseの実装が用意されています。 Gowiki Writing Web ApplicationはGowikiという超簡易的なWikiを作るチュートリアルです。 簡単のため、Wikiのページはファイルで管理されています。 APIとしては ページの参照 編集(新規作成) 保存機能 が提供されています。 同じ機能を実装したAxumとnet/httpのコードを見比べたときの所感 net/httpはGoが標準で提供しているパッケージで、機能は厳選されています。これを素で使うことは少ないでしょうが、渋川さんの記事によるとginもechoもnet/httpのラッパーみたいなので、実質かなり広く使われているフレームワークと言っていいでしょう。Axumはサービスのルーティングに特化したかなりスリムなフレームワークです。もしかしたら、net/httpと同じようにAxumをベースにしたさらにリッチなフレームワークが登場するかもしれません。 ルーティング まず、実装を見比べてみます。 go func main() { http.HandleFunc("/view/", makeHandler(viewHandler)) http.HandleFunc("/edit/", makeHandler(editHandler)) http.HandleFunc("/save/", makeHandler(saveHandler)) log.Fatal(http.ListenAndServe(":8080", nil)) } https://golang.org/doc/articles/wiki/ から引用 rust #[tokio::main] async fn main() { let app = Router::new() .route("/view/:title", get(view)) .route("/edit/:title", get(edit)) .route("/save/:title", post(save)) .layer(extractor_middleware::<ValidTitle>()); axum::Server::bind(&"127.0.0.1:8080".parse().unwrap()) .serve(app.into_make_service()) .await .unwrap(); } net/httpでまず気になったのが、ぱっと見ハンドラがルータに紐付けられているように見えないことです。 この記事が非常に参考になりましたが、DefaultServeMuxというグローバル変数が存在していてhandlerFuncが呼ばれるとDefaultServeMuxに紐付けられるそうです。 そして、http.ListenAndServeの第2引数にnilが渡されるとサーバーにDefaultServeMuxが渡されるという作りになっています。 知ってしまえばなんてことはないですが、私は最初にコードを見た時に少し戸惑いました。 Axumのコードはスタンダードな作りに見えます。AxumはhyperのServerを再エクスポートしていてaxum::Server::bind(&"127.0.0.1:3000".parse().unwrap()).serve(app.into_make_service()).~~というのは実際はhyperの関数を呼び出しています。 hyperのserve関数はサービスを引数に要求するのでapp(ルーター)のinto_make_service()関数を呼び出してサービスに変換しています。 ミドルウェア こちらも実装を示します。 go var validPath = regexp.MustCompile("^/(edit|save|view)/([a-zA-Z0-9]+)$") func makeHandler(fn func(http.ResponseWriter, *http.Request, string)) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { m := validPath.FindStringSubmatch(r.URL.Path) if m == nil { http.NotFound(w, r) return } fn(w, r, m[2]) } } https://golang.org/doc/articles/wiki/ から引用 rust pub struct ValidTitle; #[async_trait] impl<B> FromRequest<B> for ValidTitle where B: Send, { type Rejection = (StatusCode, HeaderMap, String); async fn from_request(req: &mut RequestParts<B>) -> Result<Self, Self::Rejection> { let re = Regex::new(r"^/[a-zA-Z0-9]+$").unwrap(); let path = req.uri().path(); let title = path .trim_start_matches('/') .trim_start_matches(|c| c != '/'); if !re.is_match(title) { return Err(( StatusCode::NOT_FOUND, HeaderMap::new(), "invalid Page Title".to_string(), )); } Ok(Self) } } /* let app = Router::new() .route("/view/:title", get(view)) .route("/edit/:title", get(edit)) .route("/save/:title", post(save)) .layer(extractor_middleware::<ValidTitle>()); // 1. ここで使用。 */ チュートリアルではURLのpathのバリデーションをするミドルウェアを実装する章がありました。 net/httpもAxumもハンドラに機能を付加するミドルウェアという仕組みを持っています。net/httpはミドルウェアはhandlerFuncを返す関数を引数にとり、handerFuncを返すという非常に分かりやすい作りになっています。 サンプルコードを見て気になったのは引数の関数のシグネチャに戻り値が書かれていないことです。これは推論されるということなのでしょうか。。 Axumはtowerのミドルウェアの仕組みをシームレスに使えるようになっています。towerにおけるミドルウェアとは生成時にServiceを受け取るServiceです。ハンドラはServiceに変換できます。なので、ハンドラをミドルウェアでレイヤリングすることができます。 「~はServiceです。」は「~はServiceトレイトを実装したなにかです。」という意味です。 もともと存在するミドルウェアを使うのは簡単なのですが、ミドルウェアを自作するためにはRustの非同期に関する知識とtowerのServiceに関する知識が必要になります。 例としてTimeoutミドルウェアの実装を示します。 Timeoutのミドルウェアの実装 use pin_project::pin_project; use std::time::Duration; use std::{ fmt, future::Future, pin::Pin, task::{Context, Poll}, }; use tokio::time::Sleep; use tower::Service; #[derive(Debug, Clone)] struct Timeout<S> { inner: S, timeout: Duration, } impl<S> Timeout<S> { fn new(inner: S, timeout: Duration) -> Self { Timeout { inner, timeout } } } impl<S, Request> Service<Request> for Timeout<S> where S: Service<Request>, S::Error: Into<BoxError>, { type Response = S::Response; type Error = BoxError; type Future = ResponseFuture<S::Future>; fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> { self.inner.poll_ready(cx).map_err(Into::into) } fn call(&mut self, request: Request) -> Self::Future { let response_future = self.inner.call(request); let sleep = tokio::time::sleep(self.timeout); ResponseFuture { response_future, sleep, } } } #[pin_project] struct ResponseFuture<F> { #[pin] response_future: F, #[pin] sleep: Sleep, } impl<F, Response, Error> Future for ResponseFuture<F> where F: Future<Output = Result<Response, Error>>, Error: Into<BoxError>, { type Output = Result<Response, BoxError>; fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> { let this = self.project(); match this.response_future.poll(cx) { Poll::Ready(result) => { let result = result.map_err(Into::into); return Poll::Ready(result); } Poll::Pending => {} } match this.sleep.poll(cx) { Poll::Ready(()) => { let error = Box::new(TimeoutError(())); return Poll::Ready(Err(error)); } Poll::Pending => {} } Poll::Pending } } #[derive(Debug, Default)] struct TimeoutError(()); impl fmt::Display for TimeoutError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.pad("request timed out") } } impl std::error::Error for TimeoutError {} type BoxError = Box<dyn std::error::Error + Send + Sync>; https://github.com/tower-rs/tower/blob/master/guides/building-a-middleware-from-scratch.md から引用 。。。? #[pin]、Future、Pollなど、Rustの非同期を勉強したことがないと、意味が分からないと思います。私はRustの非同期を少し勉強したことはありますが(全然理解できてない?)、それでもミドルウェアをすらすら書ける自信がありません? とはいえ、独自のミドルウェアを実装したいときに、必ずしもこのような実装をしなければいけないわけではなく、AxumにはExtractorをミドルウェアに変換するextractor_middlewareという関数が存在します。私の実装ではこれを用いました。常識が全く分かりませんが、汎用性を考えないミドルウェアはextractor_middlewareを使うのが定石なのかもしれません。 正常系以外のレスポンスの返し方 サーバーは常に正常のレスポンスを返すわけではなく、INTERNAL_SERVER_ERROR、BAD_REQUEST、UNAUTHORIZEDなどのエラー(400、500番台)やリダイレクト(300番台)を返すこともあります。 これの実現方法ですがnet/httpではhttp.Error(...)やhttp.Redirect(...)等を呼ぶだけでした。 Axumの場合はResultを使います。Resultは列挙型です。以下のような定義になっていて、OK<T>とErr<E>の直和です。Axumでは、正常ならOk<T>を返し、エラーやリダイレクトならErr<T>を返すように実装します。(と思っていますが、違うかもしれません。違かったらごめんなさい?‍♂️) Resultの定義 pub enum Result<T, E> { Ok(T), Err(E), } ただ、Axumでエラーとリダイレクトを実装したときに少し困ったことがありました。 Wikiのページを返すview関数では、ページが見つからなければリダイレクト、ページが見つかればそのページを返します。 Goは以下のような実装でした。 func viewHandler(w http.ResponseWriter, r *http.Request, title string) { p, err := loadPage(title) if err != nil { http.Redirect(w, r, "/edit/"+title, http.StatusFound) return } renderTemplate(w, "view", p) } https://golang.org/doc/articles/wiki/ から引用 チュートリアルなので、renderTemplate(内部でfunc (t *Template) Execute(wr io.Writer, data interface{}) errorを呼んでいる)のエラーハンドリングは省かれてましたが、renderTemplateはエラーを発生しうる箇所です。つまり、この関数は正常系、リダイレクト、サーバーエラーの3種類のレスポンスを返す可能性があります。 Rustでは、私は最初以下のように書きました。 async fn view(Path(title): Path<String>) -> Result<impl IntoResponse, impl IntoResponse> { let page = match load_page(&title) { Ok(page) => page, Err(_) => return Err(Redirect::found(format!("/edit/{}", title))), // 1. Err<Redirect>を返す。 }; let mut context = Context::new(); context.insert("title", &page.title); context.insert("body", &String::from_utf8(page.body).unwrap()); match TEMPLATES.render("view.html", &context) { Ok(html) => Ok(Html(html)), Err(err) => Err(handle_error(err)), // 2.Err<(StatusCode, HeaderMap, String)>を返す。 } } fn handle_error(err: impl std::error::Error) -> (StatusCode, HeaderMap, String) { ( StatusCode::INTERNAL_SERVER_ERROR, HeaderMap::new(), format!("Something went wrong: {}", err), ) } しかし、これはコンパイルエラーになります。なぜなら1と2で返す型が違うからです。従って、私は以下のように書いてエラーで返す型を一致させました。 async fn view(Path(title): Path<String>) -> Result<impl IntoResponse, impl IntoResponse> { let page = match load_page(&title) { Ok(page) => page, Err(_) => return Err(handle_redirect(&title)), // 1.Err<(StatusCode, HeaderMap, String)>を返す。 }; let mut context = Context::new(); context.insert("title", &page.title); context.insert("body", &String::from_utf8(page.body).unwrap()); match TEMPLATES.render("view.html", &context) { Ok(html) => Ok(Html(html)), Err(err) => Err(handle_error(err)), // 2.Err<(StatusCode, HeaderMap, String)>を返す。 } } fn handle_error(err: impl std::error::Error) -> (StatusCode, HeaderMap, String) { ( StatusCode::INTERNAL_SERVER_ERROR, HeaderMap::new(), format!("Something went wrong: {}", err), ) } fn handle_redirect(title: &str) -> (StatusCode, HeaderMap, String) { let mut headers = HeaderMap::new(); headers.insert( axum::http::header::LOCATION, HeaderValue::from_str(&format!("/edit/{}", title)).unwrap(), ); (StatusCode::TEMPORARY_REDIRECT, headers, "".to_string()) } しかし、リダイレクトを返す関数があるのに、手動でヘッダーにLocationをセットするというのも変な感じです。おそらくいいやり方はあるんでしょうが、私には分かりませんでした。 おわりに 本記事ではAxumでGoのチュートリアルWriting Web Applicationを実装してみました。私はGoもRustも素人で不正確な情報が多いと思うので事前に謝罪申し上げます? Axumはまだ情報が少ないため、「こういうときにどう書くの?」という疑問を解消しづらい面はありましたが、フレームワーク自体はとても使いやすかったです。機能は少ないですが、それは意図的なものです。Goでnet/httpをベースとしたフレームワークがたくさん作られているように、Axumをベースとしたフレームワークがこれから作られていったり、関連するクレートがさらに充実していけば、開発体験はより快適になると思います。 ただ、Rustは言語自体の習得難易度が高く、使用者も多いとは言えないため、例えばGoの代わりにRustでWebバックエンド開発をやろうとすると、おそらく開発者集めに苦労すると思います。なので、企業がWebバックエンド開発でそれでもRustを採用するという場面は限られていそうで、ニッチなユースケースで使われるに留まるんだろうなあという気はしています。私は特別な理由が無い限り、Rustでコーディングしたいので悲しいです? 明日は@RuyPKGさんです!
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む