feat(dnfs): delegate Token verification to gateway /whoami
Replace the insecure local base64 decode (with FIXME comment about missing signature verification) with proper remote RSA signature verification through gateway's /whoami endpoint: - Add [gateway] config section (url, timeout) to dnfs.ini with defaults. - Support GATEWAY_URL environment variable override for Docker. - decode_token() now POSTs to gateway /whoami with Authorization: Bearer <token>; gateway verifies RSA signature and expiry, returns login/user/ is_admin. Network/gateway errors fail-closed with 503. - Add @_handle_exp decorators to delete_note() and update_note() which were missing exception handling wrappers (consistent with other routes). - docker-compose.yml: add GATEWAY_URL, CONFS_HOST, CONFS_PORT env vars plus host.docker.internal extra_hosts so dnfs container can reach host-side gateway and confs services.
This commit is contained in:
1 parent
7d6ba6de93
commit
e7a2877422
3 files changed
+58
-11
No files matched your search
@@ -10,3 +10,7 @@ timeout = 10
|
|||||||
[ai_review]
|
[ai_review]
|
||||||
url = http://127.0.0.1:8007/review
|
url = http://127.0.0.1:8007/review
|
||||||
timeout = 10
|
timeout = 10
|
||||||
|
|
||||||
|
[gateway]
|
||||||
|
url = http://127.0.0.1:8443/whoami
|
||||||
|
timeout = 5
|
||||||
+49
-11
@@ -19,7 +19,6 @@ from waitress import serve
|
|||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
import json
|
import json
|
||||||
import base64
|
|
||||||
import configparser
|
import configparser
|
||||||
import urllib.request
|
import urllib.request
|
||||||
import urllib.error
|
import urllib.error
|
||||||
@@ -48,6 +47,7 @@ _dnfs_config = {
|
|||||||
'server': {'host': '', 'port': ''},
|
'server': {'host': '', 'port': ''},
|
||||||
'database': {'host': '', 'port': '', 'timeout': ''},
|
'database': {'host': '', 'port': '', 'timeout': ''},
|
||||||
'ai_review': {'url': '', 'timeout': ''},
|
'ai_review': {'url': '', 'timeout': ''},
|
||||||
|
'gateway': {'url': '', 'timeout': ''},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -93,12 +93,15 @@ if os.environ.get('DB_PORT'):
|
|||||||
_dnfs_config['database']['port'] = os.environ['DB_PORT']
|
_dnfs_config['database']['port'] = os.environ['DB_PORT']
|
||||||
if os.environ.get('REVIEW_URL'):
|
if os.environ.get('REVIEW_URL'):
|
||||||
_dnfs_config['ai_review']['url'] = os.environ['REVIEW_URL']
|
_dnfs_config['ai_review']['url'] = os.environ['REVIEW_URL']
|
||||||
|
if os.environ.get('GATEWAY_URL'):
|
||||||
|
_dnfs_config['gateway']['url'] = os.environ['GATEWAY_URL']
|
||||||
|
|
||||||
# 配置完整性校验
|
# 配置完整性校验
|
||||||
_required = {
|
_required = {
|
||||||
'server': ['host', 'port'],
|
'server': ['host', 'port'],
|
||||||
'database': ['host', 'port', 'timeout'],
|
'database': ['host', 'port', 'timeout'],
|
||||||
'ai_review': ['url', 'timeout'],
|
'ai_review': ['url', 'timeout'],
|
||||||
|
'gateway': ['url', 'timeout'],
|
||||||
}
|
}
|
||||||
for section, keys in _required.items():
|
for section, keys in _required.items():
|
||||||
for key in keys:
|
for key in keys:
|
||||||
@@ -113,11 +116,14 @@ _DBV2_ADDR = _dnfs_config['database']['host']
|
|||||||
_DBV2_PORT = int(_dnfs_config['database']['port'])
|
_DBV2_PORT = int(_dnfs_config['database']['port'])
|
||||||
_REVIEW_SERVER_URL = _dnfs_config['ai_review']['url']
|
_REVIEW_SERVER_URL = _dnfs_config['ai_review']['url']
|
||||||
_REVIEW_TIMEOUT = int(_dnfs_config['ai_review']['timeout'])
|
_REVIEW_TIMEOUT = int(_dnfs_config['ai_review']['timeout'])
|
||||||
|
_GATEWAY_URL = _dnfs_config['gateway']['url']
|
||||||
|
_GATEWAY_TIMEOUT = int(_dnfs_config['gateway']['timeout'])
|
||||||
_ADDR = _dnfs_config['server']['host']
|
_ADDR = _dnfs_config['server']['host']
|
||||||
_PORT = int(_dnfs_config['server']['port'])
|
_PORT = int(_dnfs_config['server']['port'])
|
||||||
|
|
||||||
_config_source = f'confs (dnfs.ini @ {_confs_host}:{_confs_port})'
|
_config_source = f'confs (dnfs.ini @ {_confs_host}:{_confs_port})'
|
||||||
print(f'[dnfs] 配置来源: {_config_source}')
|
print(f'[dnfs] 配置来源: {_config_source}')
|
||||||
|
print(f'[dnfs] Token 验签: gateway {_GATEWAY_URL}')
|
||||||
|
|
||||||
# 管理员判定已上移至 gateway:登录时命中 [Admin] users 白名单即在 Token payload
|
# 管理员判定已上移至 gateway:登录时命中 [Admin] users 白名单即在 Token payload
|
||||||
# 写入 is_admin=true,本服务通过 info.get('is_admin') 读取,不再本地维护名单。
|
# 写入 is_admin=true,本服务通过 info.get('is_admin') 读取,不再本地维护名单。
|
||||||
@@ -301,14 +307,46 @@ apply_security_headers(app)
|
|||||||
CORS(app)
|
CORS(app)
|
||||||
|
|
||||||
|
|
||||||
def decode_token(token: str):
|
def decode_token(token: str):
|
||||||
idx = token.find('_')
|
"""通过 gateway /whoami 接口验证 Token 并返回用户信息。
|
||||||
if idx != -1:
|
|
||||||
raw = token[:idx]
|
gateway /whoami 内部完成 RSA 签名验证和过期检查,返回:
|
||||||
# FIXME: 验证数字签名
|
- 成功: {"login": true, "user": "...", "is_admin": true/false, ...}
|
||||||
decoded = base64.b64decode(raw)
|
- 失败: {"login": false}
|
||||||
jj = decoded.decode('utf-8')
|
网络异常(gateway 不可达)按 fail-closed 处理,抛 DnfsExp 拒绝请求。
|
||||||
return json.loads(jj)
|
"""
|
||||||
|
if not token:
|
||||||
|
return None
|
||||||
|
|
||||||
|
req = urllib.request.Request(
|
||||||
|
_GATEWAY_URL,
|
||||||
|
data=b'{}',
|
||||||
|
headers={
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"Authorization": f"Bearer {token}",
|
||||||
|
},
|
||||||
|
method="POST",
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
with urllib.request.urlopen(req, timeout=_GATEWAY_TIMEOUT) as resp:
|
||||||
|
data = json.loads(resp.read().decode("utf-8"))
|
||||||
|
except (urllib.error.URLError, urllib.error.HTTPError, json.JSONDecodeError, OSError) as e:
|
||||||
|
# gateway 不可达或返回异常:fail-closed,拒绝请求
|
||||||
|
logs.warning('dnfs', f'Token 验签失败(gateway 异常): {e}', extra={})
|
||||||
|
raise DnfsExp('认证服务暂时不可用', 503)
|
||||||
|
except Exception as e:
|
||||||
|
logs.warning('dnfs', f'Token 验签未知异常: {e}', extra={})
|
||||||
|
raise DnfsExp('认证失败', 401)
|
||||||
|
|
||||||
|
if not data.get('login'):
|
||||||
|
return None
|
||||||
|
|
||||||
|
# gateway 返回 login=true 时 user 字段必定存在
|
||||||
|
return {
|
||||||
|
'user': data.get('user', ''),
|
||||||
|
'is_admin': bool(data.get('is_admin', False)),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@app.route('/add_note', methods=['POST'])
|
@app.route('/add_note', methods=['POST'])
|
||||||
@@ -426,7 +464,7 @@ def get_notes():
|
|||||||
|
|
||||||
# eg: {"id": 1}
|
# eg: {"id": 1}
|
||||||
@app.route('/delete_note', methods=['POST'])
|
@app.route('/delete_note', methods=['POST'])
|
||||||
|
@_handle_exp
|
||||||
def delete_note():
|
def delete_note():
|
||||||
|
|
||||||
data = request.get_json()
|
data = request.get_json()
|
||||||
@@ -529,7 +567,7 @@ def delete_note():
|
|||||||
# "content": "hello"
|
# "content": "hello"
|
||||||
#}
|
#}
|
||||||
@app.route('/update_note', methods=['POST'])
|
@app.route('/update_note', methods=['POST'])
|
||||||
|
@_handle_exp
|
||||||
def update_note():
|
def update_note():
|
||||||
|
|
||||||
data = request.get_json()
|
data = request.get_json()
|
||||||
|
|||||||
@@ -83,6 +83,11 @@ services:
|
|||||||
- DB_HOST=db
|
- DB_HOST=db
|
||||||
- DB_PORT=8005
|
- DB_PORT=8005
|
||||||
- REVIEW_URL=http://ai_review:8007/review
|
- REVIEW_URL=http://ai_review:8007/review
|
||||||
|
- GATEWAY_URL=http://host.docker.internal:8443/whoami
|
||||||
|
- CONFS_HOST=host.docker.internal
|
||||||
|
- CONFS_PORT=8010
|
||||||
|
extra_hosts:
|
||||||
|
- "host.docker.internal:host-gateway"
|
||||||
depends_on:
|
depends_on:
|
||||||
db:
|
db:
|
||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
|
|||||||
Reference in new issue
Block a user