beego + nginx 实现反向代理统一认证

前言

上回在 用 Nginx 的 auth_request 模块集成 LDAP 认证 里介绍了如何用 Nginx 的 auth_request 集成外部的第三方认证,以及官方 demo nginxinc/nginx-ldap-auth 的实现。

官方 demo 里直接把用户名密码往 cookie 里写的方式自然是太粗暴了一点,我们尝试重新写一个基于 session 来做验证的 demo。

Demo 基于 Golang 的 Beego 框架来实现,单纯只是因为方便而已。你可以用任何自己熟悉的方式来实现,意思是一样的。

Go 版的 nginx-ldap-auth

项目地址 -- https://github.com/shanghai-edu/nginx-ldap-auth

repo 中提供了一个 nginx.conf 的配置模板,auth_request 上回已经讲过了,不再赘述。简单介绍下路由

location /
# 对应后端的 backend 的 /,测试 demo 中的受保护路径
location /login
# 认证部分
location /logout
# 登出部分
location /captcha
# 验证码部分
location /static
# 静态部分,css, js 等
location /auth-proxy
# auth_request 的校验

Demo 我们用了 Beego 框架,非常适合快速的做一个简单的 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
路由

同样我们从路由开始看。我们通过 RESTful Controller 的方式注册了这些路由。

# 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{})

除了之前在 nginx.conf 中提及的,api/v1/ 部分提供了一些简单的控制管理的 API。(例如热重载配置)

静态文件部分,我们通过 beego.SetStaticPath 建立,然后把 css 等都丢到 static 目录里就好了

# http/http.go
beego.SetStaticPath("/static", "static")
基于 session 的认证

我们说了,生成环境上的认证控制,肯定要通过 session 来做,不可能把用户名密码直接写到 cookie 里去的,所以我们在 beego 里开启 session ,用默认的内存模式就好了。

# http/router.go
beego.BConfig.WebConfig.Session.SessionOn = true
beego.BConfig.WebConfig.Session.SessionName = "sessionID"

这样我们的 login, logout 就非常好处理了。操作 session 就好了嘛。login 就加一条 session

# 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 就删掉 session

# 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, "/")
}

/auth-proxy 上校验也就很简单了,查 session 就好了

# 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 认证

回过头来我们看 /login 的代码。首先是 LDAP 认证,Go 上的 LDAP 认证我在 用 Go 写一个轻量级的 ldap 测试工具 写过,用 go-ldap/ldap 这个库就好,很简单。

我们把表单里拿到的用户名密码去做个 LDAP 认证,如果通过了写进 session ,然后重定向给 target 就好了。

还记得 target 吗?我们在 nginx 中送在 header 里的 X-Target 字段,我们通过这个字段来决定认证之后往哪里跳转。和之前 python 的 demo 一样,我们把这个字段放在表单里,以 hidden 的方式重新提交上来以使用。

# http/controllers/login.go
target := this.Ctx.Input.Header("X-Target") 
this.Data["target"] = target
# http/views/login.tpl
<input type="hidden" name="target" value={{.target}}>

现在思考一个问题,用户认证失败的时候怎么办?
肯定会重新跳回 /login 上对不对?但是此时获得的 X-Taget 已经不是请求资源的路径了,而会变成 /login。为什么?

我们回过来看看 nginx 中的配置:

# 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;

        }

诶? 插入 X-Targetlocation/login 诶,这样说来,首次请求时获得的 X-Target/ 才比较奇怪吧?(这里 / 是受 auth_request 保护的路径),为什么?

因为在受保护路径中,我们的重定向是通过 error_page 401=200 /login 做的,这里其实是一种 nginx 内部的重定向,此时url 是不会变的。看看我们上回 demo 里的代码和运行截图:

        if url.path.startswith("/login"):
            return self.auth_form()


对不对,表单是在 /login 上的,但是我们请求资源跳过来时路由仍然是 /

nginxinc/nginx-ldap-auth 这个 demo 的实现里不需要考虑认证失败的问题,因为他的认证处理逻辑全部在 /auth-proxy 里面——就是用户名密码直接写进 cookie 里去了。然后在 nginx 内部请求 /auth-proxy 时从 cookie 里拆出用户名密码来做校验。所有认证错误产生的重定向,都由 /auth-proxy 返回 401 然后最终回到 nginxerror_page 上,变成一个循环。

