218 lines
5.6 KiB
Markdown
218 lines
5.6 KiB
Markdown
---
|
|
title: "使用NGINX的auth_request进行统一jwt鉴权 - 侯锐的思考与分享"
|
|
source: "https://www.nosuchfield.com/2025/07/24/Using-NGINX-auth_request-for-unified-JWT-authentication/"
|
|
author:
|
|
- "[[Raymond]]"
|
|
published:
|
|
created: 2025-10-30
|
|
description: "<p>NGINX的<code>auth_request</code>模块提供了一种统一的认证机制,可以在NGINX层面进行JWT鉴权,而不需要在每个后端服务中重复实现认证逻辑。</p><p>首先我们定义一下nginx的配置,它的配置如下</p><figure class=\"highlight plaintext\"><figcaption><span>flat</span></figcaption><table><tr><td class=\"gutter\"><pre><span class=\"line\">1</span><br><span class=\"line\">2</span><br><span class=\"line\">3</span><br><span class=\"line\">4</span><br><span class=\"line\">5</span><br><span class=\"line\">6</span><br><span class=\"line\">7</span><br><span class=\"line\">8</span><br><span class=\"li"
|
|
tags:
|
|
- "clippings"
|
|
---
|
|
NGINX的 `auth_request` 模块提供了一种统一的认证机制,可以在NGINX层面进行JWT鉴权,而不需要在每个后端服务中重复实现认证逻辑。
|
|
|
|
首先我们定义一下nginx的配置,它的配置如下
|
|
|
|
```
|
|
flatserver {
|
|
|
|
listen 8965;
|
|
|
|
# 鉴权接口,仅供 Nginx 内部 auth_request 使用
|
|
|
|
location = /auth {
|
|
|
|
internal; # 该接口只能被 Nginx 内部请求,防止外部访问
|
|
|
|
# 转发到实际的认证服务
|
|
|
|
proxy_pass http://localhost:5001/api/auth/verify;
|
|
|
|
# 不转发请求体,提升效率
|
|
|
|
proxy_pass_request_body off;
|
|
|
|
# 防止后端因 Content-Length 不确定而报错
|
|
|
|
proxy_set_header Content-Length "";
|
|
|
|
# 将客户端传来的 Authorization 头(JWT Token)传给认证服务
|
|
|
|
proxy_set_header Authorization $http_authorization;
|
|
|
|
# 传入原始请求路径,供认证服务判断路径是否需要鉴权
|
|
|
|
proxy_set_header X-Original-URI $request_uri;
|
|
|
|
}
|
|
|
|
# 所有 / 路径下的请求都进行认证
|
|
|
|
location / {
|
|
|
|
# 认证请求会先调用上面的 /auth 接口
|
|
|
|
auth_request /auth;
|
|
|
|
# 如果认证失败(如返回 401),跳转到自定义处理逻辑
|
|
|
|
error_page 401 = @unauthorized;
|
|
|
|
# 从 /auth 的响应头中提取用户信息
|
|
|
|
auth_request_set $user_id $upstream_http_x_user_id;
|
|
|
|
# 把用户信息注入到请求头中,转发给后端业务服务
|
|
|
|
proxy_set_header X-User-ID $user_id;
|
|
|
|
# 转发到后端服务
|
|
|
|
proxy_pass http://localhost:5001;
|
|
|
|
}
|
|
|
|
# 自定义未授权响应(认证失败时返回)
|
|
|
|
location @unauthorized {
|
|
|
|
# 返回 401 状态码 + 文本内容
|
|
|
|
return 401 "Unauthorized";
|
|
|
|
}
|
|
|
|
}
|
|
```
|
|
|
|
有了这个nginx的配置之后,我们就可以实现鉴权的逻辑了,具体逻辑如下
|
|
|
|
```python
|
|
flatimport jwt
|
|
|
|
from flask import Flask, request, make_response, jsonify
|
|
|
|
from jwt import InvalidTokenError
|
|
|
|
app = Flask(__name__)
|
|
|
|
SECRET_KEY = "a-string-secret-at-least-256-bits-long"
|
|
|
|
# 路径白名单,不需要鉴权的接口
|
|
|
|
PUBLIC_PATHS = [
|
|
|
|
"/api/public/hello",
|
|
|
|
"/api/login",
|
|
|
|
"/api/register"
|
|
|
|
]
|
|
|
|
def verify_token():
|
|
|
|
original_uri = request.headers.get("X-Original-URI", "")
|
|
|
|
# 不需要鉴权的接口,直接返回200
|
|
|
|
if original_uri in PUBLIC_PATHS:
|
|
|
|
return "", 200
|
|
|
|
# 否则继续验证 JWT
|
|
|
|
auth_header = request.headers.get("Authorization")
|
|
|
|
if not auth_header or not auth_header.startswith("Bearer "):
|
|
|
|
return "Missing or invalid Authorization header", 401
|
|
|
|
# 解析得到token
|
|
|
|
token = auth_header.split(" ", 1)[1]
|
|
|
|
try:
|
|
|
|
payload = jwt.decode(token, SECRET_KEY, algorithms=["HS256"])
|
|
|
|
response = make_response("", 200)
|
|
|
|
response.headers["X-User-ID"] = str(payload.get("user_id", ""))
|
|
|
|
return response
|
|
|
|
except InvalidTokenError:
|
|
|
|
return "Invalid token", 401
|
|
|
|
def hello():
|
|
|
|
return jsonify({"message": "hello from auth", "user_id": request.headers.get("X-User-ID")})
|
|
|
|
def ping():
|
|
|
|
return {"msg": "hello without auth"}
|
|
|
|
if __name__ == "__main__":
|
|
|
|
app.run(host="0.0.0.0", port=5001)
|
|
```
|
|
|
|
启动nginx和如上python服务,之后我们使用如下payload和header以及密钥 [生成token](https://www.jwt.io/)
|
|
|
|
payload
|
|
|
|
```
|
|
{
|
|
"alg": "HS256",
|
|
"typ": "JWT"
|
|
}
|
|
```
|
|
|
|
header
|
|
|
|
```
|
|
{
|
|
"user_id": 656670838050885
|
|
}
|
|
```
|
|
|
|
密钥
|
|
|
|
```
|
|
a-string-secret-at-least-256-bits-long
|
|
```
|
|
|
|
生成得到token
|
|
|
|
```
|
|
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjo2NTY2NzA4MzgwNTA4ODV9.caQ6cp-BA-OMxXu4zTUjV0OiZo1iygvdi7GPQNjNVHM
|
|
```
|
|
|
|
之后我们就可以使用token进行测试了,具体测试结果如下
|
|
|
|
```
|
|
~ AUTH_HEADER="Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjo2NTY2NzA4MzgwNTA4ODV9.caQ6cp-BA-OMxXu4zTUjV0OiZo1iygvdi7GPQNjNVHM"
|
|
|
|
~ curl -H "$AUTH_HEADER" http://localhost:8965/api/hello
|
|
{"message":"hello from auth","user_id":"656670838050885"}
|
|
|
|
~ curl -H "$AUTH_HEADER" http://localhost:8965/api/public/hello
|
|
{"msg":"hello without auth"}
|
|
|
|
~ curl -H "$AUTH_HEADER" http://localhost:8965/api/login
|
|
<!doctype html>
|
|
<html lang=en>
|
|
<title>404 Not Found</title>
|
|
<h1>Not Found</h1>
|
|
<p>The requested URL was not found on the server. If you entered the URL manually please check your spelling and try again.</p>
|
|
|
|
~ curl http://localhost:8965/api/public/hello
|
|
{"msg":"hello without auth"}
|
|
|
|
~ curl http://localhost:8965/api/hello
|
|
Unauthorized%
|
|
```
|
|
|
|
如上我们正确设置了Authorization之后就可以正常访问需要鉴权的接口了,但是去掉了Authorization之后需要鉴权的接口就会返回Unauthorized。此外还可以看到,不需要鉴权的接口,即使不添加鉴权配置也是可以正常访问的。 |