使用 TinyAuth 为您的 Docker 服务添加轻量级认证

导言

在微服务和容器化架构中,我们经常需要将内部服务或管理后台暴露在网络上,但又不希望它们被公开访问。为每个服务都实现一套用户认证和权限管理逻辑,不仅工作量巨大,而且难以维护。一个更优雅的解决方案是使用认证网关(Authentication Gateway)。

本文将介绍一种轻量级的认证网关实现思路,我们称之为 TinyAuth。它将与 Nginx 反向代理结合,为任何后端的 Docker 服务提供简单、高效、非侵入式的认证保护。我们将深入探讨其原理、优缺点、适用场景,并提供一套完整的、可操作的部署步骤。


认证网关模式与 TinyAuth 原理

原理概述

认证网关的核心思想是将认证逻辑从业务服务中剥离出来,前置到一个统一的入口(如反向代理)中。当用户请求到达时,反向代理会先将请求的凭证(如 Authorization 头)转发给一个专门的认证服务进行验证。只有当认证服务确认凭证有效时,反向代理才会将原始请求转发给后端的业务服务。否则,直接拒绝请求。

在这个架构中,我们有三个关键组件:

  1. Nginx (反向代理): 作为流量入口,负责拦截所有请求。它使用 auth_request 模块来实现认证逻辑的外部化。
  2. TinyAuth (认证服务): 一个极简的、独立的Web服务。它的唯一职责是接收来自 Nginx 的认证请求,判断凭证是否有效,并返回相应的 HTTP 状态码(200 表示成功,401/403 表示失败)。
  3. 后端业务服务 (Your Docker Service): 真正的业务应用,它可以是任何语言、任何框架构建的 Docker 服务。它完全不需要关心认证逻辑,可以假定所有到达的请求都是可信的。

Nginx auth_request 模块

这个模式得以实现的关键是 Nginx 的 ngx_http_auth_request_module 模块。通过在 location 块中配置 auth_request 指令,Nginx 会在处理主请求之前,先向指定的子请求(即我们的 TinyAuth 服务)发起一个认证请求。

  • 如果子请求返回 2xx 状态码,Nginx 认为认证成功,继续处理主请求。
  • 如果子请求返回 401403,Nginx 认为认证失败,立即中断主请求,并将子请求的 401/403 状态码返回给客户端。

这种机制巧妙地将认证逻辑解耦,使得后端服务可以保持纯净。


适用场景

TinyAuth 这种轻量级认证网关并非万能,它特别适用于以下场景:

  • 内部工具和仪表盘: 保护公司内部使用的监控系统(如 Prometheus, Grafana)、管理后台、CI/CD 界面等。
  • 开发和测试环境: 为临时暴露的开发或测试环境提供快速、便捷的访问控制。
  • 简单的 API 保护: 当你的 API 只需要一个全局的 API 密钥或简单的 Token 进行认证,而不需要复杂的用户、角色和权限系统时。
  • 遗留系统: 为那些没有内置认证功能或难以修改的遗留系统添加一层安全保护。

注意: 对于需要复杂用户管理、角色权限(RBAC)、单点登录(SSO)或符合 OAuth2/OIDC 等标准协议的生产级应用,建议使用更成熟的解决方案,如 Authelia, Authentik, Keycloak 或云厂商提供的 IAM 服务。


优缺点分析

优点

  • 逻辑解耦: 业务服务无需编写任何认证代码,可以专注于核心业务逻辑,更加纯粹和易于维护。
  • 集中式认证: 认证逻辑统一由 TinyAuth 和 Nginx 管理,方便统一更新和审计。可以轻松地为多个不同的后端服务提供相同的认证保护。
  • 语言无关: 无论你的后端服务是 Java, Python, Go, Node.js 还是 PHP,都可以被这种模式保护。
  • 部署简单: 相对于复杂的认证框架,TinyAuth 的概念和实现都非常简单,可以快速部署和应用。
  • 高性能: Nginx 处理 auth_request 的性能极高,认证服务本身逻辑简单,对请求的额外延迟非常小。

缺点

  • 功能有限: TinyAuth 只解决“是否允许访问”的问题,不包含用户管理、角色管理、权限控制等复杂功能。
  • 性能开销: 虽然开销很小,但每个请求都增加了一次到认证服务的额外网络调用(通常是内网调用),在高并发场景下需要评估其影响。
  • 单点故障: 认证网关(Nginx 和 TinyAuth)成为了所有受保护服务的入口,如果它出现故障,将导致所有后端服务不可用。需要确保其高可用性。

详细部署步骤

下面,我们将通过 docker-compose 来搭建一整套环境,包括 Nginx, TinyAuth 服务和一个示例的后端应用。

1. 项目结构

首先,创建如下的项目目录结构:

1
2
3
4
5
6
7
8
9
10
/tinyauth-demo
├── docker-compose.yml
├── nginx
│ └── nginx.conf
├── tinyauth
│ ├── Dockerfile
│ └── app.py
└── my-app
├── Dockerfile
└── app.py

2. 创建后端业务服务 (my-app)

这是一个简单的 Flask 应用,代表我们需要保护的业务服务。

my-app/app.py

1
2
3
4
5
6
7
8
9
10
11
12
from flask import Flask, request

app = Flask(__name__)

@app.route('/')
def home():
# 业务服务可以从特定的头获取用户信息(如果认证服务传递了的话)
user_info = request.headers.get('X-Authenticated-User', 'anonymous')
return f"Hello from my-app! You are authenticated as: {user_info}\n"

