#!/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 "验证完成"