beego + nginx 实现反向代理统一认证
前言
上回在 里介绍了如何用 Nginx 的 auth_request 集成外部的第三方认证,以及官方 demo 的实现。
官方 demo 里直接把用户名密码往 cookie 里写的方式自然是太粗暴了一点,我们尝试重新写一个基于 session 来做验证的 demo。
基于 Golang 的 框架来实现,单纯只是因为方便而已。你可以用任何自己熟悉的方式来实现,意思是一样的。
Go 版的 nginx-ldap-auth
中提供了一个 的配置模板, 上回已经讲过了,不再赘述。简单介绍下路由
location /
# 对应后端的 backend 的 /,测试 demo 中的受保护路径
location /login
# 认证部分
location /logout
# 登出部分
location /captcha
# 验证码部分
location /static
# 静态部分,css, js 等
location /auth-proxy
# auth_request 的校验
Demo 我们用了 框架,非常适合快速的做一个简单的 Demo 示例。目录结构如下:
# tree
.
├── cfg.example.json
├── cfg.json
├── control
├── g
│ ├── cfg.go
│ └── const.go
├── http
│ ├── controllers
│ │ ├── auth-proxy.go
│ │ ├── control.go
│ │ ├── default.go
│ │ ├── error.go
│ │ ├── login.go
│ │ └── logout.go
│ ├── http.go
│ └── router.go
├── LICENSE
├── main.go
├── nginx.conf
├── README_CN.MD
├── README.MD
├── static
│ ├── css
│ │ ├── bootstrap.min.css
│ │ ├── ie10-viewport-bug-workaround.css
│ │ └── signin.css
│ ├── favicon.ico
│ └── js
│ ├── ie10-viewport-bug-workaround.js
│ └── ie-emulation-modes-warning.js
├── utils
│ ├── ipCheck.go
│ ├── ip_test.go
│ ├── ldap.go
│ ├── ldap_test.go
│ ├── time_check.go
│ ├── time_test.go
│ └── utils.go
└── views
├── deny.tpl
├── direct.tpl
└── login.tpl
路由
同样我们从路由开始看。我们通过 的方式注册了这些路由。
# http/router.go
beego.Router("/", &controllers.MainController{})
beego.Router("/login", &controllers.LoginController{})
beego.Router("/logout", &controllers.LogoutController{})
beego.Router("/auth-proxy", &controllers.AuthProxyController{})
beego.Router("/api/v1/:control", &controllers.ControlController{})
除了之前在 中提及的, 部分提供了一些简单的控制管理的 API。(例如热重载配置)
静态文件部分,我们通过 建立,然后把 等都丢到 目录里就好了
# http/http.go
beego.SetStaticPath("/static", "static")
基于 session 的认证
我们说了,生成环境上的认证控制,肯定要通过 来做,不可能把用户名密码直接写到 里去的,所以我们在 里开启 ,用默认的内存模式就好了。
# http/router.go
beego.BConfig.WebConfig.Session.SessionOn = true
beego.BConfig.WebConfig.Session.SessionName = "sessionID"
这样我们的 login, logout 就非常好处理了。操作 就好了嘛。login 就加一条
# http/controllers/login.go
func (this *LoginController) Post() {
this.Ctx.Request.ParseForm()
username := this.Ctx.Request.Form.Get("username")
password := this.Ctx.Request.Form.Get("password")
target := this.Ctx.Request.Form.Get("target")
err := utils.LDAP_Auth(g.Config().Ldap, username, password)
if err == nil {
this.SetSession("uname", username)
this.Ctx.Redirect(302, target)
}
}
logout 就删掉
# http/controllers/logout.go
func (this *LogoutController) Get() {
clientIP := this.Ctx.Input.IP()
uname := this.GetSession("uname")
if uname != nil {
this.DelSession("uname")
}
this.Ctx.Redirect(302, "/")
}
上校验也就很简单了,查 就好了
# http/controllers/auth-proxy.go
func (this *AuthProxyController) Get() {
this.Ctx.Output.Header("Cache-Control", "no-cache")
uname := this.GetSession("uname")
if uname == nil {
this.Ctx.Abort(401, "401")
return
}
this.Ctx.Output.Body([]byte("ok"))
}
login 认证
回过头来我们看 的代码。首先是 LDAP 认证,Go 上的 LDAP 认证我在 写过,用 这个库就好,很简单。
我们把表单里拿到的用户名密码去做个 LDAP 认证,如果通过了写进 ,然后重定向给 就好了。
还记得 吗?我们在 中送在 里的 字段,我们通过这个字段来决定认证之后往哪里跳转。和之前 python 的 demo 一样,我们把这个字段放在表单里,以 的方式重新提交上来以使用。
# http/controllers/login.go
target := this.Ctx.Input.Header("X-Target")
this.Data["target"] = target
# http/views/login.tpl
现在思考一个问题,用户认证失败的时候怎么办?
肯定会重新跳回 上对不对?但是此时获得的 已经不是请求资源的路径了,而会变成 。为什么?
我们回过来看看 中的配置:
# nginx.conf
location / {
auth_request /auth-proxy;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $http_host;
error_page 401 =200 /login;
proxy_pass http://backend/;
}
location /login {
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $http_host;
proxy_set_header X-Target $request_uri;
proxy_pass http://backend/login;
}
诶? 插入 的 是 诶,这样说来,首次请求时获得的 是 才比较奇怪吧?(这里 是受 保护的路径),为什么?
因为在受保护路径中,我们的重定向是通过 做的,这里其实是一种 内部的重定向,此时 是不会变的。看看我们上回 demo 里的代码和运行截图:
if url.path.startswith("/login"):
return self.auth_form()

