バックエンドをGoでフロントエンドをReactでアプリを作成しており、CSRF攻撃の対策のためgorilla/csrfを導入しようとしたのですが、CORS関係でエラーが頻発しました。
この記事では、Reactは使わず、goで立ち上げた仮想フロントエンドサーバーにaxiosを入れて解説します。
gorilla/csrfライブラリの公式
https://github.com/gorilla/csrf#javascript-applications
実装結果は下記の通りです。
ファイル構成
.
├── back
│ └── main.go
├── front
│ ├── index.html
│ └── main.go
├── go.mod
└── go.sum
front/main.go
package main
import (
"log"
"net/http"
"text/template"
)
func main() {
http.HandleFunc("/", func(w http.ResponseWriter, _ *http.Request) {
t, _ := template.ParseFiles("index.html")
t.Execute(w, nil)
})
log.Fatal(http.ListenAndServe(":3000", nil))
}
front/index.html
<!DOCTYPE html>
<head>
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
</head>
<body>
<button onclick="submit()">Post Data</button>
<script>
async function submit() {
const instance = axios.create({
baseURL: "http://localhost:8000/api",
withCredentials: true, // Cookieのやりとりをオンにする
})
// トークンを取得
const response = await instance.get("/token")
// 受け取ったトークンをX-CSRF-Tokenヘッダーに付ける
instance.defaults.headers.post["X-CSRF-Token"] = response.headers["x-csrf-token"]
instance
.post("/post", new URLSearchParams({say: "The World! Stop time!"}))
.then(res => alert(res.data))
}
</script>
</body>
</html>
back/main.go
package main
import (
"fmt"
"net/http"
"github.com/gorilla/csrf"
"github.com/gorilla/mux"
)
func main() {
r := mux.NewRouter()
csrfMiddleware := csrf.Protect([]byte("32-byte-long-auth-key"))
api := r.PathPrefix("/api").Subrouter()
api.Use(csrfMiddleware)
api.HandleFunc("/token", token).Methods(http.MethodGet)
api.HandleFunc("/post", post).Methods(http.MethodOptions) // プリフライトを受け付ける
api.HandleFunc("/post", post).Methods(http.MethodPost)
http.ListenAndServe(":8000", r)
}
func token(w http.ResponseWriter, r *http.Request) {
// オリジン間リソース共有と言ったらこれが無いと始まらないよね
w.Header().Set("Access-Control-Allow-Origin", "http://localhost:3000")
// X-CSRF-Tokenをフロント側で受け取れるようにする
w.Header().Set("Access-Control-Expose-Headers", "X-CSRF-Token")
// フロント側のブラウザにクッキーがセットされるようにする
w.Header().Set("Access-Control-Allow-Credentials", "true")
// トークンをセット
w.Header().Set("X-CSRF-Token", csrf.Token(r))
}
func post(w http.ResponseWriter, r *http.Request) {
// オリジン間リソース共有と言ったらこれが無いと始まらないよね
w.Header().Set("Access-Control-Allow-Origin", "http://localhost:3000")
// 受け入れるヘッダーを指定。特にX-CSRF-Tokenは忘れずに。
w.Header().Set("Access-Control-Allow-Headers", "Content-Type, X-CSRF-Token")
// プリフライトのときにPOSTメソッド許可を返す。
w.Header().Set("Access-Control-Allow-Methods", "POST")
// Cookieを受け入れる。
w.Header().Set("Access-Control-Allow-Credentials", "true")
fmt.Fprint(w, r.FormValue("say"))
}
動作確認
zsh
$ go run front/main.go
$ go run back/main.go
http://local:3000
をブラウザで開いて、ボタン押してコンソールエラーが無ければ、OKです。
以上です。