前言

首先,各位朋友好久不见,月更已经断档三个月了。存货还是有不少的,就是今年年前年后事情比较多,没有什么时间写,最近会尽量找时间把之前的存货清理清理,把之前落下的补上

回到正题,在 k8s 集群内,我们要把服务暴露出来,最简单的方式是开 NodePort ,就像 docker 的 -p 一样,把端口映射出来供大家访问,复杂一点的话就是 ingress ,一般是 nginx ingress,像 nginx 反代一样,再高级一点,就是 istio,除了他的 ingress-gateway 暴露服务之外,还可以做微服务的流量治理

当我们部署一些开源组件的时候,比如 flink 、 Node-RED 这些,部署完成之后,你会发现这些项目的 Dashboard 之类的页面是完全没有认证的,当内网可信的时候,这通常没有什么问题,当内网环境不少那么可信的时候,我们又该怎么办呢?另外,广开 NodePort 也有扩大端口暴露面的风险

有人会说,这个问题简单,我只需要用 Nginx 反代一下,在 Nginx 上面设一个密码就好,但是当很多人共同使用的时候怎么办呢

那么我们不妨自己写一个集成认证的 “Nginx” 吧

Golang 的反向代理(GIN 为例)

最简实现

在做认证之前,咱们需要先了解一下 Go 的反代怎么写,这是最基础的功能,来看以下最简代码

// router路由
router.Any("/proxy/*name", proxyHandler)

// handler
func proxyHandler(c *gin.Context) {
var target = "http://192.168.1.2:1234"
proxyUrl, _ := url.Parse(target)
c.Request.URL.Path = c.Param("name") // 重点是这行代码,传递了 path 给后端服务,值是路由上拿到的【*name】的值
proxy := httputil.NewSingleHostReverseProxy(proxyUrl)
proxy.ServeHTTP(c.Writer, c.Request)
}

处理 WebSocket & Host &自签证书

看完了最简代码,咱们已经能够写一个不带认证的反代出来了,不过这个反代只能代理 http / https 的流量,不能代理 WebSocket ,另外地址也是写死的,所以咱们需要解决这两个问题

先来看 WebSocket 的问题,我们需要判断 HTTP 头里面的 Connection -> Upgrade 以及 Upgrade -> websocket,如果是的话,咱们用 websocket 的库转发,如果不是的话,咱们再用上面的简单反代方法

以下代码用 logrus 打了一些日志,实际运行下看下日志就知道原理了,另外这里针对 https 自签证书的情况,重写了 proxy 的 Transport ,让他跳过证书验证,以及针对后端校验 host 的情况,填写 host 头

import (
"crypto/tls"
"github.com/gin-gonic/gin"
"github.com/gorilla/websocket"
"github.com/sirupsen/logrus"
"net/http"
"net/http/httputil"
"net/url"
"strings"
)

func GinkaReverseProxy(ctx *gin.Context, target, setHost string, ignoreCert bool) {
logrus.Debugln("进入 GinkaReverseProxy 方法")

proxyUrl, _ := url.Parse(target)
ctx.Request.URL.Path = ctx.Param("name")
logrus.Debugln("代理路径:", ctx.Request.URL.Path)

if len(setHost) > 0 {
ctx.Request.Host = setHost
}

logrus.Debugln("c.Request.URL.Path", ctx.Request.URL.Path)
logrus.Debugln("c.Request.Host:", ctx.Request.Host)
logrus.Debugln("c.Request.Response:", ctx.Request.Response)
logrus.Debugln("c.Request.RequestURI:", ctx.Request.RequestURI)
logrus.Debugln("c.Request.RemoteAddr:", ctx.Request.RemoteAddr)

if isWebSocketRequest(ctx.Request) {
logrus.Warnln("GinkaReverseProxy -> WebSocket Connection", ctx.Request.Header.Get("Connection"))
handleWebSocket(ctx.Writer, ctx.Request, proxyUrl)
} else {
var proxy *httputil.ReverseProxy
if ignoreCert {
proxy = &httputil.ReverseProxy{
Rewrite: func(r *httputil.ProxyRequest) {
r.SetURL(proxyUrl)
},
Transport: &http.Transport{
TLSClientConfig: &tls.Config{
InsecureSkipVerify: true,
},
}}
} else {
proxy = httputil.NewSingleHostReverseProxy(proxyUrl)
}
proxy.ServeHTTP(ctx.Writer, ctx.Request)
}
}
func isWebSocketRequest(r *http.Request) bool {
return strings.Contains(r.Header.Get("Connection"), "Upgrade") && r.Header.Get("Upgrade") == "websocket"
}

