0
点赞
收藏
分享

微信扫一扫

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

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

背景

首先要说明下我们目前有部分集群的环境使用的是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了,这个时候所有的代理连接都会通过代理服务器做转发,路由路径会被拼接到端口之后:

如果我们只是代理单一的服务,可以这样去做,反正我们不用关心路由具体去哪,只要来我8088端口的流量我都转发到9444就行了,但是此处我们需要进行路由的处理,所以传入了path变量,方便对路由的内容做处理;最后一个变量sp就是用于设置Spnego请求头的时候用的SPN,在Kerberos认证的时候,如果我请求的url对应的域名是host001的时候,我必须要指定SPN是属于host001的,具体的解释可以参照这段

因此,在每次创建代理的时候我都需要对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简单写了下,看着也算是有模有样了:
在这里插入图片描述
在这里插入图片描述

举报

相关推荐

0 条评论