📘
Go + React(TypeScript)の実装で理解するCORS

1.記事を書いた背景
私はインフラ側の経験が主ですが、開発チームから依頼されてS3のCORS許可ポリシーを設定することが何度かありました。
ただ、その度に調べて何となく理解し直すということを繰り返していてインフラ視点での理解に留まっていて少しモヤのある状態でした。
(実際にアプリ側の実装を行い、いくつかの実装パターンがあるという点も把握しないと感覚的な理解が得にくい設定だと思います。)
今回、GoのAPI開発を通してアプリケーション側でCORSを実装したことでより深く理解できたので、自分の知識を整理する目的で本記事をまとめています。
2.対象読者
- CORSの概念や設定についてふわっと理解しているが、実装レベルでの理解を深めたい方(自分みたいな)
- フロント,バックの実装レベルで処理を追いたい方(自分みたいなインフラ出身の方とか)
3.本記事について
3-1.書くこと
- GoにおけるCORSの実装を行います。
最終的にフロントエンドからCORSのリクエストが許可、拒否されていることを確認していきたいと思います。
3-2.書かないこと
- CORSの詳細については既に分かりやすい記事はありますので、以下あたりをご参照ください。
4.実装
4-1.AWSの認証について
- 今回はローカルの都合上、
.aws/configure配下にAWS SSO経由で一時的なAdmin権限を取得しています。
4-2.実装パターンについて
-
今回はS3に格納している画像ファイルをGoのAPI側でCORSの制御を行い、バックエンド-S3間で発行した署名付きURLをフロントに返します。
フロントのHTMLから直接署名付きURLを叩いて画像ファイルを取得して表示します。 -
実際のアプリケーションでは認証・認可が必要ですが、
CloudFront+Lambda@Edgeを使ったエッジでの検証や、
API Gateway+Lambda Authorizerによる権限制御などの選択肢があります。
今回はCORSの処理の流れを追うことを焦点にしているため、これらの実装は省略しました。
4-3.処理の流れ
- バックエンドAPIでCORS制御を行い、S3の署名付きURLを発行しています。署名付きURLを使用することで、S3バケット側でCORSポリシーを設定する必要がありません。
4-5.インフラ側の認証
- 事前にインフラ側(今回はAWS)の認証をクリアしておくこと。
- 今回のケースではテスト用にローカルに埋め込んでいるSSOで一時的なセッショントークンを使用しているため、以下コマンドを事前に入力してブラウザで承認しました。
- 実際にECSやLambdaでデプロイする場合だとIAMロールで制御します。
aws sso login --profile {profile_name}
4-4.フロントエンド(TypeScript + React)
-
ボタンを押下するとAPIサーバをフェッチして最終的に署名付きURLで直接S3から画像データを取得、表示しています。
-
今回は検証目的のため、Authorization ヘッダーには固定値を設定していますが、本番環境では適切な認証トークンを使用してください。
{project_root}/src/components/Cors.tsx
import { useState } from "react";
export const CorsTest = () => {
const [imageS3, setImageS3] = useState<string>("");
const [error, setError] = useState<string>("");
const API_DOMAIN = "http://localhost:8081";
const API_PATH = "pic"
const getS3 = async () => {
try {
setError("");
const response = await fetch(`${API_DOMAIN}/${API_PATH}`, {
method: "GET",
// この記事では認証処理は省略しています。
headers: { "Authorization": "valid-token" },
});
if (!response.ok) {
setError(`GET Error: HTTP ${response.status} ${response.statusText}`);
return;
}
const data = await response.json();
console.log("data",data)
setImageS3(data.PresignURL);
} catch (err) {
if (err instanceof TypeError && err.message === 'Failed to fetch') {
setError(`GET Error: CORS error or network error. Check browser console for details.`);
} else {
setError(`GET Error: ${err instanceof Error ? err.message : String(err)}`);
}
console.error('Detailed error:', err);
}
};
// imgタグに署名付きURLをステートで設定しボタンがクリックされたタイミングでGETを実行し表示する。
return (
<div style={{ padding: "20px" }}>
<h2>CORS Middleware Test</h2>
<h3>GET /{API_PATH}</h3>
<button onClick={getS3}>GET S3 Image</button>
{imageS3 && (
<div style={{ marginBottom: "20px" }}>
<img
src={imageS3}
alt="S3 Image"
style={{ maxWidth: "500px", marginTop: "10px" }}
/>
</div>
)}
{error && (
<div style={{ color: "red" }}>
<h3>Error:</h3>
<pre>{error}</pre>
</div>
)}
</div>
);
};
{project_root}/src/App.tsx
import { CorsTest } from "@/components/Cors";
function App() {
return (
<>
<CorsTest />
</>
);
}
export default App;
バックエンド
- API側でCORS制御を行い、問題なければS3の署名付きURLの発行を行なっています。
./tasks/{task12-cors}/routes/s3.go
package routes
import (
"context"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/config"
"github.com/aws/aws-sdk-go-v2/service/s3"
"github.com/gin-gonic/gin"
"github.com/takehiro1111/gin-api/tasks/task12-cors/constants"
"net/http"
"time"
)
// 定数で定義している箇所はよしなに変更してください。
func GetS3FIle(c *gin.Context) {
// S3との処理が10秒以上かかる場合はtimeoutするよう設定。
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
cfg, err := config.LoadDefaultConfig(
ctx,
// constants.DIとしているが、ローカル環境に応じた設定を行う。
config.WithSharedConfigProfile(constants.DI),
config.WithRegion("ap-northeast-1"),
)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"error": "failed get aws configure",
})
return
}
s3Client := s3.NewFromConfig(cfg)
input := &s3.GetObjectInput{
Bucket: aws.String(constants.S3BucketName),
Key: aws.String(constants.S3KeyName),
}
s3PresignClient := s3.NewPresignClient(s3Client)
presignedReq, err := s3PresignClient.PresignGetObject(ctx, input,
func(opts *s3.PresignOptions) {
// セキュリティ上の要件に応じて発行した署名付きURLに15分の時間制限をつける。
opts.Expires = 15 * time.Minute
})
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"error": err.Error(),
})
return
}
// フロント側ではPresignURLの値を取得する。
c.JSON(http.StatusOK, gin.H{
"PresignURL": presignedReq.URL,
"header": presignedReq.SignedHeader,
})
}
AllowOriginsでlocalhost:3086以外のリクエストを拒否する。
./tasks/{task12-cors}/main.go
package main
import (
"github.com/gin-contrib/cors"
"github.com/gin-gonic/gin"
// packageのパスは各ローカル環境に応じて設定が必要。
m "github.com/takehiro1111/gin-api/tasks/internal/middleware"
r "github.com/takehiro1111/gin-api/tasks/task12-cors/routes"
"os"
"time"
)
func main() {
router := gin.Default()
loggerConfig := m.NewLoggerConfig(
"info",
"text",
m.WithTimeFunc(time.Now),
m.WithWriter(os.Stdout),
m.WithEnableRequestBody(true),
)
router.Use(m.LoggerMiddleware(loggerConfig))
// 今回はフロント側の処理を行うサーバ(localhost:3086)のみ許可する
router.Use(cors.New(cors.Config{
// フロント側は3086でローカルにサーバを立ててます。
AllowOrigins: []string{"http://localhost:3086"},
AllowMethods: []string{"GET", "OPTIONS"},
AllowHeaders: []string{"Origin", "Content-Type", "Authorization"},
ExposeHeaders: []string{"Content-Length"},
AllowCredentials: true,
MaxAge: 12 * time.Hour,
}))
// フロント側でlocalhost:8081/picを叩くと署名付きURLをS3から取得してフロントへ返す処理が走る。
router.GET("/pic", r.GetS3FIle)
router.Run(":8081")
}
確認
許可
- フロントのサーバを
3086で起動し、APIを叩く - ブラウザでボタンをクリックすると、APIサーバのCORS制御を経由して、S3に格納した画像(今回はS3のロゴ)が返ることを確認できました。
拒否
- バックエンド側で
AllowOriginsの許可設定から3086を削除する。(他のポート番号にしてみる)
// 3086を指定しない
AllowOrigins: []string{"http://localhost:10000"},
- CORSエラーが返ることを確認できました。
GET Error: CORS error or network error. Check browser console for details.

参考
Discussion