【Go】实现一个代理Kerberos环境部分组件控制台的Web服务

news2024/11/24 9:34:58

实现一个代理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 = path
		req.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.Director
	proxy.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.Path
		if r.StatusCode == 307 {
			// 307的情况下是到了备的yarn节点
			// 判断下是不是yarn
			if 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 = v
			break
		}
	}
}

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 = v
				lock.Unlock()
			}
		}
		return nil
	}

这部分的处理逻辑只需要在设置代理的函数中进行添加即可。

结果

最终在各种修修补补下,完成了控制台程序的整体逻辑,并且能够正常使用了,前端使用BootStarp简单写了下,看着也算是有模有样了:
在这里插入图片描述
在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/766973.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

python opencv 级联Haar多目标检测

一、基于OpenCV的haar分类器实现笑脸检测 1、Haar分类器介绍 🚀Haar分类器是一种基于机器学习的目标检测算法,它使用Haar特征描述图像中的目标。Haar特征是基于图像亮度的局部差异计算得出的,可以用来描述目标的边缘、角落和线条等特征。 使用…

Jenkins (一)

Jenkins (一) Docker Jenkins 部署 一. 安装 jenkins $ mkdir -p /home/tester/data/docker/jenkins $ vim jenkins:lts-jdk11.sh./jenkins:lts-jdk11.sh 内容 #! /bin/bash mkdir -p /home/tester/data/docker/jenkins/jenkins_homesudo chown -R 1000:1000 /home/tester/da…

利用LightHouse进行合理的页面性能优化,看这一篇就够了!

利用LightHouse进行合理的页面性能优化,看这一篇就够了! 前言一. Lighthouse下载1.1 相关指标概念1.2 Lighthouse 优化建议 二. 跟着 Lighthouse 进行性能优化2.1 Enable text compression 开启文本压缩2.2 Resize images 重新设定合适大小的图片2.3 Eli…

Java阶段五Day08

Java阶段五Day08 文章目录 Java阶段五Day08内容回顾学习内容目的自动配置原理SPI-API:一对类似的概念 自定义Starter属性配置问题 网关组件SpringCloud Gateway网关架构微服务网关介绍Spring Cloud Gateway(技术选型)网关转发入门案例明确案例需求实现案…

学习系统编程No.31【多线程互斥与同步】

引言: 北京时间:2023/7/16/14:32,摆烂至今,在耍这方面,谁能比我行,哈哈哈,乐观!欠了一堆课要补,等我们把线程相关知识学完,对于系统编程方面我们搞定的就差不…

二分类结局变量Logistic回归临床模型预测——分训练集和测试集(完结)

1. 介绍 2. 基线特征 3. 单因素多因素logistic回归分析及三线表 4. 构建临床列线图模型 5. 模型评价 6. 外部数据集验证 7. 另一种发文章的办法,分训练集和测试集,分析上述3-6节的内容 这里就讲一下如何分训练集和测试集,其余的步骤和之前是一样的,分训练集和测试集用…

Fiddler网络调试器,抓包工具供大家学习研究参考

Fiddler 是一个 http 协议调试代{过}{滤}理工具,它能够记录并检查所有你的电脑和互联网之间的 http 通讯,设置断 点,查看所有的“进出”Fiddler 的数据(指 cookiehtmljscss等文件)。 Fiddler 要比其他的网络调试器要更加简单,因为…

从Vue2到Vue3【零】——Vue3简介

系列文章目录 内容链接从Vue2到Vue3【零】Vue3简介及创建 文章目录 系列文章目录前言一、Vue3的发布带来了什么1.1 性能提升1.2 源码升级1.3 支持TypeScript1.4 新特性1.5 支持 vue3 的UI组件库 二、创建Vue3.0工程2.1 什么是Vite2.2 利用Vite创建Vue3.0工程2.3 利用vue-cli脚…

下载最新版 VC_redist.x86.exe / VC_redist.x64.exe for Visual Studio 2015, 2017, 2019, and 2022