if __name__ == '__main__':
app.run(host='0.0.0.0', port=5000)

my-app/Dockerfile

1
2
3
4
5
6
7
8
9
FROM python:3.9-slim

WORKDIR /app

RUN pip install Flask

COPY app.py .

CMD ["python", "app.py"]

3. 创建 TinyAuth 认证服务

这也是一个 Flask 应用,逻辑非常简单:检查 Authorization 头是否存在且等于一个预设的秘密 Token。

tinyauth/app.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import os
from flask import Flask, request, abort

app = Flask(__name__)

# 从环境变量获取合法的 Token,更安全
VALID_TOKEN = os.environ.get('AUTH_TOKEN', 'my-secret-token')

@app.route('/auth')
def auth():
auth_header = request.headers.get('Authorization')
if auth_header and auth_header == f'Bearer {VALID_TOKEN}':
# 认证成功,可以设置一些头信息返回给 Nginx,再由 Nginx 传递给后端
response = app.make_response('OK')
response.headers['X-Authenticated-User'] = 'service-user'
return response
else:
# 认证失败
abort(401)

if __name__ == '__main__':
app.run(host='0.0.0.0', port=5001)

tinyauth/Dockerfile

1
2
3
4
5
6
7
8
9
FROM python:3.9-slim

WORKDIR /app

RUN pip install Flask

COPY app.py .

CMD ["python", "app.py"]

4. 配置 Nginx

这是整个流程的核心,配置 auth_request 指令。

nginx/nginx.conf

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
worker_processes 1;

events {
worker_connections 1024;
}

http {
# 定义上游服务
upstream my_app_server {
server my-app:5000;
}

# 定义认证服务
upstream tinyauth_server {
server tinyauth:5001;
}

server {
listen 80;

location / {
# 1. 设置认证请求的地址
auth_request /_auth;

# 2. 将认证服务返回的头信息传递给后端服务
auth_request_set $authenticated_user $upstream_http_x_authenticated_user;
proxy_set_header 'X-Authenticated-User' $authenticated_user;

# 3. 如果认证成功,将请求代理到后端业务服务
proxy_pass http://my_app_server;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}

# 内部 location,用于认证
location = /_auth {
internal; # 只能被内部请求(如 auth_request)调用

# 4. 将请求代理到认证服务
proxy_pass http://tinyauth_server/auth;
proxy_pass_request_body off; # 认证请求不需要 body
proxy_set_header Content-Length "";
proxy_set_header X-Original-URI $request_uri;

# 5. 将原始请求的 Authorization 头传递给认证服务
proxy_set_header Authorization $http_authorization;
}
}
}

5. 编排 Docker Compose

最后,使用 docker-compose.yml 将所有服务串联起来。

docker-compose.yml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
version: '3.8'

services:
my-app:
build: ./my-app
container_name: my-app
networks:
- app-net

tinyauth:
build: ./tinyauth
container_name: tinyauth
environment:
# 将 Token 设置为环境变量
- AUTH_TOKEN=super-secret-and-long-token-12345
networks:
- app-net

nginx:
image: nginx:1.21
container_name: nginx-proxy
ports:
- "8080:80"
volumes:
- ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
depends_on:
- my-app
- tinyauth
networks:
- app-net

networks:
app-net:
driver: bridge

6. 启动和测试

tinyauth-demo 根目录下,执行:

1
docker-compose up --build

等待所有服务启动完成。现在,我们可以用 curl 来测试认证效果。

测试 1: 不带任何凭证的请求 (应该被拒绝)

1
curl -i http://localhost:8080

你会收到 Nginx 返回的 401 Unauthorized 错误,因为 tinyauth 服务拒绝了请求。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
HTTP/1.1 401 Unauthorized
Server: nginx/1.21.6
Date: ...
Content-Type: text/html
Content-Length: 179
Connection: keep-alive

<html>
<head><title>401 Authorization Required</title></head>
<body>
<center><h1>401 Authorization Required</h1></center>
<hr><center>nginx/1.21.6</center>
</body>
</html>

测试 2: 带错误凭证的请求 (应该被拒绝)

1
curl -i -H "Authorization: Bearer wrong-token" http://localhost:8080

结果同上,依然是 401 Unauthorized

测试 3: 带正确凭证的请求 (应该成功)

使用 docker-compose.yml 中定义的环境变量 super-secret-and-long-token-12345

1
curl -i -H "Authorization: Bearer super-secret-and-long-token-12345" http://localhost:8080

这次,请求成功!你会看到来自后端 my-app 服务的响应,并且 X-Authenticated-User 头也被成功传递。

1
2
3
4
5
6
7
8
HTTP/1.1 200 OK
Server: nginx/1.21.6
Date: ...
Content-Type: text/html; charset=utf-8
Content-Length: 55
Connection: keep-alive

Hello from my-app! You are authenticated as: service-user

结论

通过 Nginx 的 auth_request 模块和 TinyAuth 这样的微型认证服务,我们实现了一个强大而灵活的认证网关。这种模式极大地简化了对内部服务的安全保护,将认证逻辑与业务逻辑完美解耦,提高了开发效率和系统的可维护性。

虽然它功能简单,但在许多场景下已经足够使用。当你需要一个快速、轻量、非侵入式的认证方案时,TinyAuth 模式无疑是一个值得考虑的优秀选择。