0
点赞
收藏
分享

微信扫一扫

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 了



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


传说中的 webvpn

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

而现在 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

举报

相关推荐

0 条评论