目录 1、如何处理程序需要的C/C运行时库? 2、VC_redist.x64.exe的主界面以及支持的命令行参数 3、到微软官网上下载最新版本的VC_redist.x86.exe / VC_redist.x64.exe VC常用功能开发汇总(专栏文章列表,欢迎订阅,持续更新...&a…

总结几个GPT的超实用之处【附带Python案例】

GPT(Generative Pre-trained Transformer)是人工智能领域中最受欢迎的预训练语言模型之一,由OpenAI开发。该模型使用深度学习技术,可以自动生成各种文本,如文章、日记、小说等。GPT的超实用之处有很多,下面…

【Ubuntu 20.04LTS系统】安装CUDA11.8、cuDNN,可进行CUDA版本切换

Ubuntu 20.04LTS系统安装CUDA11.8、cuDNN,可进行CUDA版本切换 1. 更改为清华源并更新软件列表和依赖项2. 安装CUDA3. 安装cuDNN4. CUDA版本切换 1. 更改为清华源并更新软件列表和依赖项 https://mirrors.tuna.tsinghua.edu.cn/help/ubuntu/ # 默认注释了源码镜像以提…

java项目之毕业生就业信息管理系统(ssm+mysql+jsp)

风定落花生,歌声逐流水,大家好我是风歌,混迹在java圈的辛苦码农。今天要和大家聊的是一款基于ssm的毕业生就业信息管理系统。技术交流和部署相关看文章末尾! 开发环境: 后端: 开发语言:Java …

哇~真的是你呀!今天是LINUX中的SSH服务。

目录 前言 一、概述 二、身份验证机制及验证过程 三、加密机制 四、基本参数 五、基本操作 六、操作 前言 SSH(Secure Shell)是一种加密网络协议,用于远程登录和安全传输数据。在Linux系统中,SSH服务是一种常见的远程管理工具&am…

python copy.copy与copy.deepcopy 区别

结论 b a.copy(): 浅拷贝, a 和 b 是一个独立的对象,但他们的子对象还是指向统一对象(是引用)。 b copy.deepcopy(a): 深拷贝, a 和 b 完全拷贝了父对象及其子对象,两者是完全独立的。 import copy origin [1, 2, [3, 4]]…

CS拒绝连接,Cobalt Strike连接失败,Cobalt Strike使用方法(二)

本文主要介绍Cobalt Strike的界面及使用方法。 界面 菜单介绍 Cobalt Strike New Connection // 新建连接,可连接多个服务器端 Preferences // 设置Cobal Strike界面、控制台、以及输出报告样式、TeamServer连接记录 Visualization // 主要展…

精益生产有哪些管理工具?

精益生产有哪些管理工具? 一、什么是精益生产 智能制造是落实我国制造强国战略的重要举措,加快推进智能制造,是加速我国工业化和信息化深度融合、推动制造业供给侧结构性改革的重要着力点,对重塑我国制造业竞争新优势具有重要意义…

java项目---2048

目录 游戏介绍 游戏玩法 全代码 (1)Main类: (2)GamePanel类 (3)Card类 (4)GameFrame类 项目设计思路 1.绘制一个窗口 2.创建菜单 3.创建所有空白卡片 &…

项目经理在日常管理工作中需要注意什么?

项目管理的核心在于以项目经理为主导的团队协作,项目经理需要管理团队成员,保障团队的产出和效能。因此,项目经理需要具备多种能力,如沟通能力、管理能力、洞察人心的能力等,才能有效推动项目进展。 1、制定计划是项…

如何更改idea的背景,以及显示某些属性

idea项目无法正常显示的问题 在idea中右侧的属性都不存在的解决方法,或左边的不显示,我浏览了很多的文章才找到的解决办法,在这里分享给大家 就可以显示出来了,因为我原来已经显示了,当我取消后就看不到了 修改背景 1.先进入到设置的页面 选择完成后点击ok把设置关了就设置成功…

Java开发 - 深入理解Redis哨兵机制原理

前言 Redis的主从、哨兵模式、集群模式,在前文中都已经有了详细的搭建流程,可谓是手把手教程,也得到了很多朋友的喜欢。由于前文偏向于应用方面,就导致了理论知识的匮乏,我们可能会用了,但却不明所以&…