今天想试着做一个gin的反向代理,代理其他的服务,给前端使用,思路如下:
主要组成部分
-
跨域中间件(
Cors
函数)Cors
中间件函数通过在 HTTP 响应中设置合适的头部信息,允许跨域请求。- 它添加了允许的方法(例如
POST
、GET
、OPTIONS
等),并向客户端公开特定的头部字段。 - 对于预检请求(即
OPTIONS
请求),直接返回 "ok" 表示请求被允许。 - 还包含一个
defer
函数,用于在发生 panic 时进行恢复处理。
-
反向代理中间件(
ReverseProxy
函数)ReverseProxy
函数将传入的请求转发到由targetURL
指定的目标服务器。- 使用
httputil.NewSingleHostReverseProxy
为指定目标创建一个反向代理。 - 调整请求的 host 和 URL,使其与目标服务器匹配,以确保请求被正确转发。
-
动态更新目标 URL(
UpdateTargetURL
函数)- 该端点(
/api/update-target
)用于更新反向代理所使用的targetURL
,这是一个受读写锁保护的全局变量。 - 接收包含
host
和port
值的 JSON 负载,以设置新的目标 URL。 - 更新后的
targetURL
会被格式化并存储,然后返回一个确认消息。
- 该端点(
-
服务器启动函数(
Start
函数)Start
函数根据配置的模式(debug 或 release)初始化 Gin 服务器。- 全局应用 CORS 中间件,并设置一个用于更新目标 URL 的端点。
- 设置反向代理路由,将
/v1/*proxyPath
下的所有请求转发到当前目标 URL。 - 服务器监听并在端口 8080 上运行。
功能要求
- 并发和安全性 :通过使用
mu
锁,确保对targetURL
的线程安全访问,因为多个 goroutine 可能同时读取或更新此值。 - 错误处理 :
UpdateTargetURL
函数处理格式不正确的 JSON 请求,如果输入的 JSON 无效,则返回400 Bad Request
。 - 可配置的代理目标 :允许动态更新
targetURL
使该服务器设置更灵活,可以根据需要将请求路由到不同的后端服务器。 - 调试 :在
Start
中检查配置的详细模式,以根据需要设置 Gin 的 debug 或 release 模式,用于在生产与开发环境中切换日志记录。
此设置非常适合需要将请求动态代理到不同后端服务器的场景,同时处理跨域请求并确保并发访问的安全性。
代码实现
go
package web
import (
"fmt"
"net/http"
"net/http/httputil"
"net/url"
"sync"
"github.com/gin-gonic/gin"
"client/backend/internal/config"
)
var (
targetURL string = "http://127.0.0.1:8080" // Default target URL
mu sync.RWMutex // Mutex for safe concurrent access
)
// 跨域中间件
func Cors() gin.HandlerFunc {
return func(c *gin.Context) {
method := c.Request.Method
origin := c.Request.Header.Get("Origin") //请求头部
if origin != "" {
//接收客户端发送的origin (重要!)
c.Writer.Header().Set("Access-Control-Allow-Origin", origin)
//服务器支持的所有跨域请求的方法
c.Header("Access-Control-Allow-Methods", "POST, GET, OPTIONS, PUT, DELETE,UPDATE")
//允许跨域设置可以返回其他子段,可以自定义字段
c.Header("Access-Control-Allow-Headers", "Content-Type,Authorization, Content-Length, X-CSRF-Token, Token,session")
// 允许浏览器(客户端)可以解析的头部 (重要)
c.Header("Access-Control-Expose-Headers", "Content-Length, Access-Control-Allow-Origin, Access-Control-Allow-Headers")
//设置缓存时间
c.Header("Access-Control-Max-Age", "172800")
//允许客户端传递校验信息比如 cookie (重要)
c.Header("Access-Control-Allow-Headers", "Origin, Content-Type, Accept, Authorization, X-Requested-With")
c.Header("Access-Control-Allow-Credentials", "true")
}
//允许类型校验
if method == "OPTIONS" {
c.JSON(http.StatusOK, "ok!")
}
defer func() {
if err := recover(); err != nil {
fmt.Println("Panic info is: %v", err)
}
}()
c.Next()
}
}
// ReverseProxy function to forward requests to the target server.
// ReverseProxy function to forward requests to the current target server.
func ReverseProxy() gin.HandlerFunc {
return func(c *gin.Context) {
mu.RLock() // Lock for reading
defer mu.RUnlock()
target, err := url.Parse(targetURL)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Invalid target URL"})
return
}
proxy := httputil.NewSingleHostReverseProxy(target)
// Update the request to the target URL
c.Request.Host = target.Host
c.Request.URL.Host = target.Host
c.Request.URL.Scheme = target.Scheme
// Serve the request using the reverse proxy
proxy.ServeHTTP(c.Writer, c.Request)
}
}
type BackendServer struct {
Host string `json:"host"`
Port int `json:"port"`
}
// UpdateTargetURL updates the target URL dynamically.
func UpdateTargetURL(c *gin.Context) {
var options BackendServer
// Bind the JSON body to the newTarget struct
if err := c.ShouldBindJSON(&options); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request body", "code": 400})
return
}
mu.Lock() // Lock for writing
defer mu.Unlock()
// Update the global targetURL variable
targetURL = fmt.Sprintf("%s:%d", options.Host, options.Port)
c.JSON(http.StatusOK, gin.H{"message": "Target URL updated", "new_target": targetURL, "code": 200})
}
func Start() {
if config.C().Verbose {
gin.SetMode(gin.DebugMode)
} else {
gin.SetMode(gin.ReleaseMode)
}
router := gin.Default()
router.Use(Cors())
// Route to update the target server URL
router.POST("/api/update", UpdateTargetURL)
// Proxy all requests to the /api endpoint to the current target server
router.Any("/v1/*proxyPath", ReverseProxy())
// Run the Gin server on port 8080
if err := router.Run(":8080"); err != nil {
panic(err)
}
}