我们经常会看到在访问应用前,系统提示用户进行鉴权操作,或出于某些原因,内部提供公网服务的应用需要藏在一些基础的鉴权认证后,避免直接向大众公开。

除了使用各种语言来实现鉴权外,使用 Traefik 也可以简单快速的满足这些需求。

准备基础的 Web 服务Demo

我们先以 whoami 为例,启动一个 Web 服务,配置如下:

version: '3'

services:

  whoami:
    image: containous/whoami
    labels:
      - "traefik.enable=true"
      - "traefik.docker.network=traefik"
	  # 参考 https://soulteary.com/2020/12/02/easier-way-to-use-traefik-2.html
      - "traefik.http.routers.test-auth-web.middlewares=https-redirect@file"
      - "traefik.http.routers.test-auth-web.entrypoints=http"
      - "traefik.http.routers.test-auth-web.rule=Host(`whoami.lab.com`, `whoami.lab.io`)"
      - "traefik.http.routers.test-auth-ssl.entrypoints=https"
      - "traefik.http.routers.test-auth-ssl.tls=true"
      - "traefik.http.routers.test-auth-ssl.rule=Host(`whoami.lab.com`, `whoami.lab.io`)"

    networks:
      - traefik

networks:
  traefik:
    external: true

为了进一步保障数据传输安全,推荐使用 HTTPS 进行数据交互。可以使用前文中的 https-redirect 中间件,将 HTTP 请求自动转发到 HTTPS 协议上。

将配置保存为 docker-compose.yml 后,使用 docker-compose up -d 启动服务后,可以看到类似下面的页面。

服务启动之后

Basic Auth

使用 Traefik 为应用添加 Basic Auth 非常简单,只需要定义一个包含 basicAuth 用户名密码的中间件声明,然后在需要使用 Basic Auth 验证的服务路由上引用它即可,像是下面这样:

labels:
...
  - "traefik.http.middlewares.test-auth.basicauth.users=test:$$apr1$$H6uskkkW$$IgXLP6ewTrSuBkTrqE8wj/,test2:$$apr1$$d9hr9HBB$$4HxwgUir3HP4EsggP/QNo0"
  - "traefik.http.routers.test-auth-ssl.middlewares=test-auth@docker"
...

重新使用 docker-compose up -d 启动服务后,会看到一个弹出框,要求我们输入密码。

使用 Basic Auth 之后

随便输入账号密码,或者取消输入,会获得 401 Unauthorized 的错误提示,如果我们输入账号和密码为 test 的内容,点击确定,则可以正常看到 Demo 服务的页面内容。

如何生成 Basic Auth 账号密码

如果你是 macOS 用户,系统默认携带了 apache htpasswd 工具,可以直接生成上面配置中的账号密码。

htpasswd -nb test test
test:$apr1$lH3nyBaa$/wCu0V3.1kYdpZPHRbiyv/

如果你的系统中找不到这个命令行,你也不想安装 apache utils,那么可以使用 Docker 来生成账号密码:

docker run --rm -it --entrypoint /usr/local/apache2/bin/htpasswd httpd:alpine -nb test test

但是需要注意的是,在 compose 中使用的话,密码中的 $ 需要使用 $$ 来进行替换,解决转义问题。

如何配置多个账号密码

配置多个账号密码可以使用两种方式:

  • 使用包含多个账号的配置文件
  • 使用包含多个账号的环境变量

如果你有多个应用都希望使用 Basic Auth 来进行基础保护,那么可以在 Traefik 的动态配置中添加这个“验证中间件”,如果你还不了解如何配置 Traefik,可以参考这篇文章

使用文件来定义、管理用户密码,需要声明下面的内容到 labels 字段中:

  - "traefik.http.middlewares.test-auth.basicauth.usersfile=/path/to/my/usersfile"

并在一个文件中使用换行来保存我们生成的用户名和密码:

test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/
test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0

如果你希望每个应用有其独立的账号密码,不希望用户账号混在一起存放、管理,那么可以使用环境变量和项目环境配置文件来解决这个问题。

先定义一个读取环境变量的验证中间件:

  - "traefik.http.middlewares.test-auth.basicauth.users=$AUTH_USER_LIST"

然后在 compose 同级目录中创建一个 .env 文件,以英文逗号为分隔符,传入生成的用户鉴权信息即可:

test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/,test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0

手动选择是否要将验证信息透传

默认情况下,当我们登录后,Traefik 会将授权后的验证头发送至后方的服务,我们在 header 中能看到类似下面的信息:

Authorization: Basic dGVzdDp0ZXN0

