前言 首先,各位朋友好久不见,月更已经断档三个月了。存货还是有不少的,就是今年年前年后事情比较多,没有什么时间写,最近会尽量找时间把之前的存货清理清理,把之前落下的补上
回到正题,在 k8s 集群内,我们要把服务暴露出来,最简单的方式是开 NodePort ,就像 docker 的 -p
一样,把端口映射出来供大家访问,复杂一点的话就是 ingress ,一般是 nginx ingress,像 nginx 反代一样,再高级一点,就是 istio,除了他的 ingress-gateway 暴露服务之外,还可以做微服务的流量治理
当我们部署一些开源组件的时候,比如 flink 、 Node-RED 这些,部署完成之后,你会发现这些项目的 Dashboard 之类的页面是完全没有认证的,当内网可信的时候,这通常没有什么问题,当内网环境不少那么可信的时候,我们又该怎么办呢?另外,广开 NodePort 也有扩大端口暴露面的风险
有人会说,这个问题简单,我只需要用 Nginx 反代一下,在 Nginx 上面设一个密码就好,但是当很多人共同使用的时候怎么办呢
那么我们不妨自己写一个集成认证的 “Nginx” 吧
Golang 的反向代理(GIN 为例) 最简实现 在做认证之前,咱们需要先了解一下 Go 的反代怎么写,这是最基础的功能,来看以下最简代码
router.Any("/proxy/*name" , proxyHandler) func proxyHandler (c *gin.Context) { var target = "http://192.168.1.2:1234" proxyUrl, _ := url.Parse(target) c.Request.URL.Path = c.Param("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 } } }
配置的动态获取 处理完反代的基础功能之后,咱们来考虑反代地址的问题,因为要在运行时动态的配置,所以不能写死,这里就需要从路由里面拿参数了
来看路由,咱们用 authType
和 siteId
两个参数来确定,是否需要认证,使用 authType 标记,站点的配置,在 siteId 标记,咱们根据 id 去数据库找这个站点的配置,校验是否符合用户请求的认证方式,如果符合,放行并传递从数据库里面拿到的反代参数,如果不符合,就拒绝用户的请求
directProxyRouter := v1.Group("/directProxy" ) directProxyRouter.Any("/:authType/:siteId/*name" , handler.GinkaReverseProxy) authProxyRouter := v1.Group("/authProxy" , subfunction.GinkaAuthMiddleware(false )) authProxyRouter.Any("/:authType/:siteId/*name" , handler.GinkaReverseProxy)
认证 完成反代功能之后,咱们来看认证,这部分,需要自己写 GIN 的中间件,这部分,对接公司的统一身份认证
这里讲一下思路,具体实现留给各位同学自行完成
通过 Authorization
或其他 Header 拿认证 Token
如果拿不到,说明他没有登录过,提醒他走统一身份认证登录
如果拿到了,到认证组件校验是否过期,如果过期,提醒他重新登录,带跳转链接跳转统一身份认证
如果没有过期,解析统一身份认证返回的结果,判断他的角色是否有权限访问,如果没有,提示用户没有权限
已经登录 & Token 在有效期 & 有访问权限 => 放行流量到反代 Handler
fluent_ui 初试 管理界面这边,之前使用的谷歌默认的 material
,material
最早是谷歌给安卓提出的设计,适合小屏幕,作为目标用户使用 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, ));
来看看效果
效果展示