我们一开始就拒绝了用户名密码进 cookie 嘛,所以这个方案不行。我们的逻辑必须放在 login 上面。

所以我们选择把 target 的信息,以 Get 的方式重新带回到认证失败的请求上去。这样我们在认证失败的 /login 上,就可以通过 Get 方式把 target 重新拿过来。

# http/controllers/login.go
this.Ctx.Redirect(302, fmt.Sprintf("/login?target=%s", target))
验证码

为了防止被暴力撞密码,基本的验证码策略还是要是做的。好在 beego直接内置了验证码的库,所以这事就很简单了。。。

首先引入 beegocache 模块和 captcha 模块

# http/controllers/login.go
import (
    "github.com/astaxie/beego/cache"
    "github.com/astaxie/beego/utils/captcha"
)

然后我们需要增加验证码初始化的代码。开启 cache,验证码的字数,长度宽度什么的。

# http/controllers/login.go
func init() {
    store := cache.NewMemoryCache()
    cpt = captcha.NewWithFilter("/captcha/", store)
    cpt.ChallengeNums = 6
    cpt.StdWidth = 120
    cpt.StdHeight = 40
}

考虑这个验证码其实挺考验眼力的。。。所以我们只在用户认证失败的时候再增加验证码。认证失败的信息通过 session 来记录

# http/controllers/login.go
    loginFailed := this.GetSession("loginFailed")
    if loginFailed != nil {
        this.Data["captcha"] = true
    }

在模板里,我们把这块根据 captcha 的值做个判断,来决定是否开启验证码。

# http/views/login.tpl
            {{if .captcha}}
            <div class="form-group">
                <div class="row">
                    <div class="col-md-6">
                        <input  class="form-control" name="captcha"  type="text" placeholder="Captcha" required>
                    </div>
                    <div class="col-md-6">
                        {{create_captcha}}
                    </div>
                </div>
            </div>
            {{end}}     

如果开启了验证码,那么拿收到的验证码做验证就好了,验证方法也给封装好了,beego 很贴心呢。

# 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 ,或者根据时间来做一些特殊的策略。

其实做法也很简单,在 /login 的时候根据请求的 IP 和时间做个判断,然后直接写入 session 或者直接拒绝访问就好了。

此外,有时候我们希望限制仅允许部分 LDAP 用户来访问,但是 LDAP 内属性又不太完整,不太方便通过 Filter 的方式来做。那么我们也可以在 login 的时候通过检查请求的用户名来直接做过滤。

最后的 control 配置就会变成这样。

    "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 默认是没有认证功能的,他的认证模块集成在 X-Pack 的高级授权里。所以我们要限制 kibana 访问的时候,通常就是限下 ip 地址完事。

现在我们可以通过 shanghai-edu/nginx-ldap-auth 来实现了。

首先我们给 kibana 增加一个路径后缀,以便于在 nginx 上区分。如下修改 kibana.yml 就可以了,修改完重启 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"

现在我们给 nginx 增加对 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/;
        }
       …………

好啦,现在我们安装我们的 nginx-ldap-auth 就好了,直接下载编译好的 release
解压,修改配置文件,启动即可

# tar -zxvf nginx-ldap-auth-0.1.3.tar.gz
# mv cfg.example.json cfg.json
# ./control start

好了,现在我们访问 kibana 时就会弹出认证 Portal 了


image.png

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


image.png
传说中的 webvpn

webvpn 本来大多指的是 ssl vpn,也就是基于 ssl (其实现在应该说 tls 了)来建立隧道的 vpn 技术。因为基于 SSL,所以大多时候仅通过浏览器就能够使用,当然通常需要装一些插件:

https://www.cisco.com/c/en/us/about/security-center/ssl-vpn-security.html
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 

参考文献

ssl-vpn-security
webvpn_百度百科
beego 简介

以上

转载授权

CC BY-SA

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,324评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,303评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,192评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,555评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,569评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,566评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,927评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,583评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,827评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,590评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,669评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,365评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,941评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,928评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,159评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,880评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,399评论 2 342

推荐阅读更多精彩内容