func handleWebSocket(w http.ResponseWriter, r *http.Request, target *url.URL) {
wsTarget := "ws://" + target.Host + r.URL.Path
logrus.Debugln("WebSocket target URL:", wsTarget)

destConn, _, err := websocket.DefaultDialer.Dial(wsTarget, nil)
if err != nil {
logrus.Errorln("Error connecting to WebSocket backend:", err)
http.Error(w, "Error connecting to WebSocket backend", http.StatusInternalServerError)
return
}
defer destConn.Close()

upgrader := websocket.Upgrader{
CheckOrigin: func(r *http.Request) bool { return true },
}

srcConn, err := upgrader.Upgrade(w, r, nil)
if err != nil {
logrus.Errorln("Error upgrading connection:", err)
http.Error(w, "Error upgrading connection", http.StatusInternalServerError)
return
}
defer srcConn.Close()

go copyWebSocket(destConn, srcConn)
copyWebSocket(srcConn, destConn)
}

func copyWebSocket(dst, src *websocket.Conn) {
for {
mt, message, err := src.ReadMessage()
if err != nil {
logrus.Errorln("Error reading message:", err)
break
}
err = dst.WriteMessage(mt, message)
if err != nil {
logrus.Errorln("Error writing message:", err)
break
}
}
}

配置的动态获取

处理完反代的基础功能之后,咱们来考虑反代地址的问题,因为要在运行时动态的配置,所以不能写死,这里就需要从路由里面拿参数了

来看路由,咱们用 authTypesiteId 两个参数来确定,是否需要认证,使用 authType 标记,站点的配置,在 siteId 标记,咱们根据 id 去数据库找这个站点的配置,校验是否符合用户请求的认证方式,如果符合,放行并传递从数据库里面拿到的反代参数,如果不符合,就拒绝用户的请求

directProxyRouter := v1.Group("/directProxy")
directProxyRouter.Any("/:authType/:siteId/*name", handler.GinkaReverseProxy)

// http://192.168.1.2:8080/ginka/v1/authProxy/platformAuth/1/
authProxyRouter := v1.Group("/authProxy", subfunction.GinkaAuthMiddleware(false))
authProxyRouter.Any("/:authType/:siteId/*name", handler.GinkaReverseProxy)

认证

完成反代功能之后,咱们来看认证,这部分,需要自己写 GIN 的中间件,这部分,对接公司的统一身份认证

这里讲一下思路,具体实现留给各位同学自行完成

  1. 通过 Authorization 或其他 Header 拿认证 Token
  2. 如果拿不到,说明他没有登录过,提醒他走统一身份认证登录
  3. 如果拿到了,到认证组件校验是否过期,如果过期,提醒他重新登录,带跳转链接跳转统一身份认证
  4. 如果没有过期,解析统一身份认证返回的结果,判断他的角色是否有权限访问,如果没有,提示用户没有权限
  5. 已经登录 & Token 在有效期 & 有访问权限 => 放行流量到反代 Handler

fluent_ui 初试

管理界面这边,之前使用的谷歌默认的 materialmaterial 最早是谷歌给安卓提出的设计,适合小屏幕,作为目标用户使用 PC 操作的 B/S 应用,用他显然是不大合适的,这里咱们就搬出巨硬给 Win11 和 UWP 应用设计的 Fluent's design 来做,用法和 material 大同小异,需要注意的一些组件比如表格这边没有就需要引入 material ,像这样

import 'package:flutter/material.dart' as material;
// 用法
material.DataCell createdByCell = material.DataCell(Text(
"${site.createdBy!.isEmpty ? "管理员" : site.createdBy}",
style: styleFontSimkai,
));

来看看效果

效果展示

QQ20250303-162643

QQ20250303-162945

QQ20250303-163016

QQ20250303-163059