【Golang】Goa v3でAPIキー認証(APIKyeAuth)を設定する
経緯
GoaのAPIキー認証(APIKeyAuth)の実装例はJWT認証などと比べると数が非常に少なく、さらにv2から仕様が大きく変わっておりv3での具体的な実装例を見つけられなかったため、今回実装してみた例を紹介します。
参考にしたサイト
①goadesign/goa/blob/v3/dsl/security.go
GoaのGithubリポジトリです。大まかな実装例が乗っており、参考になります。
②Goa v1 や Goa v2 から v3 にアップグレードする
v1やv2での実装例をv3に適用する際に参考にしました。
実装
Goa公式のCalculator Serviceのdesign.go
を題材にします。(別オリジンからのリクエストを想定しています。)
package design import ( . "goa.design/goa/v3/dsl" ) var _ = API("calc", func() { Title("Calculator Service") Description("Service for adding numbers, a Goa teaser") Server("calc", func() { Host("localhost", func() { URI("http://localhost:8000") }) }) }) var _ = Service("calc", func() { Description("The calc service performs operations on numbers.") cors.Origin("接続元のURL", func() { cors.Headers("Access-Control-Allow-Origin", "Authorization") cors.Methods("GET") cors.Credentials() }) Method("add", func() { Security(APIKeyAuth) Payload(func() { APIKey("api_key", "key", String, "API key used to perform authorization") Field(1, "a", Int, "Left operand") Field(2, "b", Int, "Right operand") Required("key", "a", "b") }) Result(Int) HTTP(func() { GET("/add/{a}/{b}") Header("key:Authorization") }) }) }) var APIKeyAuth = APIKeySecurity("api_key", func() { Description("Secures endpoint by requiring an API key.") })
順に説明していきます。
var APIKeyAuth = APIKeySecurity("api_key", func() { Description("Secures endpoint by requiring an API key.") })
まず、APIKeyAuthを使用するため、ファイル最下部で以下の変数設定を行っています。
var _ = Service("calc", func() { Description("The calc service performs operations on numbers.") cors.Origin("接続元のURL", func() { cors.Headers("Access-Control-Allow-Origin", "Authorization") cors.Methods("GET") cors.Credentials() })
次に、CORSの設定を行います。ここで、後ほど設定するAPIキーを格納するヘッダーもcors.Headersに追加しておく必要があります(ここではAuthorization
ヘッダー)。これを設定しないとCORSはクリアできてもリクエストヘッダーからAPIキーを取得できません。
Method("add", func() { Security(APIKeyAuth) Payload(func() { APIKey("api_key", "key", String, "API key used to perform authorization") Field(1, "a", Int, "Left operand") Field(2, "b", Int, "Right operand") Required("key", "a", "b") }) Result(Int)
次に、Method
へ記述していきます。まず、このメソッドでAPIキー認証を適用することを宣言するため、Security(APIKeyAuth)
を追加します。次にPayload
内でAPIKey
を記述し、APIキーの変数について定義します。(gRPCで利用する際は、APIKeyField
の設定が必要になるようです。詳しい仕様は以下を参照してください。)
またRequired
に、APIキー用の変数を追加します。
HTTP(func() { GET("/add/{a}/{b}") Header("key:Authorization") })
最後にHTTP
部で、Header
にてAPIキーを格納するヘッダーを設定します。括弧内は、”キー:ヘッダー”の組み合わせです。
以上が、APIキー認証を実装する場合のdesignとなります。
【Golang】個々のチャットルーム(ウェブページ)毎で独立してWebSocket接続を管理する方法例
経緯
複数のチャットルームが存在するチャットアプリでWebSocketを使用するため、それぞれのルーム(ページ)毎にWebSocketを管理する必要がありました。そこで、セッション管理の仕組みを応用して、各ルームのIDとルームのインスタンスとのマップとしてメモリ上で管理することにしました。
全コードは以下のリンクにアップしています。
https://github.com/taishi-enomoto/go-chat-app/blob/main/wsserver/wsserver.go
参考にしたサイト
① 6.2 Goはどのようにしてsessionを使用するか
(Build Web Application With Golang)
メモリ上でのセッション管理は、こちらのサイトの方法を参考にさせていただきました。
② [ golang ] WebSocketを使ったチャット機能を実装してみる。
(Wild Data Chase -データを巡る冒険-)
単一のルームにおけるWebSocketの実装は、こちらのサイトを参考にさせていただきました。
実装
チャットルームマネージャーの実装
まず、各ルームを保持する、チャットルームマネージャーを実装します。(リンク先①のセッションマネージャーと同じ仕組みです。)
WsChatroom
は、それぞれのチャットルームを表す構造体です。
type MANAGER struct { WsRooms map[string]*WsChatroom } var Manager *MANAGER func init() { Manager = NewManager() } func NewManager() *MANAGER { database := make(map[string]*WsChatroom) return &MANAGER{WsRooms: database} }
マネージャーは、各ルームのIDをキー、ルームインスタンスを値としたマップを保持します。
WebSocketHandlerの実装
次に、チャットルームへアクセスがあった際の処理を実装していきますが、ルームの存在状況により、①ルームが既に存在する場合と②部屋がまだ存在しない場合の2つの分岐を用意します。
func WebSocketHandler(ws *websocket.Conn) { defer ws.Close() var chatroomJson string if err := websocket.Message.Receive(ws, &chatroomJson); err == nil { var chatroom WsChat json.Unmarshal([]byte(chatroomJson), &chatroom) roomId := chatroom.Id //①ルームが既に存在する場合 if _, exist := Manager.WsRooms[roomId]; exist { WsClient := &WsClient{ Send: make(chan string), Room: Manager.WsRooms[roomId], } Manager.WsRooms[roomId].Join <- WsClient defer func() { Manager.WsRooms[roomId].Leave <- WsClient if len(Manager.WsRooms[roomId].clients) == 0 { delete(Manager.WsRooms, roomId) fmt.Println("WebSocket用ルーム削除") } }() go WsClient.Write(ws) WsClient.Read(ws) } else { //②部屋がまだ存在しない場合 WsChatroom := &WsChatroom{ forward: make(chan string), Join: make(chan *WsClient), Leave: make(chan *WsClient), clients: make(map[*WsClient]bool), } go WsChatroom.ChatroomRun() var chatroom WsChat json.Unmarshal([]byte(chatroomJson), &chatroom) WsChatroom.id = chatroom.Id Manager.WsRooms[WsChatroom.id] = WsChatroom WsClient := &WsClient{ Send: make(chan string), Room: WsChatroom, } WsChatroom.Join <- WsClient defer func() { WsChatroom.Leave <- WsClient Manager.WsRooms[WsChatroom.id].Leave <- WsClient if len(Manager.WsRooms[WsChatroom.id].clients) == 0 { delete(Manager.WsRooms, WsChatroom.id) fmt.Println("WebSocket用ルーム削除") } }() go WsClient.Write(ws) WsClient.Read(ws) } } }
var chatroomJson string if err := websocket.Message.Receive(ws, &chatroomJson); err == nil { var chatroom WsChat json.Unmarshal([]byte(chatroomJson), &chatroom) roomId := chatroom.Id
冒頭のコードについてだけ先に説明しておくと、初回接続確率時に送信されるデータにルームNoが含まれる仕組みにしており、ここで得られたルームNoとルームインスタンスを紐付けています。html側での詳しい処理については、ここでは割愛します。
①ルームが既に存在する場合
//①ルームが既に存在する場合 if _, exist := Manager.WsRooms[roomId]; exist { WsClient := &WsClient{ Send: make(chan string), Room: Manager.WsRooms[roomId], } Manager.WsRooms[roomId].Join <- WsClient defer func() { Manager.WsRooms[roomId].Leave <- WsClient if len(Manager.WsRooms[roomId].clients) == 0 { delete(Manager.WsRooms, roomId) fmt.Println("WebSocket用ルーム削除") } }() go WsClient.Write(ws) WsClient.Read(ws)
最初に取得したルームIDを元に、チャットルームマネージャーが入室したチャットルームのインスタンスを保持しているかexist
で確認します。チャットルームがすでに存在する場合、ユーザーを表すWsClient
インスタンスを生成、ルームインスタンスのJoinフィールドに追加します。
【補足】
ルーム構造体のChatroomRun
メソッドはチャネルにより入退室状況・メッセージの送受信状況を管理しており、Write
、Read
メソッドはデータの読み込み・書き出しを行います。詳細は参照元のページを確認ください。
②部屋がまだ存在しない場合
//②部屋がまだ存在しない場合 WsChatroom := &WsChatroom{ forward: make(chan string), Join: make(chan *WsClient), Leave: make(chan *WsClient), clients: make(map[*WsClient]bool), } go WsChatroom.ChatroomRun() var chatroom WsChat json.Unmarshal([]byte(chatroomJson), &chatroom) WsChatroom.id = chatroom.Id Manager.WsRooms[WsChatroom.id] = WsChatroom WsClient := &WsClient{ Send: make(chan string), Room: WsChatroom, } WsChatroom.Join <- WsClient defer func() { WsChatroom.Leave <- WsClient Manager.WsRooms[WsChatroom.id].Leave <- WsClient if len(Manager.WsRooms[WsChatroom.id].clients) == 0 { delete(Manager.WsRooms, WsChatroom.id) fmt.Println("WebSocket用ルーム削除") } }() go WsClient.Write(ws) WsClient.Read(ws) } } }
本来書く順序的には①より前が良さそうですが、、ルームがまだ作成されていなかった場合の処理になります。
WsChatroom := &WsChatroom{ forward: make(chan string), Join: make(chan *WsClient), Leave: make(chan *WsClient), clients: make(map[*WsClient]bool), } go WsChatroom.ChatroomRun() var chatroom WsChat json.Unmarshal([]byte(chatroomJson), &chatroom) WsChatroom.id = chatroom.Id Manager.WsRooms[WsChatroom.id] = WsChatroom
まずルーム構造体を初期化して入室したチャットルームのインスタンスを生成します。次に取得したルームIDをキーに、ルームインスタンスをチャットルームマネージャーで保持します。後は①の時と同様です。
以上の仕組みで、個々のチャットルームで独立したWebSocket通信を行うことが可能になります。
【Golang】ExecuteTemplate関数で渡した値を、scriptタグ内で参照する方法
経緯
Goのhtml/template
パッケージのExecuteTemplate
関数の第二引数に何らかの値を代入することで、描画するhtml
に値を渡すことができ、html
のタグ内に展開する例がよく紹介されています。しかし、渡した値が配列やスライスの場合、script
タグ内で参照する時と、script
タグ外で参照する時で記法が少し異なってきます。
script
タグ内とscript
タグ外で参照方法が同じ場合
以下の2つの例のように1つのデータを渡す時は、script
タグ内とscript
タグ外で参照方法は変わりません。
なお、script
タグ内で{{.}}
を使うとIDEの設定によってはエラー判定となりますが、問題なく動作します。
①単純な値を渡す場合
hoge := "hoge" tpl := template.Must(template.ParseFiles("./templates/test.html")) tpl.ExecuteTemplate(w, "test.html", hoge)
<body> <p>{{.}}</p> //hoge </body> <script> console.log({{.}}) //hoge </script>
②構造体を1つ渡す場合
type Person struct { //json変換用のキー名の設定はなくてもok Name string `json:"name"` Age int `json:"age"` Height int `json:"height"` } NewPerson := new(Person) NewPerson.Name = "John" NewPerson.Age = 30 NewPerson.Height = 180 tpl := template.Must(template.ParseFiles("./templates/test.html")) tpl.ExecuteTemplate(w, "test.html", NewPerson) }
<body> <p>{{.}}</p> //{John 30 180} <p>{{.Name}}</p> //John <p>{{.Age}}</p> //30 <p>{{.Height}}</p> //180 </body> <script> console.log({{.}}) //{name: "John", age: 30, height: 180} //json用のキー名を設定している場合も、元の構造体のフィールド名を使う! console.log({{.Name}}) //John console.log({{.Age}}) //30 console.log({{.Height}}) //180 </script>
scriptタグ内とscriptタグ外で参照方法が異なる場合
しかし、配列やスライスといったデータを渡す場合、参照方法が異なってきます。script
タグ外では{{index . 0}}
や{{range .}}
{{end}}
などで参照を行いますが、script
タグ内では{{.}}[0]
、{{.}}[1]
というように{{.}}[n]
で配列の要素を取得する形でまず参照を行います。配列の要素が構造体の場合、{{.}}[0].name
というようにキー名を指定し、値を取得することができます。
type Person struct { //json変換用のキー名の設定はなくてもok Name string `json:"name"` Age int `json:"age"` Height int `json:"height"` } func testHandler(w http.ResponseWriter, r *http.Request) { NewPerson1 := new(Person) NewPerson1.Name = "John" NewPerson1.Age = 30 NewPerson1.Height = 180 NewPerson2 := new(Person) NewPerson2.Name = "Mike" NewPerson2.Age = 25 NewPerson2.Height = 175 Slice := []*Person{NewPerson1, NewPerson2} tpl := template.Must(template.ParseFiles("./templates/test.html")) tpl.ExecuteTemplate(w, "test.html", Slice)
<body> <p>{{.}}</p> //[0xc000169240 0xc000169260]などと表示される。 <p>{{index . 0}}</p> //{John 30 180} <p>{{(index . 0).Name}}</p> //John </body> <script> console.log({{.}}) //↓(2) [{…}, {…}] //→0: {Name: "John", Age: 30, Height: 180} //→1: {Name: "Mike", Age: 25, Height: 175} //→length: 2 //→__proto__: Array(0) console.log({{.}}[0]) //{Name: "Mike", Age: 25, Height: 175} console.log({{.}}[1]["name"]) //Mike ※hashの値を取得する形式でも可 console.log({{.}}[1].name) //Mike console.log({{.}}[1].age) //25 console.log({{.}}[1].height) //175 </script>
ちなみに、例では構造体の定義でjson変換時のキー名の設定を追加していますが、設定しなくても値の取得が行えます。その場合はそのままフィールド名をキー名に使用して個々の値を取得できます。
ExecuteTemplate関数で渡される値について
html/templateパッケージの仕様書には詳しい記載はないのですが、これまでの例を見た限り、ExecuteTemplate
関数での値渡しは以下の規則で行われるようです。
①ただの変数・配列などの単純な部分はそのまま渡す。
②構造体・連想配列などの部分があれば、json形式に変換する。
(json変換時のキー名が設定されていなければ、フィールド名をそのまま使用する)
注意点
値の参照でエラーが起きると、scriptタグ内の関数が全て実行されなくなります。
【Golang】同一ページ内の複数のPOSTメソッドを選択的に処理する方法
経緯
一つのページ内で複数のPOST
処理を選択的に実行したいと思ったものの、ハンドラ関数内での分岐をGET
かPOST
かだけでswitchしており、どう処理するか迷いました。
解決法
html
側の各input
要素に異なるname
属性を付け、Request.Method
で分岐後、さらにRequest.FormValue()
関数でどのinput
要素からPOST
されてきたかを、if
で分岐します。POST
されたフォームデータには使用されたinput
要素の連想配列しか含まれないため、POST
されていない処理が行われてしまう心配はありません。
・html form部分の記述
<form action="" method="post"> <p><input type="submit" value="処理1" name="process1"></p> </form> <form action="" method="post"> <p><input type="submit" value="処理2" name="process2"></p> </form>
サーバー側の記述
func SampleHandler(w http.ResponseWriter, r *http.Request) { switch r.Method { case "GET": //あればGET処理 case "POST": err := r.ParseForm() //FormValue関数だけ使う際は不要、r.Formの内容を使用したい場合はParseForm()を行う if err != nil { fmt.Println(err.Error()) } //FormValue()は引数で渡したキーに対応する値を返す if r.FormValue("process1") == "処理1" { //処理1の内容 fmt.Println(r.Form) //r.Formの内容は map[process1:[処理1]] } if r.FormValue("process2") == "処理2" { //処理2の内容 fmt.Println(r.Form) //r.Formの内容は map[process2:[処理2]] } } }
【Golang】自前でマルチプレクサを設定している場合のWebSocketの実装
前提
- 標準ライブラリのみでの実装です。
- WebSocketのパッケージはgorilla/websocketではなく、準標準ライブラリのgolang.org/x/net/WebSocketを使用しています。
一般的なWebSocketの実装方法
Goの準標準ライブラリでWebSocketを実装したサーバーを立てる際、http.Handle(/hoge, Handle)
を用いて、
// https://pkg.go.dev/golang.org/x/net/websocketでの実装例 func EchoServer(ws *websocket.Conn) { io.Copy(ws, ws) } func main() { http.Handle("/echo", websocket.Handler(EchoServer)) err := http.ListenAndServe(":12345", nil) if err != nil { panic("ListenAndServe: " + err.Error()) } }
とDefaultServeMux
にWebSocket用のハンドラを登録するのが一般的かと思われます。
ここで問題…
しかし、私はいくつもhttp.Handle
やらhttp.HandleFunc
を書くのがなんとなく好きでなかったため、ListenAndServe
で第二引数に'nil'を渡さず、下記のような自家製マルチプレクサを渡していました。
この場合、上記のhttp.Handle("/echo", websocket.Handler(EchoServer))
の実装では、websocket.Handler
が自前のマルチプレクサに登録されずDefaultServeMux
に登録されてしまうため、/echo
のルーティング時にWebSocketが機能しません。
// WebSocketが機能しない例(http.Handleで登録) type Mux struct{} func (mux Mux) ServeHTTP(w http.ResponseWriter, r *http.Request) { switch r.URL.Path { case "/hoge": case "/hage": } http.NotFound(w, r) } func EchoServer(ws *websocket.Conn) { io.Copy(ws, ws) } func main() { http.Handle("/echo", websocket.Handler(EchoServer)) //DefaultServeMuxに登録される err := http.ListenAndServe(":12345", mux) if err != nil { panic("ListenAndServe: " + err.Error()) } }
また、下記のようにマルチプレクサ内に処理を記述しても動きませんでした。
// WebSocketが機能しない例(マルチプレクサ内にハンドル記述) type Mux struct{} func (mux Mux) ServeHTTP(w http.ResponseWriter, r *http.Request) { switch r.URL.Path { case "/hoge": case "/echo": //error: websocket.Handler(EchoServer) evaluated but not used websocket.Handler(EchoServer) //ただハンドラをコピペするだけではダメ } http.NotFound(w, r) } func EchoServer(ws *websocket.Conn) { io.Copy(ws, ws) } func main() { err := http.ListenAndServe(":12345", mux) if err != nil { panic("ListenAndServe: " + err.Error()) } }
解決策
Type Handler
に実装されているServeHTTP
メソッドを付け加えると、ハンドリングがうまくいきWebSocketが機能します。イマイチはっきりした理屈はわからないのですが、http.Handle
ではハンドラのServeHTTP
メソッドを勝手に呼び出してくれるところ、今回のように関数内で使用する場合はしっかりServeHTTP(w, r)
を明示しないといけないようです。
// Websocketが起動する例 type Mux struct{} func (mux Mux) ServeHTTP(w http.ResponseWriter, r *http.Request) { switch r.URL.Path { case "/hoge": case "/echo": websocket.Handler(EchoServer)).ServeHTTP(w, r) } http.NotFound(w, r) } func EchoServer(ws *websocket.Conn) { io.Copy(ws, ws) } func main() { err := http.ListenAndServe(":12345", mux) if err != nil { panic("ListenAndServe: " + err.Error()) } }