对不对,表单是在 上的,但是我们请求资源跳过来时路由仍然是 。
这个 demo 的实现里不需要考虑认证失败的问题,因为他的认证处理逻辑全部在 里面——就是用户名密码直接写进 里去了。然后在 内部请求 时从 里拆出用户名密码来做校验。所有认证错误产生的重定向,都由 返回 401 然后最终回到 的 上,变成一个循环。
我们一开始就拒绝了用户名密码进 嘛,所以这个方案不行。我们的逻辑必须放在 上面。
所以我们选择把 的信息,以 的方式重新带回到认证失败的请求上去。这样我们在认证失败的 上,就可以通过 方式把 重新拿过来。
# http/controllers/login.go
this.Ctx.Redirect(302, fmt.Sprintf("/login?target=%s", target))
验证码
为了防止被暴力撞密码,基本的验证码策略还是要是做的。好在 直接内置了验证码的库,所以这事就很简单了。。。
首先引入 的 模块和 模块
# http/controllers/login.go
import (
"github.com/astaxie/beego/cache"
"github.com/astaxie/beego/utils/captcha"
)
然后我们需要增加验证码初始化的代码。开启 ,验证码的字数,长度宽度什么的。
# http/controllers/login.go
func init() {
store := cache.NewMemoryCache()
cpt = captcha.NewWithFilter("/captcha/", store)
cpt.ChallengeNums = 6
cpt.StdWidth = 120
cpt.StdHeight = 40
}
考虑这个验证码其实挺考验眼力的。。。所以我们只在用户认证失败的时候再增加验证码。认证失败的信息通过 来记录
# http/controllers/login.go
loginFailed := this.GetSession("loginFailed")
if loginFailed != nil {
this.Data["captcha"] = true
}
在模板里,我们把这块根据 的值做个判断,来决定是否开启验证码。
# http/views/login.tpl
{{if .captcha}}
{{create_captcha}}
{{end}}
如果开启了验证码,那么拿收到的验证码做验证就好了,验证方法也给封装好了, 很贴心呢。
# http/controllers/login.go
if _, ok := this.Ctx.Request.Form["captcha"]; ok {
if !cpt.VerifyReq(this.Ctx.Request) {
beego.Notice(fmt.Sprintf("%s - - [%s] Login Failed: Captcha Wrong", clientIP, logtime))
this.Ctx.Redirect(302, fmt.Sprintf("/login?loginFailed=3&target=%s", target))
return
}
}
XSRF
由于我们的请求要通过 cookie 来校验,那么开启 XSRF 就很有必要了。beego 可以很方便的开启 XSRF ——
特殊策略
上回我们还说过,有时候我们需要根据 IP ,或者根据时间来做一些特殊的策略。
其实做法也很简单,在 的时候根据请求的 IP 和时间做个判断,然后直接写入 或者直接拒绝访问就好了。
此外,有时候我们希望限制仅允许部分 LDAP 用户来访问,但是 LDAP 内属性又不太完整,不太方便通过 的方式来做。那么我们也可以在 的时候通过检查请求的用户名来直接做过滤。
最后的 配置就会变成这样。
"control":{
"ipAcl":{
"deny":["192.168.2.10","192.168.0.0/24","192.168.1.0-192.168.1.255"],
"direct":[]
},
"timeAcl":{
"deny":["00:00-8:00","17:00-23:59"],
"direct":[]
},
"allowUser":["user1"]
},
应用示例
ELK(Kibana) + 认证
众所周知,ELK 中的 Kibana 默认是没有认证功能的,他的认证模块集成在 的高级授权里。所以我们要限制 kibana 访问的时候,通常就是限下 ip 地址完事。
首先我们给 kibana 增加一个路径后缀,以便于在 上区分。如下修改 就可以了,修改完重启 kibana 。
# Enables you to specify a path to mount Kibana at if you are running behind a proxy. This only affects
# the URLs generated by Kibana, your proxy is expected to remove the basePath value before forwarding requests
# to Kibana. This setting cannot end in a slash.
server.basePath: "/kibana"
现在我们给 增加对 Kibana 的路径保护配置。
upstream elk {
server elk.local:5601;
}
server {
…………
location /kibana/ {
auth_request /auth-proxy;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $http_host;
error_page 401 =200 /login;
proxy_pass http://elk/;
}
…………
解压,修改配置文件,启动即可
# tar -zxvf nginx-ldap-auth-0.1.3.tar.gz
# mv cfg.example.json cfg.json
# ./control start
好了,现在我们访问 kibana 时就会弹出认证 Portal 了

认证之后,正常访问进入 kibana,美滋滋。

传说中的 webvpn
webvpn 本来大多指的是 ssl vpn,也就是基于 ssl (其实现在应该说 tls 了)来建立隧道的 vpn 技术。因为基于 SSL,所以大多时候仅通过浏览器就能够使用,当然通常需要装一些插件:
Today, this SSL/TLS function exists ubiquitously in modern web browsers. Unlike traditional IP Security (IPSec) remote-access VPN technology, which requires installation of IPSec client software on a client machine before a connection can be established, users typically do not need to install client software in order to use SSL VPN. As a result, SSL VPN is also known as “clientless VPN” or “Web VPN”.
而现在 webvpn 通常特指无需任何插件或客户端,纯 "web" 式访问的 web vpn,甚至已经有了 。
之所以能实现无浏览器依赖和无插件依赖,是因为这种模式真的是纯 "web" 的,也就是说只能使用 "webvpn" 来访问 web 资源,你想 vpn 上来然后开个 ssh 或者 mstsc 上的是没可能的。(业内有利用 webtty 这样的方式来实现这种需求,这是后话)。
在我了解的一些产品里,他的具体实现就是个叠加了认证的反向代理。这很好理解,既然本质上是反向代理,自然就无浏览器依赖了,因为最终访问的还是原来的网站嘛。
而且我们可以利用反向代理上的控制策略,还能顺便实现诸如特定时间开启日志,对特定 IP 地址(比如内网地址)直通访问等等,来灵活的实现一些复杂的需求。
2018/07/03 16:43:33.395 [N] 192.168.95.65 - 192.168.95.65 [03/Jul/2018 04:43:33] Login Successed: Direct IP
2018/07/03 16:44:08.153 [N] 127.0.0.1 - - [03/Jul/2018 04:44:08] Config Reloaded
2018/07/03 16:44:14.049 [N] 192.168.95.65 - 192.168.95.65 [03/Jul/2018 04:44:14] Logout Successed
2018/07/03 16:44:14.839 [N] 192.168.95.65 - - [03/Jul/2018 04:44:14] Login Failed: IP 192.168.95.65 is not allowed
2018/07/03 16:44:57.971 [N] 127.0.0.1 - - [03/Jul/2018 04:44:57] Config Reloaded
2018/07/03 16:45:00.398 [N] 192.168.95.65 - timeDirect [03/Jul/2018 04:45:00] Login Successed: Direct Time
2018/07/03 16:45:29.570 [N] 127.0.0.1 - - [03/Jul/2018 04:45:29] Config Reloaded
2018/07/03 16:45:32.980 [N] 192.168.95.65 - timeDirect [03/Jul/2018 04:45:32] Logout Successed
2018/07/03 16:45:32.991 [N] 192.168.95.65 - - [03/Jul/2018 04:45:32] Login Failed: This Time is not allowed
参考文献
以上
行文有微调。