【Go】实现一个代理Kerberos环境部分组件控制台的Web服务
实现一个代理Kerberos环境部分组件控制台的Web服务
- 背景
- 安全措施引入的问题
- SSO单点登录
- 过程
- 整体设计
- 路由
- 反向代理
- 登录会话
- 组件代理
- Yarn
- Hbase
- 结果
背景
首先要说明下我们目前有部分集群的环境使用的是HDP-3.1.5.0的大数据集群,除了集成了一些自定义的服务以外,没有对组件做二次开发。
安全措施引入的问题
生产环境部署的集群由于安全需求,一般都必须打开Kerberos认证,有的客户会要求同时开启https访问,而且不允许关闭web访问的安全认证。这种情况下,如果直接在浏览器访问组件的控制台,就会提示输入用户名密码进行登录或者直接报403:

而且一般来说我们用户只能拿到keytabs文件,拿不到用户名密码信息;一种常见的解决方式是安装KIT工具,这样浏览器就可以通过keytab进行认证了,不过只要涉及到在自己电脑上装多余的东西就很难受。
有的客户会提供单独的Windows堡垒机,堡垒机上安装好需要的插件、客户端,然后需要访问webUI的时候就登录堡垒机进行。
SSO单点登录
有的公司具备较强的研发实力,能够基于组件去开发统一的单点登录程序,这种确实很牛逼,但是一般小公司不具备这种实力;开源解决方案里,唯一可选的只有Knox,用过都觉得难受,如果对Knox以及认证流程不熟悉的话,会把自己玩死。而且Knox的方便是有前提的,那就是能够配置好,配置好了以后做单点访问确实很方便。
基于此,当然还有很多其他的原因,最终我想自己做一个简单的web服务,通过反向代理等手段实现一般用户对开启Kerberos的hdfs、yarn、hbase环境的控制台访问。
过程
整体设计
功能大概分为这几点:
- 代理hdfs、yarn、hbase三种服务的控制台UI
- hdfs和hbase的web可以不考虑主备,因为正常情况下也是都能访问的
- yarn的web在访问到备节点时会跳转到主节点,这个要做一下处理,不然会出现页面找不到
- 通过单一代理服务同时能够访问多个服务,所以需要对路由做区分,这就对代理转发有一些特殊要求
- 请求过程中要完成kerberos认证设置SPNEGO请求认证头,以及TLS的配置
- 由于将安全链接代理出来了,为了防止引入新的安全问题,还需要一个简易的登录逻辑
整体来说上述的几点就是程序的主要功能点和需求点了。
路由
web服务我使用Gin框架,路由的设置单独放在一个初始化函数initRoute中,因为服务很小,就直接用Gin自带的模板功能渲染html页面了,所以/login路由需要有GET和POST两个接口,一个用来渲染模板页,一个用来点击登录提交信息:
route.GET("/login", func(ctx *gin.Context) {ctx.HTML(http.StatusOK, "login.html", gin.H{})})route.POST("/login", Login)// 注销按钮,注销后清空session信息route.POST("/signout", SignOut)// 主页,根据组件服务信息渲染页面route.GET("/index", AuthMiddler, func(ctx *gin.Context) {ctx.HTML(http.StatusOK, "index.html", gin.H{"nns": config.Y.Namenode.Servers,"nn_port": config.Y.Namenode.Port,"rms": config.Y.ResourceManager.Servers,"rm_port": config.Y.ResourceManager.Port,"hms": config.Y.HbaseMaster.Servers,"hm_port": config.Y.HbaseMaster.Port,})})// 根路由重定向到主页route.GET("/", func(ctx *gin.Context) {ctx.Redirect(http.StatusMovedPermanently, "/index")})// 服务的世界代理页serviceGroup := route.Group("/service"){// serviceGroup.Use(AuthMiddler)serviceGroup.GET("/nn/:host/:port/*path", GetNNWeb)serviceGroup.GET("/rm/:host/:port/*path", GetRMWeb)serviceGroup.GET("/hm/:host/:port/*path", GetHMWeb)}route.NoRoute(defaultFunc)
gin中有一个NoRoute方法,这个是用来处理找不到路由时的情况,防止出现找不到页面时没有平滑的处理,这里用一个默认处理函数对404进行处理,比如跳转到404页面。
反向代理
首先是反向代理,Go实现反向代理是比较简单的,这里由于需要区分路由进行代理转发所以就要做个处理,实现逻辑大概如下:
NewProxy函数要求输入代理服务器targetHost,路由路径path,以及用于Kerberos认证时区分主机的sp字符串,targetHost在传入后使用url.Parse进行解析并作为变量传入NewSingleHostReverseProxy方法就能生成一个反向代理结构体指针*httputil.ReverseProxy了,这个时候所有的代理连接都会通过代理服务器做转发,路由路径会被拼接到端口之后:
比如你的方向代理监听端口是http://127.0.0.1:8088,实际服务地址是http://10.0.0.1:9444/dfs/index.html,那么此时你的url会变成http://127.0.0.1:8088/dfs.index.html
如果我们只是代理单一的服务,可以这样去做,反正我们不用关心路由具体去哪,只要来我8088端口的流量我都转发到9444就行了,但是此处我们需要进行路由的处理,所以传入了path变量,方便对路由的内容做处理;最后一个变量sp就是用于设置Spnego请求头的时候用的SPN,在Kerberos认证的时候,如果我请求的url对应的域名是host001的时候,我必须要指定SPN是属于host001的,具体的解释可以参照这段
SPN - Service Principal Name. It is an identifier associated with each account in a KDC implementation(AD, OpenLDAP etc). Basically if your account acts as a service to which a client authenticates, the client has to specify “who” it wants to communicate to. This “who” identifier is the SPN. This is the strict definition. Many people often call the client name (UPN - User Principal Name) of a service as SPN. This happens when the service itself may act as a client( google the delegation scenario ). This is not strictly correct but widely assumed true.
因此,在每次创建代理的时候我都需要对SPN进行设置,这样才能保证每次我都能用正确的身份去请求对应的服务;
通过*httputil.ReverseProxy的Director设置我可以对代理的请求进行处理,因此进行Kerberos认证的操作就在此处进行:
proxy.Director = func(req *http.Request) {// 修改请求,此处尝试添加kerberos认证originalDirector(req)if ck != nil {req.AddCookie(ck)}kc, err := config.Y.KerberosClient.CreatConfig()if err != nil {log.Error(err)}if err := spnego.SetSPNEGOHeader(kc, req, fmt.Sprintf("HTTP/%s", sp)); err != nil {log.Error(err)}req.URL.Path = pathreq.Host = url.Host}
代理服务创建的完全代码如下,这其中还包括的TLS相关信息的配置,这里我按照自己需要进行了封装,最终返回一个*tls.Config结构体指针用于创建http.Transport即可
func NewProxy(targetHost string, path string, sp string) (*httputil.ReverseProxy, error) {url, err := url.Parse(targetHost)if err != nil {return nil, err}proxy := httputil.NewSingleHostReverseProxy(url)t := tls.New(config.Y.TLS.HTTPS, config.Y.TLSCa, config.Y.TLSCert, config.Y.TLSKey, config.Y.InsecureSkipVerify)tlsConfig, err := t.TLSConfig()if err != nil {return nil, err}dialer := &net.Dialer{}proxy.Transport = &http.Transport{DialContext: dialer.DialContext,DisableKeepAlives: true,TLSClientConfig: tlsConfig,}// 此处是为了获取到原本的请求处理函数,不加这个的话,预制的处理逻辑会丢失originalDirector := proxy.Directorproxy.Director = func(req *http.Request) {originalDirector(req)if ck != nil {req.AddCookie(ck)}kc, err := config.Y.KerberosClient.CreatConfig()if err != nil {log.Error(err)}if err := spnego.SetSPNEGOHeader(kc, req, fmt.Sprintf("HTTP/%s", sp)); err != nil {log.Error(err)}req.URL.Path = path// 一定要将请求头的Host修改成代理的目标Host,否则Kerberos认证也不会通过req.Host = url.Host}return proxy, nil
}
登录会话
因为我懒得去做啥用户系统,这本身也就是一个方便运维人员用的小程序,所以就简单依赖于Session来实现一个登录逻辑。
首先每个页面都需要对是否登录做一个验证,这就需要一个中间件,对于单一浏览器的session,直接使用github.com/gin-gonic/gin包中的session进行设置,中间件会去检查Session中是否有名为Owl的头信息,并且值是否为Login,如果是Login就直接认为登陆过了,否则重定向到登录页,中间件逻辑如下:
func AuthMiddler(c *gin.Context) {session := sessions.Default(c)if session.Get("Owl") != "Login" {c.Redirect(http.StatusMovedPermanently, "/login")return}
}
Login服务的逻辑就是简单比对用户名和密码,因为懒得去做用户系统,这里就写成硬编码,用户名必须为admin,然后设置Session信息,这个信息会保存在请求头的Cookies中:

代码如下:
func Login(c *gin.Context) {// 获取前端传来的登录用户信息user := c.PostForm("username")password := c.PostForm("password")// 只做简单的比对并设置Session信息if user == "admin" && password == "XXXX" {session := sessions.Default(c)session.Set("Owl", "Login")session.Save()c.Redirect(http.StatusFound, "/index")} else {c.Redirect(http.StatusMovedPermanently, "/login")}
}
注销逻辑就是直接删除Owl信息即可:
func SignOut(c *gin.Context) {session := sessions.Default(c)session.Delete("Owl")session.Save()c.Redirect(http.StatusMovedPermanently, "/login")
}
组件代理
对于Namenode的Web代理比较简单,因为没有什么特殊的跳转,但是对于Yarn和Hbase有一定特殊性需要单独处理;
Yarn
Yarn的特殊性在于如果点入了备节点,会被默认重定向到主节点,然后代理可能就会404,这里要单独进行处理。
当发生重定向的时候,路由会变成/cluster,并且http状态码会是307,所以这里可以在代理的响应处理中做处理,当状态码是307的时候,判断url中是否有cluster关键字,如果有就更改内存中保存的ActiveRm变量的值为另一个节点的hostname,然后重定向到另一个节点的WebUi就可以了
proxy.ModifyResponse = func(r *http.Response) error {url := r.Request.URL.Pathif r.StatusCode == 307 {// 307的情况下是到了备的yarn节点// 判断下是不是yarnif strings.Contains(url, "cluster") {service.GetActiveRm(r.Request.URL.Hostname())}c.Redirect(http.StatusMovedPermanently, fmt.Sprintf("/service/rm/%s/%v", service.ActiveRm, config.Y.ResourceManager.Port))}return nil}
切换内存变量的方法逻辑如下:
func GetActiveRm(host string) {if ActiveRm == "" {ActiveRm = config.Y.ResourceManager.Servers[0]} else {for _, v := range config.Y.ResourceManager.Servers {ActiveRm = vbreak}}
}
Hbase
Hbase的WebUI的特殊性在于他的控制台除了主页以外,都是通过jsp模板渲染出来的:

我在本地测试的时候没有问题,页面可以正常访问,比如表信息查看的页面,正常来说是这样的:

一旦进行远程访问时,页面的Js文件和样式就加载不出来:

这个问题查了很久,后来对比了一下两个请求的信息,发现在Cookies中存在差别,请求头的Cookies应该包含hadoop-auth:

如果没有包含这个请求头,就会在进行Kerberos认证的时候使用同一个认证主体进行重复认证的问题,这样会发生报错:
Authentication exception: GSSException: Failure unspecified at GSS-API level (Mechanism level: Request is a replay (34))
我的处理方法是,设置一个全局变量和全局锁:
var (yamlPath = flag.String("config.path", "./", "运行配置文件")scheme = "http"ck = &http.Cookie{}lock = sync.Mutex{}
)
在进行访问的时候,检查Cookies中是否包含hadoop-auth,如果不包含就把请求中返回的hadoop-auth加入到Cookie中,为了防止出现冲突,使用全局锁进行控制:
proxy.ModifyResponse = func(r *http.Response) error {cs := r.Cookies()for _, v := range cs {if v.Name == "hadoop.auth" {lock.Lock()ck = vlock.Unlock()}}return nil}
这部分的处理逻辑只需要在设置代理的函数中进行添加即可。
结果
最终在各种修修补补下,完成了控制台程序的整体逻辑,并且能够正常使用了,前端使用BootStarp简单写了下,看着也算是有模有样了:


相关文章:
【Go】实现一个代理Kerberos环境部分组件控制台的Web服务
实现一个代理Kerberos环境部分组件控制台的Web服务 背景安全措施引入的问题SSO单点登录 过程整体设计路由反向代理登录会话组件代理YarnHbase 结果 背景 首先要说明下我们目前有部分集群的环境使用的是HDP-3.1.5.0的大数据集群,除了集成了一些自定义的服务以外&…...
Spring Security 6.x 系列【63】扩展篇之匿名认证
有道无术,术尚可求,有术无道,止于术。 本系列Spring Boot 版本 3.1.0 本系列Spring Security 版本 6.1.0 本系列Spring Authorization Server 版本 1.1.0 源码地址:https://gitee.com/pearl-organization/study-spring-security-demo 文章目录 1. 概述2. 配置3. Anonymo…...
供应链管理系统有哪些?
1万字干货分享,国内外 20款 供应链管理软件都给你讲的明明白白。如果你还不知道怎么选择,一定要翻到第三大段,这里我将会通过8年的软件产品选型经验告诉你,怎么样才能快速选到适合自己的软件工具。 (为防后续找不到&a…...
如何在PADS Logic中查找器件
PADS Logic提供类似于Windows的查找功能,可以进行器件的查找。 (1)在Logic设计界面中,将菜单显示中的“选择工具栏”进行打开,如图1所示,会弹出对应的“选择工具栏”的分栏菜单选项,如图2所示。…...
Android 生成pdf文件
Android 生成pdf文件 1.使用官方的方式 使用官方的方式也就是PdfDocument类的使用 1.1 基本使用 /**** 将tv内容写入到pdf文件*/RequiresApi(api Build.VERSION_CODES.KITKAT)private void newPdf() {// 创建一个PDF文本对象PdfDocument document new PdfDocument();//创建…...
Kafka 入门到起飞 - 生产者发送消息流程解析
生产者通过send()方法发送消息消息会经过拦截器->序列化器->分区器 进行加工然后将消息存在缓冲区当缓冲区中消息达到条件会按批次发送到broker对应分区上broker将接收到的消息进行刷盘持久化消息处理broker会返回给producer响应落盘成功返回元数据…...
基于单片机智能台灯坐姿矫正器视力保护器的设计与实现
功能介绍 以51单片机作为主控系统;LCD1602液晶显示当前当前光线强度、台灯灯光强度、当前时间、坐姿距离等;按键设置当前时间,闹钟、提醒时间、坐姿最小距离;通过超声波检测坐姿,当坐姿不正容易对眼睛和身体腰部等造成…...
欧姆龙以太网模块如何设置ip连接 Kepware opc步骤
在数字化和自动化的今天,PLC在工业控制领域的作用日益重要。然而,PLC通讯口的有限资源成为了困扰工程师们的问题。为了解决这一问题,捷米特推出了JM-ETH-CP转以太网模块,让即插即用的以太网通讯成为可能,不仅有效利用了…...
PLEX如何搭建个人局域网的视频网站
Plex是一款功能非常强大的影音媒体管理系统,最大的优势是多平台支持和界面优美,几乎可以在所有的平台上安装plex服务器和客户端,让你可以随时随地享受存储在家中的电影、照片、音乐,并且可以实现观看记录无缝衔接,手机…...
java学习02
一、基本数据类型 Java有两大数据类型,内置数据类型和引用数据类型。 内置数据类型 Java语言提供了八种基本类型。六种数字类型(四个整数型,两个浮点型),一种字符类型,还有一种布尔型。 byte࿱…...
libcurl库使用实例
libcurl libcurl是一个功能强大的跨平台网络传输库,支持多种协议,包括HTTP、FTP、SMTP等,同时提供了易于使用的API。 安装 ubuntu18.04平台安装 sudo apt-get install libcurl4-openssl-dev实例 这个示例使用libcurl库发送一个简单的HTTP …...
大数据存储架构详解:数据仓库、数据集市、数据湖、数据网格、湖仓一体
前言 本文隶属于专栏《大数据理论体系》,该专栏为笔者原创,引用请注明来源,不足和错误之处请在评论区帮忙指出,谢谢! 本专栏目录结构和参考文献请见大数据理论体系 思维导图 数据仓库 数据仓库是一个面向主题的&…...
ESP32(MicroPython) 网页控制五自由度机械臂
ESP32(MicroPython) 网页控制五自由度机械臂 本程序通过网页控制五自由度机械臂,驱动方案改用PCA9685。 代码如下 #导入Pin模块 from machine import Pin import time from machine import SoftI2C from servo import Servos import networ…...
前端笔记_OAuth规则机制下实现个人站点接入qq三方登录
文章目录 ⭐前言⭐qq三方登录流程💖qq互联中心创建网页应用💖配置回调地址redirect_uri💖流程分析 ⭐思路分解⭐技术选型实现💖技术选型:💖实现 ⭐结束 ⭐前言 大家好,我是yma16,本…...
huggingface新作品:快速和简便的训练模型
AutoTrain Advanced是一个用于训练和部署最先进的机器学习模型的工具。它旨在提供更快速、更简便的方式来进行模型训练和部署。 安装 您可以通过PIP安装AutoTrain-Advanced的Python包。请注意,为了使AutoTrain Advanced正常工作,您将需要python > 3.…...
利用鸿鹄优化共享储能的SCADA 系统功能,赋能用户数据自助分析
摘要 本文主要介绍了共享储能的 SCADA 系统大数据架构,以及如何利用鸿鹄来更好的优化 SCADA 系统功能,如何为用户进行数据自助分析赋能。 1、共享储能介绍 说到共享储能,可能不少朋友比较陌生,下面我们简单介绍一下共享储能的价值…...
noSQL语句练习
Redis练习题 string list hash结构中,每个至少完成5个命令,包含插入 修改 删除 查询,list 和hash还需要增加遍历的操作命令 1、 string类型数据的命令操作: (1) 设置键值: 127.0.0.1:63…...
Spring:Bean生命周期
Bean 生命周期生命周期 Bean 生命周期是 bean 对象从创建到销毁的整个过程。 简单的 Bean 生命周期的过程: 1.实例化(调用构造方法对 bean 进行实例化) 2.依赖注入(调用 set 方法对 bean 进行赋值) 3.初始化(手动配置 xml 文件中 bean 标签的 init-method 属性值,来指…...
Vue自定义指令
需求1:定义一个v-big指令,和v-text功能类似,但会把绑定的数值放大10倍。 需求2:定义一个v-fbind指令,和v-bind功能类似,但可以让其所绑定的input元素默认获取焦点。 自定义指令函数式v-big: &l…...
SpringBoot+JWT实现单点登录解决方案
一、什么是单点登录? 单点登录是一种统一认证和授权机制,指在多个应用系统中,用户只需要登录一次就可以访问所有相互信任的系统,不需要重新登录验证。 单点登录一般用于互相授信的系统,实现单一位置登录,其他信任的…...
网络编程(Modbus进阶)
思维导图 Modbus RTU(先学一点理论) 概念 Modbus RTU 是工业自动化领域 最广泛应用的串行通信协议,由 Modicon 公司(现施耐德电气)于 1979 年推出。它以 高效率、强健性、易实现的特点成为工业控制系统的通信标准。 包…...
手游刚开服就被攻击怎么办?如何防御DDoS?
开服初期是手游最脆弱的阶段,极易成为DDoS攻击的目标。一旦遭遇攻击,可能导致服务器瘫痪、玩家流失,甚至造成巨大经济损失。本文为开发者提供一套简洁有效的应急与防御方案,帮助快速应对并构建长期防护体系。 一、遭遇攻击的紧急应…...
零门槛NAS搭建:WinNAS如何让普通电脑秒变私有云?
一、核心优势:专为Windows用户设计的极简NAS WinNAS由深圳耘想存储科技开发,是一款收费低廉但功能全面的Windows NAS工具,主打“无学习成本部署” 。与其他NAS软件相比,其优势在于: 无需硬件改造:将任意W…...
ffmpeg(四):滤镜命令
FFmpeg 的滤镜命令是用于音视频处理中的强大工具,可以完成剪裁、缩放、加水印、调色、合成、旋转、模糊、叠加字幕等复杂的操作。其核心语法格式一般如下: ffmpeg -i input.mp4 -vf "滤镜参数" output.mp4或者带音频滤镜: ffmpeg…...
python爬虫:Newspaper3k 的详细使用(好用的新闻网站文章抓取和解析的Python库)
更多内容请见: 爬虫和逆向教程-专栏介绍和目录 文章目录 一、Newspaper3k 概述1.1 Newspaper3k 介绍1.2 主要功能1.3 典型应用场景1.4 安装二、基本用法2.2 提取单篇文章的内容2.2 处理多篇文档三、高级选项3.1 自定义配置3.2 分析文章情感四、实战案例4.1 构建新闻摘要聚合器…...
数据库分批入库
今天在工作中,遇到一个问题,就是分批查询的时候,由于批次过大导致出现了一些问题,一下是问题描述和解决方案: 示例: // 假设已有数据列表 dataList 和 PreparedStatement pstmt int batchSize 1000; // …...
汇编常见指令
汇编常见指令 一、数据传送指令 指令功能示例说明MOV数据传送MOV EAX, 10将立即数 10 送入 EAXMOV [EBX], EAX将 EAX 值存入 EBX 指向的内存LEA加载有效地址LEA EAX, [EBX4]将 EBX4 的地址存入 EAX(不访问内存)XCHG交换数据XCHG EAX, EBX交换 EAX 和 EB…...
Java毕业设计:WML信息查询与后端信息发布系统开发
JAVAWML信息查询与后端信息发布系统实现 一、系统概述 本系统基于Java和WML(无线标记语言)技术开发,实现了移动设备上的信息查询与后端信息发布功能。系统采用B/S架构,服务器端使用Java Servlet处理请求,数据库采用MySQL存储信息࿰…...
C#中的CLR属性、依赖属性与附加属性
CLR属性的主要特征 封装性: 隐藏字段的实现细节 提供对字段的受控访问 访问控制: 可单独设置get/set访问器的可见性 可创建只读或只写属性 计算属性: 可以在getter中执行计算逻辑 不需要直接对应一个字段 验证逻辑: 可以…...
C语言中提供的第三方库之哈希表实现
一. 简介 前面一篇文章简单学习了C语言中第三方库(uthash库)提供对哈希表的操作,文章如下: C语言中提供的第三方库uthash常用接口-CSDN博客 本文简单学习一下第三方库 uthash库对哈希表的操作。 二. uthash库哈希表操作示例 u…...
