尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

TLS + Web API 安全 · 04 · API 基础与认证机制

TLS + Web API 安全 · 04 · API 基础与认证机制 一、API 是什么APIApplication Programming Interface应用编程接口通俗说就是程序之间说话的约定。现实里到处都是你刷手机 App 看到的商品列表是 App 去调后端 API拿到的。前后端分离的网站前端页面浏览器里跑的 JS通过 API 向服务器要数据。你用微信登录某网站网站去调微信的开放 API验证你的身份。你写脚本批量下载 GitHub 仓库用的是 GitHub 的REST API。一个典型场景浏览器前端 服务器后端 │ GET /api/users/1001 │ │ Authorization: Bearer token │ │ ────────────────────────────────── │ │ │ 查数据库、做鉴权 │ 200 OK │ │ {id:1001,name:alice} │ │ ────────────────────────────────── │为什么 API 安全这么重要因为前端能看到的接口攻击者也能直接调。前端做的校验比如按钮灰掉、隐藏字段一律不可信——攻击者用 curl 直接发请求绕过所有前端逻辑。所以所有安全校验必须在服务端做。既然前端校验不可信那前端还做校验干嘛前端校验的定位是提升用户体验和减少无效请求不是安全。比如密码长度不够就变红能让用户少发一次注定失败的请求、少等一个来回按钮灰掉能防误操作。但它永远只是第一道体验层服务端必须再校验一遍。判断一个系统安不安全看的是服务端有没有校验而不是前端。二、REST 与 HTTP 语义2.1 HTTP 请求长什么样一个 HTTP 请求由三部分组成POST /api/login HTTP/1.1 ← 请求行方法 路径 版本 Host: api.lab ← 请求头Header一堆键值对 Content-Type: application/json Authorization: Bearer eyJ... ​ {username:alice,password:password1} ← 请求体Body方法Method要对资源做什么GET/POST/PUT/DELETE...。路径Path操作哪个资源/api/users/1001。请求头附加信息比如身份凭证、数据类型。请求体要提交的数据通常是 JSON。2.2 常用 HTTP 方法方法语义幂等例子GET读取资源是GET /api/users/1001POST创建资源 / 提交动作否POST /api/users、POST /api/loginPUT整体更新覆盖是PUT /api/profilePATCH局部更新否PATCH /api/profileDELETE删除资源是DELETE /api/users/1001幂等idempotent同一个请求执行一次和执行多次对服务器状态的影响相同。GET幂等读一百次和读一次数据不变。PUT幂等整体覆盖执行多次结果一样。POST不幂等调两次就可能创建两条记录、扣两次款——这也是为什么重复提交是个问题。安全含义会改变服务器状态的请求POST/PUT/PATCH/DELETE要防 CSRF——浏览器会自动带上 Cookie可能被恶意页面诱导发出这些请求同时它们尤其不幂等的 POST还要防重放。GET 本应是只读、无副作用的如果被错误地用来改状态也会有 CSRF 风险。TLS 1.3 的 0-RTT 只建议用于幂等请求原因也在这。2.3 状态码服务器用状态码告诉客户端结果怎么样。分五大类类别含义常见码1xx信息很少见2xx成功200 OK、201 Created、204 No Content3xx重定向301永久、302临时、304 Not Modified4xx客户端错你请求有问题400参数错、401未认证、403无权限、404不存在、429太频繁5xx服务端错500内部错误、502网关错误、503不可用安全上最需要分清的两个401 Unauthorized其实含义是未认证——你没登录 / token 无效。解决方式是去登录。403 Forbidden已认证但没权限——你登录了但这个资源不归你。解决方式是你没资格。这个区分在讲越权第 06 篇时极其重要。我们用靶场看看真实状态码cd /opt/tls-api-labs echo -- 正常 --; curl -s -k -o /dev/null -w %{http_code}\n https://127.0.0.1/api/health echo -- 未认证 --; curl -s -k -o /dev/null -w %{http_code}\n https://127.0.0.1/api/me echo -- 不存在的接口 --; curl -s -k -o /dev/null -w %{http_code}\n https://127.0.0.1/api/nopecurl命令行 HTTP 客户端。-ssilent静默。不加它会打印下载进度条和一些提示加上后只输出我们要的内容方便脚本处理。-kinsecure跳过证书校验-o /dev/null把响应体写到/dev/null一个黑洞设备写进去就没了也就是丢弃响应体只关心状态码。-w %{http_code}\n-wwrite-out表示请求结束后按这个格式额外打印信息%{http_code}是 curl 内置变量代表 HTTP 状态码\n是换行。echo -- 正常 --打印一行提示文字方便区分每段输出。;把多条命令写在同一行表示依次执行和换行等价。真实回显-- 正常 -- 200 -- 未认证 -- 401 -- 不存在的接口 -- 4042.4 JSONAPI 的通用数据格式{ id: 1001, username: alice, role: user, tags: [vip, active] }JSON 是纯文本的键值结构几乎成了 API 的事实标准。请求头里用Content-Type: application/json声明我发的是 JSON。安全含义JSON 里能放任意字段。如果服务端把用户提交的字段照单全收就可能被塞进本不该改的字段role、is_admin——这就是批量赋值Mass Assignment第 06 篇会亲手打。2.5 REST 是什么REST是一种 API 设计风格核心思想是用 URL 表示资源用 HTTP 方法表示操作GET /api/users # 列出用户 POST /api/users # 创建用户 GET /api/users/1001 # 读取 1001 号用户 PUT /api/users/1001 # 更新 1001 号用户 DELETE /api/users/1001 # 删除 1001 号用户REST 不是强制的很多接口是RPC 风格POST /api/getUser。但理解 REST 有助于你读接口文档、猜接口路径信息收集时很有用。2.6 OpenAPI / Swagger现实中的 API 通常有一份机器可读的文档叫OpenAPI 规范旧名 Swagger。它描述每个接口的路径、方法、参数、返回。很多框架能自动生成也常配一个网页界面供调试。安全含义/swagger-ui、/openapi.json、/api-docs这类地址如果暴露在公网等于把接口清单直接送给攻击者。实战信息收集时找这些路径是常规操作。三、认证 vs 授权必须分清的一对概念这是 API 安全里最容易被混淆、也最重要的一组词概念英文回答的问题例子认证Authentication你是谁输入账号密码、带 token授权Authorization你能做什么你能不能读别人的订单、能不能进后台一句话先认证证明身份再授权判断权限。为什么必须分清因为大量漏洞的根因就是只做了认证没做授权用户登录了吗 —— 登录了放行。这个订单是用户的吗 ——没检查结果任意登录用户都能读别人的订单。这类漏洞叫IDOR / BOLA是 OWASP API Top 10 的第一名用靶场直观感受一下认证 ≠ 授权cd /opt/tls-api-labs # 登录 alice拿到 token ALICE$(curl -s -k -X POST https://127.0.0.1/api/login -H Content-Type: application/json -d {username:alice,password:password1} | python3 -c import sys,json;print(json.load(sys.stdin)[token])) ​ # alice 已认证读自己的资料 echo 读自己的; curl -s -k https://127.0.0.1/api/users/1001 -H Authorization: Bearer $ALICE; echo # alice 已认证但读别人的资料 —— 竟然成功了 echo 读别人的; curl -s -k https://127.0.0.1/api/users/1002 -H Authorization: Bearer $ALICE; echoALICE$(...)命令替换command substitution。括号里那条命令的输出会被取出来赋值给变量ALICE之后用$ALICE就能引用它这里存的是登录拿到的 JWT。-X POST-X指定 HTTP 方法request method。登录接口要求用POST所以要显式写出来curl 默认发 GET。-H Content-Type: application/json-H添加一个请求头。这里告诉服务器我发的是 JSON 格式。-d {username:alice,password:password1}-ddata发送请求体。外面用单引号包住 JSONJSON 内部用双引号。\行末反斜杠续行符表示这条命令还没写完下一行接着。纯粹为了排版好读不是命令的一部分。python3 -c ...调用 Python。-ccommand表示把引号里的字符串当成一段 Python 代码直接执行不写文件。这段代码干的事是从管道传来的登录响应里把 JWT 提取出来。import sys, json导入两个标准库。sys用来读取标准输入json用来解析 JSON。json.load(sys.stdin)sys.stdin就是管道传进来的内容curl 的响应json.load把它解析成 Python 字典。[token]取出字典里 key 为token的值也就是那串 JWT。print(...)打印出来成为整条命令的输出于是被$(...)赋值给ALICE。再看读资料那两条echo 读自己的; curl -s -k https://127.0.0.1/api/users/1001 -H Authorization: Bearer $ALICE; echo-H Authorization: Bearer $ALICE带上传身份凭证。Authorization是标准请求头Bearer token是它的取值格式第 05 篇细讲。结尾的; echo再打印一个空行让输出分段更清楚。真实回显读自己的 {id: 1001, username: alice, email: alicelab, phone: 13800000001, role: user, secret: alice 的信用卡号: 6222-0000-0000-1001} 读别人的 {id: 1002, username: bob, email: boblab, phone: 13800000002, role: user, secret: bob 的信用卡号: 6222-0000-0000-1002}alice 拿到了 bob 的信用卡号。认证没问题alice 确实登录了授权有问题没检查这条数据是不是 alice 的。这就是越权。第 06 篇细讲。四、认证方式4.1 HTTP Basic用户名密码放请求头格式是把用户名:密码用 Base64 编码后放进Authorization头Authorization: Basic YWxpY2U6cGFzc3dvcmQxBase64 解码就是alice:password1。Base64 不是加密只是编码谁都能解。问题每次请求都带密码暴露面大。必须配合 HTTPS否则明文传密码。没法细粒度控制、没法方便地登出。用途内网简单服务、路由器后台等。现代 API 很少用。4.2 API Key一串预先分发的密钥服务器给每个调用方发一个 Key调用时带上curl -k https://127.0.0.1/api/data -H X-API-Key: lab-api-key-123-H X-API-Key: lab-api-key-123添加一个自定义请求头X-API-Key值就是服务器发给调用方的密钥。X-开头是自定义头的惯例命名。真实回显{data: 这是用 API Key 保护的资源}不带 Key 时curl -s -k https://127.0.0.1/api/data真实回显{error: invalid api key}特点简单适合服务器对服务器的调用。Key 通常长期有效一旦泄露就等于身份被盗用。很多 API Key 放在 URL 参数里?api_keyxxx会出现在日志、浏览器历史、Referer 里更容易泄露。安全含义API Key 是共享密钥不是用户身份。所以它适合标识哪个应用不适合标识哪个用户。用户级别的授权还是得靠 token。4.3 Session / Cookie传统网站的做法流程用户登录服务器在内存/数据库里创建一条会话记录生成一个session id。服务器通过Set-Cookie把 session id 发给浏览器。浏览器之后每次请求自动带上这个 Cookie。服务器查 session id 对应的会话知道是谁。特点状态存在服务器有状态。适合传统网页。问题跨域/移动端不方便服务器要存会话CSRF 攻击浏览器自动带 Cookie。4.4 Bearer Token / JWT现代 API 的主流客户端登录后服务器返回一个token之后请求带在Authorization头Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...Bearer的意思是持有者——谁拿到这个 token谁就被当成那个用户。所以 token 泄露 身份泄露。常见的 token 是JWTJSON Web Token它把用户信息直接编码在 token 里服务器不用查库就能知道你是谁。为什么现代 API 爱用 token无状态服务器不用存会话方便水平扩展。跨域友好不依赖 Cookie。适合移动端、前后端分离。代价token 一旦泄露在过期前都有效且很难提前作废因为服务器不存它。所以 JWT 的过期时间要短还要配合刷新机制。那 JWT 和 Session 到底该选哪个没有绝对好坏看场景传统网页、单体应用、需要服务端随时踢人下线→ Session 更合适状态在服务端作废容易前后端分离、移动端、多服务/跨域、追求水平扩展→ JWT/token 更合适无状态服务器不用存。实践中常常两者结合用短期 JWT 做访问凭证 服务端存 refresh token 以便吊销。五、JWT 长什么样一个 JWT 是三段用点号.连起来的字符串eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9 . eyJzdWIiOiIxMDAxIiwidXNlcm5hbWUiOiJhbGljZSIsInJvbGUiOiJ1c2VyIn0 . rkjtHjPhiGHxGudoYQxpZ0_gbiHz-EVcTSRddX0fF2Q Header Payload Signature我们把真实 token 解出来看看cd /opt/tls-api-labs TOKEN$(curl -s -k -X POST https://127.0.0.1/api/login \ -H Content-Type: application/json \ -d {username:alice,password:password1} \ | python3 -c import sys,json;print(json.load(sys.stdin)[token])) python3 - $TOKEN PY import sys, base64, json t sys.argv[1] for name, part in zip([header, payload], t.split(.)[:2]): pad part * (-len(part) % 4) print(name, , json.loads(base64.urlsafe_b64decode(pad))) print(signature(hex) , base64.urlsafe_b64decode(t.split(.)[2] ).hex()) PY第一部分TOKEN$(...)和前面一样登录并把响应里的 JWT 提取出来存进变量TOKEN。第二部分python3 - $TOKEN PY ... PY这是另一种把代码交给 Python 执行的写法。python3 -后面的-表示从标准输入读取要执行的脚本而不是执行某个文件。$TOKEN把 token 作为一个命令行参数传给脚本脚本里用sys.argv[1]取到。sys.argv[0]是脚本名这里是-sys.argv[1]才是第一个真正的参数。PY ... PY这叫heredoc这里文档。它把两个PY标记之间的所有行当作标准输入喂给前面的命令。PY加了引号表示中间内容原样传入不做变量替换免得 shell 把代码里的$、反引号当成变量去展开。脚本内部逐行看import sys, base64, jsonbase64负责 Base64 解码json负责解析sys用来取参数。t sys.argv[1]取到传进来的 token。t.split(.)用点号把 token 切成三段Header、Payload、Signature[:2]表示只取前两段。zip([header, payload], t.split(.)[:2])把名字和内容一一配对循环时同时拿到两者。part * (-len(part) % 4)JWT 用的是Base64URL会把末尾的填充去掉解码前要补回来。-len(part) % 4正好算出还差几个才能凑成 4 的倍数。base64.urlsafe_b64decode(pad)按 Base64URL 规则解码把 URL 安全字符-、_还原。json.loads(...)把解码出来的字节解析成 Python 字典。.hex()把二进制的签名转成十六进制字符串方便肉眼看。真实回显header {alg: HS256, typ: JWT} payload {sub: 1001, username: alice, role: user} signature(hex) ae48ed1e33e18861f11ae768610c69674fe06e21f3f8455c4d245d757d1f1764Header声明用的签名算法这里是HS256。Payload真正携带的信息用户 id、用户名、角色。Signature用密钥对前两段签名防止篡改。关键认知前两段只是Base64URL 编码不是加密——任何人都能解开看内容。所以 JWT 里绝对不能放密码、密钥等敏感信息。JWT 的安全性完全靠第三段签名改了内容签名就对不上。这也是为什么我们说JWT 能解码 ≠ 能伪造。解码是个人都会伪造需要密钥。六、CORS浏览器的一道墙和它的坑同源策略Same-Origin Policy浏览器默认禁止网页的 JS 去读另一个源的响应。源 协议 域名 端口三者全一样才算同源。https://a.com和https://b.com不同源。https://a.com和http://a.com不同源协议不同。https://a.com:443和https://a.com:8443不同源端口不同。CORS跨域资源共享是服务器用来主动允许某些跨域请求的机制。服务器在响应头里加Access-Control-Allow-Origin: **表示任何网站都可以读我的响应。看看我们靶场的响应头curl -s -k -D - -o /dev/null https://127.0.0.1/api/health-s静默模式不打印进度条。-k跳过证书校验自签证书场景。-D --Ddump headers把响应头打印出来-表示输出到标准输出屏幕。-o /dev/null丢弃响应体只留下响应头。这里演示的是一个过度宽松的 CORS配置Access-Control-Allow-Origin: *任何网站都能读。Access-Control-Allow-Headers: *、Access-Control-Allow-Methods: *几乎不设限。为什么危险要分两种情况看只有Access-Control-Allow-Origin: *任何网站都能读取不带凭证不带 Cookie的响应。对公开数据接口这是正常的但如果接口本以为是只有自己人能用那就等于把数据公开了。注意浏览器规定*不能和携带凭证一起用所以单靠*还偷不到带 Cookie 的数据。真正危险的组合服务端把请求里的Origin原样反射回Access-Control-Allow-Origin并且设置了Access-Control-Allow-Credentials: true。这时任意恶意网站都能带着用户的 Cookie发请求并读到响应 → 用户数据被窃取。正确的做法是白名单只允许自家前端域名需要携带凭证时Allow-Origin必须是具体域名不能是*并配合Allow-Credentials: true。注意CORS 是浏览器的限制对 curl、Python 脚本等非浏览器客户端无效。所以 CORS 不是访问控制不能当成安全边界。七、现实世界API 长什么样概念现实中的样子API 文档大多数公司内部有 OpenAPI/Swagger开放平台有在线文档API 调试开发者用 Postman、Insomnia、浏览器 DevTools 的 Network 面板认证用户级用 OAuth2/OIDC JWT服务级用 API Key / mTLS网关大厂 API 前面有 API 网关统一做鉴权、限流、日志、WAF版本URL 里带版本如/api/v1/...限流按 key/IP/用户限速返回429 Too Many Requests从浏览器看真实 API打开任意网站按 F12 → Network网络面板刷新页面就能看到前端发了哪些 API 请求、带了什么头、返回了什么。这是学习 API 最快的方式第 05、06 篇会让你用这招做实验。八、本篇小结API 是程序之间说话的约定前端校验不可信安全必须在服务端做。HTTP 方法有语义和幂等性状态码 401未认证、403无权限必须分清。REST 用 URL 表示资源、方法表示操作OpenAPI 是机器可读的接口文档。认证回答你是谁授权回答你能做什么只认证不授权 越权漏洞。认证方式Basic明文编码、API Key共享密钥、Session有状态、Bearer/JWT主流。JWT 三段 Header Payload Signature前两段只是编码谁都能看。CORS 是浏览器的同源策略*很危险但它管不了 curl不是安全边界。九、总结认证和授权有什么区别只做认证会有什么漏洞答认证回答你是谁授权回答你能做什么。只做认证不授权就会出现任何登录用户都能访问不属于自己的数据或功能——也就是越权BOLA/BFLA。401 和 403 分别代表什么为什么必须分清答401 未认证没登录 / token 无效403 已认证但无权限。分清才能定位问题401 该去登录403 说明身份没问题但权限不够测试越权时403 往往说明这里做了检查是对照组。Base64 是加密吗为什么 HTTP Basic 必须配合 HTTPS答Base64只是编码不是加密任何人拿到都能解开。HTTP Basic 每次请求都把用户名:密码用 Base64 放在请求头里如果走 HTTP 明文抓包直接就能还原出密码所以必须配合 HTTPS。API Key 和用户 token 的定位有什么不同答API Key 标识哪个应用/调用方是共享密钥、通常长期有效适合服务对服务的调用用户 token 标识哪个用户用于用户级授权。用 API Key 代替用户 token 做用户授权会导致一个 Key 泄露 所有人身份被盗用。JWT 的前两段能被人解开吗那它靠什么保证安全答能前两段只是 Base64URL 编码任何人都能解开看内容。安全完全靠第三段签名内容改一个字节签名就对不上没有密钥就伪造不出合法签名。CORS 的Access-Control-Allow-Origin: *有什么风险它能防住 curl 吗答*表示任何网站都能读取这个接口不带凭证的响应——如果这本来是只给自己人用的接口就等于把数据公开了。真正能偷带 Cookie 数据的危险组合是反射任意 Origin Allow-Credentials: true因为*本身不能和携带凭证一起用。CORS 是浏览器的策略对 curl、Python 等非浏览器客户端完全无效所以它不是访问控制、不能当安全边界。附录本篇命令与语法速查参数含义备注-s静默不显示进度条脚本里几乎都加-S配合-s出错时仍显示错误信息-k跳过证书校验危险仅实验用--cacert 文件指定信任的 CA 证书自签场景-X 方法指定 HTTP 方法默认 GET-H 键: 值添加请求头可写多次-d 数据发送请求体会自动用 POST-o 文件响应体写到文件/dev/null 丢弃-w 格式请求结束后按格式打印%{http_code}状态码-D -打印响应头-表示标准输出-i输出里带上响应头-v详细模式含 TLS 握手过程--resolve 域名:端口:IP临时把域名解析到指定 IP相当于临时写 hostsshell 语法$(命令)命令替换把命令的输出取出来。|管道左边命令的输出接到右边命令的输入。\续行符命令换行写。;同一行顺序执行多条命令。$变量/${变量}引用变量。标记 ... 标记heredoc把中间内容作为标准输入标记加引号表示不做变量替换。Python 的两种就地执行python3 -c 代码直接执行引号里的一行代码。python3 - PY ... PY从标准输入执行多行代码。常用sys.stdin标准输入、sys.argv[1]第一个参数、json.load/json.loads解析 JSON、base64.urlsafe_b64decodeBase64URL 解码。
返回列表