html是建网站导航栏怎么做,义乌小商品市场网,买公司的网站,crm与scrm实现一个代理Kerberos环境部分组件控制台的Web服务 背景安全措施引入的问题SSO单点登录 过程整体设计路由反向代理登录会话组件代理YarnHbase 结果 背景
首先要说明下我们目前有部分集群的环境使用的是HDP-3.1.5.0的大数据集群#xff0c;除了集成了一些自定义的服务以外除了集成了一些自定义的服务以外没有对组件做二次开发。
安全措施引入的问题
生产环境部署的集群由于安全需求一般都必须打开Kerberos认证有的客户会要求同时开启https访问而且不允许关闭web访问的安全认证。这种情况下如果直接在浏览器访问组件的控制台就会提示输入用户名密码进行登录或者直接报403 而且一般来说我们用户只能拿到keytabs文件拿不到用户名密码信息一种常见的解决方式是安装KIT工具这样浏览器就可以通过keytab进行认证了不过只要涉及到在自己电脑上装多余的东西就很难受。
有的客户会提供单独的Windows堡垒机堡垒机上安装好需要的插件、客户端然后需要访问webUI的时候就登录堡垒机进行。
SSO单点登录
有的公司具备较强的研发实力能够基于组件去开发统一的单点登录程序这种确实很牛逼但是一般小公司不具备这种实力开源解决方案里唯一可选的只有Knox用过都觉得难受如果对Knox以及认证流程不熟悉的话会把自己玩死。而且Knox的方便是有前提的那就是能够配置好配置好了以后做单点访问确实很方便。
基于此当然还有很多其他的原因最终我想自己做一个简单的web服务通过反向代理等手段实现一般用户对开启Kerberos的hdfs、yarn、hbase环境的控制台访问。
过程
整体设计
功能大概分为这几点
代理hdfs、yarn、hbase三种服务的控制台UIhdfs和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 httpck 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简单写了下看着也算是有模有样了