ea5021c3f0
- 添加 OpenUserController 支持分页查询本租户用户,权限为 sys:user:list - 引入 OpenUserPageParam 作为用户查询白名单参数类,过滤敏感字段 - 定义 OpenUserVO,确保响应脱敏手机号、邮箱且不包含密码字段 - 修正订单接口权限 scope 由 order:read 改为 shop:shopOrder:list - 订单及退款时间字段增加 JsonFormat 注解,统一时间格式为 yyyy-MM-dd HH:mm:ss - 更新权限常量 OpenScopes,新增 SHOP_ORDER_LIST 和 SYS_USER_LIST - 增加 OpenUserVO 单元测试,确保敏感字段不被泄露且脱敏逻辑正确 - 新增开放平台调用验证脚本 scripts/verify-open-api.sh,覆盖令牌获取及接口调用验证 - 修改接口文档与示例,明确权限 scope 和时间格式要求 - 修复 TenantController 权限校验问题,确保租户字段来源于令牌且不可伪造 - 禁用伪造 tenantId 请求头修改数据范围,保证租户数据隔离安全
181 lines
6.0 KiB
Bash
Executable File
181 lines
6.0 KiB
Bash
Executable File
#!/bin/bash
|
||
#
|
||
# 开放平台接入验证脚本
|
||
#
|
||
# 作用:验证「应用凭证 → 换取令牌 → 携带令牌调用业务接口 → 租户隔离与权限生效」整条链路。
|
||
#
|
||
# 用法一(真实链路,需要应用凭证):
|
||
# OPEN_CLIENT_ID=xxx OPEN_CLIENT_SECRET=yyy ./scripts/verify-open-api.sh
|
||
#
|
||
# 用法二(已有令牌,跳过换令牌,用于本地/联调):
|
||
# OPEN_ACCESS_TOKEN=eyJ... BUSINESS_BASE_URL=http://127.0.0.1:8080 ./scripts/verify-open-api.sh
|
||
#
|
||
# 可选环境变量:
|
||
# OPEN_BASE_URL 开放平台地址,默认 https://base-api.websoft.top/api
|
||
# BUSINESS_BASE_URL 业务服务地址,默认 http://127.0.0.1:8000
|
||
# OPEN_SCOPE 期望的权限标识,默认 shop:shopOrder:list
|
||
# OPEN_PATH 被测接口路径,默认 /api/open/v1/order/page
|
||
# 测用户列表:OPEN_PATH=/api/open/v1/user/page OPEN_SCOPE=sys:user:list
|
||
# TENANT_EXPECT 期望命中本租户的记录数,留空则不校验数量(需与数据库核对后填写)
|
||
#
|
||
|
||
set -euo pipefail
|
||
|
||
RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'; BLUE='\033[0;34m'; NC='\033[0m'
|
||
|
||
OPEN_BASE_URL="${OPEN_BASE_URL:-https://base-api.websoft.top/api}"
|
||
BUSINESS_BASE_URL="${BUSINESS_BASE_URL:-http://127.0.0.1:8000}"
|
||
OPEN_SCOPE="${OPEN_SCOPE:-shop:shopOrder:list}"
|
||
OPEN_PATH="${OPEN_PATH:-/api/open/v1/order/page}"
|
||
|
||
ok() { echo -e "${GREEN}✔${NC} $1"; }
|
||
bad() { echo -e "${RED}✘${NC} $1"; }
|
||
warn() { echo -e "${YELLOW}!${NC} $1"; }
|
||
step() { echo -e "\n${BLUE}== $1 ==${NC}"; }
|
||
|
||
need_cmd() {
|
||
command -v "$1" >/dev/null 2>&1 || { bad "缺少命令:$1"; exit 1; }
|
||
}
|
||
need_cmd curl
|
||
need_cmd python3
|
||
|
||
# 只解包 ApiResult,不校验业务语义
|
||
json_field() {
|
||
python3 -c "
|
||
import json,sys
|
||
try:
|
||
data = json.load(sys.stdin)
|
||
except Exception:
|
||
print('')
|
||
sys.exit(0)
|
||
cur = data
|
||
for part in sys.argv[1].split('.'):
|
||
if isinstance(cur, dict):
|
||
cur = cur.get(part)
|
||
else:
|
||
cur = None
|
||
break
|
||
print('' if cur is None else cur)
|
||
" "$1"
|
||
}
|
||
|
||
decode_jwt_payload() {
|
||
python3 -c "
|
||
import base64, json, sys
|
||
token = sys.argv[1].strip()
|
||
parts = token.split('.')
|
||
if len(parts) < 2:
|
||
print('{}')
|
||
sys.exit(0)
|
||
payload = parts[1].replace('-', '+').replace('_', '/')
|
||
payload += '=' * ((4 - len(payload) % 4) % 4)
|
||
try:
|
||
print(json.dumps(json.loads(base64.b64decode(payload)), ensure_ascii=False, indent=2))
|
||
except Exception as e:
|
||
print('{}')
|
||
" "$1"
|
||
}
|
||
|
||
step "0. 目标地址"
|
||
echo " 开放平台:$OPEN_BASE_URL"
|
||
echo " 业务服务:$BUSINESS_BASE_URL"
|
||
echo " 期望权限:$OPEN_SCOPE"
|
||
echo " 接口路径:$OPEN_PATH"
|
||
|
||
step "1. 获取 access token"
|
||
if [ -n "${OPEN_ACCESS_TOKEN:-}" ]; then
|
||
ACCESS_TOKEN="$OPEN_ACCESS_TOKEN"
|
||
ok "使用已提供的 OPEN_ACCESS_TOKEN"
|
||
else
|
||
if [ -z "${OPEN_CLIENT_ID:-}" ] || [ -z "${OPEN_CLIENT_SECRET:-}" ]; then
|
||
bad "请设置 OPEN_CLIENT_ID / OPEN_CLIENT_SECRET,或直接提供 OPEN_ACCESS_TOKEN"
|
||
exit 1
|
||
fi
|
||
TOKEN_RESPONSE=$(curl -s -m 20 -X POST "$OPEN_BASE_URL/v1/oauth/token" \
|
||
-H 'Content-Type: application/json' \
|
||
-d "{\"grantType\":\"client_credentials\",\"clientId\":\"$OPEN_CLIENT_ID\",\"clientSecret\":\"$OPEN_CLIENT_SECRET\"}")
|
||
echo "$TOKEN_RESPONSE" | python3 -m json.tool 2>/dev/null | head -20 || echo "$TOKEN_RESPONSE"
|
||
CODE=$(echo "$TOKEN_RESPONSE" | json_field code)
|
||
if [ "$CODE" != "0" ]; then
|
||
bad "换取令牌失败:$(echo "$TOKEN_RESPONSE" | json_field message)"
|
||
exit 1
|
||
fi
|
||
ACCESS_TOKEN=$(echo "$TOKEN_RESPONSE" | json_field data.accessToken)
|
||
ok "换取令牌成功"
|
||
fi
|
||
|
||
if [ -z "$ACCESS_TOKEN" ]; then
|
||
bad "accessToken 为空"
|
||
exit 1
|
||
fi
|
||
|
||
step "2. 解析令牌声明(仅本地解码,不代表已验签)"
|
||
decode_jwt_payload "$ACCESS_TOKEN"
|
||
|
||
CLAIMS=$(decode_jwt_payload "$ACCESS_TOKEN")
|
||
TOKEN_SCOPE=$(echo "$CLAIMS" | json_field scope)
|
||
TOKEN_TENANT=$(echo "$CLAIMS" | json_field tenant_id)
|
||
|
||
if echo " $TOKEN_SCOPE " | grep -q " $OPEN_SCOPE "; then
|
||
ok "令牌包含权限 $OPEN_SCOPE"
|
||
else
|
||
bad "令牌不含 $OPEN_SCOPE,当前 scope:${TOKEN_SCOPE:-(空)}"
|
||
warn "请确认:1) base-api 权限字典已登记该 scope;2) 已在应用管理里勾选;3) 重新换过令牌"
|
||
exit 1
|
||
fi
|
||
|
||
if [ -n "$TOKEN_TENANT" ] && [ "$TOKEN_TENANT" != "None" ]; then
|
||
ok "令牌携带租户 tenant_id=$TOKEN_TENANT(业务侧据此隔离数据)"
|
||
else
|
||
bad "令牌没有 tenant_id,业务接口会返回 403"
|
||
exit 1
|
||
fi
|
||
|
||
step "3. 携带令牌调用开放接口"
|
||
RESPONSE=$(curl -s -m 20 -H "Authorization: Bearer $ACCESS_TOKEN" \
|
||
"$BUSINESS_BASE_URL$OPEN_PATH?page=1&limit=5")
|
||
echo "$RESPONSE" | python3 -m json.tool 2>/dev/null | head -40 || echo "$RESPONSE"
|
||
|
||
CODE=$(echo "$RESPONSE" | json_field code)
|
||
if [ "$CODE" != "0" ]; then
|
||
bad "调用失败:code=$CODE message=$(echo "$RESPONSE" | json_field message)"
|
||
warn "404 说明业务服务未部署该开放接口;403 说明权限或租户不满足"
|
||
exit 1
|
||
fi
|
||
ok "接口调用成功,total=$(echo "$RESPONSE" | json_field data.total)"
|
||
|
||
if [ -n "${TENANT_EXPECT:-}" ]; then
|
||
TOTAL=$(echo "$RESPONSE" | json_field data.total)
|
||
if [ "$TOTAL" = "$TENANT_EXPECT" ]; then
|
||
ok "条数与预期一致($TOTAL)"
|
||
else
|
||
bad "条数不符:接口返回 $TOTAL,预期 $TENANT_EXPECT"
|
||
exit 1
|
||
fi
|
||
fi
|
||
|
||
step "4. 负向用例"
|
||
|
||
NO_TOKEN=$(curl -s -m 20 "$BUSINESS_BASE_URL$OPEN_PATH?page=1&limit=1")
|
||
NO_TOKEN_CODE=$(echo "$NO_TOKEN" | json_field code)
|
||
if [ "$NO_TOKEN_CODE" = "401" ]; then
|
||
ok "未携带令牌被拒绝(code=401)"
|
||
else
|
||
bad "未携带令牌竟然返回 code=$NO_TOKEN_CODE,开放接口可能未走独立安全链"
|
||
fi
|
||
|
||
if [ -n "$TOKEN_TENANT" ] && [ "$TOKEN_TENANT" != "None" ]; then
|
||
SPOOF=$(curl -s -m 20 -H "Authorization: Bearer $ACCESS_TOKEN" -H "tenantId: 1" \
|
||
"$BUSINESS_BASE_URL$OPEN_PATH?page=1&limit=5")
|
||
SPOOF_TOTAL=$(echo "$SPOOF" | json_field data.total)
|
||
BASE_TOTAL=$(echo "$RESPONSE" | json_field data.total)
|
||
if [ "$SPOOF_TOTAL" = "$BASE_TOTAL" ]; then
|
||
ok "伪造 tenantId 请求头无效(仍返回本租户数据)"
|
||
else
|
||
bad "伪造 tenantId 改变了结果($BASE_TOTAL -> $SPOOF_TOTAL),存在越权风险"
|
||
fi
|
||
fi
|
||
|
||
echo
|
||
ok "验证完成"
|