Files
core/scripts/verify-open-api.sh
gxwebsoft ea5021c3f0 feat(open-platform): 新增用户分页查询接口并更新订单权限及时间格式
- 添加 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 请求头修改数据范围,保证租户数据隔离安全
2026-09-22 03:17:18 +08:00

181 lines
6.0 KiB
Bash
Executable File
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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 "验证完成"