有一些应用支持使用请求头中的数据作为鉴权登录信息,而我们定义的用户信息很可能和系统的鉴权信息是不同的(也不推荐使用这个方案做为多数情况下应用鉴权方案),所以造成应用无法正常登陆,所以此刻我们要将这个鉴权操作的作用范围做一个限制,让它仅仅生效在首次访问应用前,流量到达 Traefik 时:

  - "traefik.http.middlewares.test-auth.basicauth.removeheader=true"

在添加了上面内容后,我们可以看到输入账号密码后,Traefik 不会再进行 Authorization 请求头的透传。

用还是不用,这是个问题

虽然相对详细的介绍了 Basic Auth,但是并不推荐大范围或者将其作为唯一鉴权手段。

因为在标准规范中,它使用 Base64 对用户名密码进行编码,然后传递给其他应用。众所周知 Base64 是可逆编码的,所以我们使用 Basic Auth 来保护应用其实并不安全,比如我们将前文中的 Authorization: Basic dGVzdDp0ZXN0 最后一段内容 dGVzdDp0ZXN0 进行解码,能够直接得到明文的 test:test

但是如果你的系统未公开暴露于网络,并且使用人员有限,或提供开放服务,但是单纯不希望被搜索引擎抓取,可以在应用前端套一层 Basic Auth,相比较用户、爬虫能够直接访问到机器,这样还能够节约大量不必要的计算资源浪费。

不要单纯听从网络人云亦云,一刀切完全不用,克制的使用在适合的场景下,事半功倍。

Digest Auth

在详细介绍了 Basic Auth 后,我们来了解 Digest Auth 会轻松不少。前文提到 Basic Auth 存在一些安全问题,所以有了这个“升级版本”,支持使用 MD5 / SHA 系列加密算法来替换简单的 Base64 “加密”。

有一件有意思的事情,目前 Mozilla 社区 buglist 中 不支持 SHA 加密 的反馈已经维持打开 12 年,最近几天有一位仁兄提了 PR,或许火狐浏览器不久之后可以支持使用 Digest Auth(SHA)。

Traefik 中的 Digest Auth 中间件和 Basic Auth 中间件使用基本一致,所以你基本可以将上文中类似下面配置中的 basicauth 替换为 digestauth 来达到相同目的:

# 使用 Basic Auth
 - "traefik.http.middlewares.test-auth.basicauth.users=$AUTH_USER_LIST"
# 使用 Digital Auth
- "traefik.http.middlewares.test-auth.digestauth.users=$AUTH_USER_LIST"

如何生成 Digital Auth 账号密码

如果你是 macOS 用户,系统同样默认携带了 apache htdigest 工具,可以直接生成上面配置中的账号密码,不过相比 htapasswd 使用起来会复杂一些,需要在使用过程中手动输入账号密码,因为默认工具会生成文件,我们的场景下,其实不一定需要它创建文件,所以这里将输出指向 /dev/stdout 即可在运行的完毕展示结果。

htdigest -c /dev/stdout test test                                                   
Adding password for test in realm test.
New password: 
Re-type new password: 

test:test:3c7ca779a9504185a7b86c8b1c388e90

类似的,如果你的系统中找不到这个命令行,你也不想安装 apache utils,那么可以使用 Docker 来生成账号密码:

docker run --rm -it --entrypoint /usr/local/apache2/bin/htdigest httpd:alpine -c /dev/stdout test test 
Adding password for test in realm test.
New password: 
Re-type new password: 

test:test:3c7ca779a9504185a7b86c8b1c388e90

用还是不用,是个问题吗

上文提到,目前浏览器对于这个类型的验证有各种各样的“兼容性”问题,有不支持 SHA1 的,有不支持 SHA 256 的,有只支持 MD5 的…而且坦白说,目前如果使用摘要算法做鉴权,SHA 256 以下其实差异不大,SHA 系列相比 MD5 的好处目前来看仅剩硬件(CPU指令集)计算加速,以及攻击成本更高一些,相对更不易碰撞。

如果你想选择使用 Digest 作为鉴权,同样是不建议的,如果有 Basic Auth 最后一小节中的需求理由,可以直接使用 Basic Auth。

Forward Auth

Forward Auth 相比上面两种方案,其实有质的不同,上面两种加密中间件本质提供的是RFC标准下的交互协议,而这个中间件提供的一个通用的鉴权业务能力:你可以自由对接任何你自己的鉴权系统、用户数据来源,甚至实现一个通用的 SSO 授权页面。

限于篇幅,这部分内容,我们放到下篇来聊。

最后

原本我只是想聊聊基于开源代码快速搭建适合用于个人、团队、基础设施使用的 SSO,万万没想到,需要展开铺垫如此这么多前置知识。

–EOF