本文实验均在个人本地部署、合法授权的 OWASP Juice Shop 靶场环境中完成,仅用于安全学习与漏洞分析。
实际生产文档中不得保留真实 Token、Cookie、邮箱、密码、用户 ID、Basket ID 或其他敏感信息。
一、实验目标与环境
1.1 实验目标
本次实验针对 OWASP Juice Shop 的登录认证流程与 JWT 使用方式进行分析。
主要关注:
- 登录成功后服务端如何返回认证令牌。
- 客户端如何通过 Bearer Token 表示登录身份。
- JWT Header / Payload / Signature 的基本结构。
- JWT Payload 中是否包含不应暴露的敏感字段。
- Authentication 与 Authorization 的区别。
1.2 实验环境
| 项目 | 说明 |
|---|---|
| 实验目标 | 本地部署的 OWASP Juice Shop 授权靶场 |
| 目标地址 | http://127.0.0.1:3000 |
| 测试工具 | Burp Suite |
| 测试账号 | test1@test.com |
| 漏洞类型 | Authentication / JWT |
二、正常登录流程
2.1 捕获登录请求
在登录界面输入测试账号:test1@test.com 测试密码:test1。开启Burp Suite拦截:
POST /rest/user/login HTTP/1.1 Host: 127.0.0.1:3000 User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:140.0) Gecko/20100101 Firefox/140.0 Accept: application/json, text/plain, */* Accept-Language: en-US,en;q=0.5 Accept-Encoding: gzip, deflate, br Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJkYXRhIjp7ImlkIjoyNCwidXNlcm5hbWUiOiIiLCJlbWFpbCI6InRlc3QxQHRlc3QuY29tIiwicGFzc3dvcmQiOiI1YTEwNWU4YjlkNDBlMTMyOTc4MGQ2MmVhMjI2NWQ4YSIsInJvbGUiOiJjdXN0b21lciIsImRlbHV4ZVRva2VuIjoiIiwibGFzdExvZ2luSXAiOiIwLjAuMC4wIiwicHJvZmlsZUltYWdlIjoiL2Fzc2V0cy9wdWJsaWMvaW1hZ2VzL3VwbG9hZHMvZGVmYXVsdC5zdmciLCJ0b3RwU2VjcmV0IjoiIiwiaXNBY3RpdmUiOnRydWUsImNyZWF0ZWRBdCI6IjIwMjYtMDktMjQgMDc6Mzg6NTUuMTQyICswMDowMCIsInVwZGF0ZWRBdCI6IjIwMjYtMDktMjQgMDc6Mzg6NTUuMTQyICswMDowMCIsImRlbGV0ZWRBdCI6bnVsbH0sImJpZCI6OSwiaWF0IjoxNzkwMjM1NTc2fQ.v1s4e3XaH-0vvYvpDxgrSDprwzQTdJ1TzMZbhgG6b3-WIB2TTjBK9X77O4tL7D3_fUuUBUPdIkjaY3yRPulQTZLstS7vtUuYEjTpWUOow6-OjIaRfdjXZHLjH-h9AYGgPo2On6xTBKy-Ot7TGCzFZf042OKnRXZeZVWAsKbHoGM Content-Type: application/json Content-Length: 45 Origin: http://127.0.0.1:3000 Connection: keep-alive Referer: http://127.0.0.1:3000/ Cookie: language=zh_CN; welcomebanner_status=dismiss; cookieconsent_status=dismiss; continueCode=P32pqXyQJYZbEekj1dKXIMSnnhXMc83TqXFR8tYRcknGn9B4Ol6VLvrWwM7x; continueCodeFindIt=JEnej6XmL2MbOGK5q1BNpgyZ0yytrzTEquDP4lJQ7DVr0WwaRdPkvx3oY9z1; continueCodeFixIt=7r7BdZOzW026GnqpYQJyvDjxbKKhBXTePuJOAwK3RME5klmgV4b98XLeoNPp Sec-Fetch-Dest: empty Sec-Fetch-Mode: cors Sec-Fetch-Site: same-origin Priority: u=0 {"email":"test1@test.com","password":"test1"}从捕获到的登录请求可以看出,客户端在登录时向后端发送了一个 POST /rest/user/login 请求,请求体采用 JSON 格式提交用户邮箱和密码:
{ "email": "test1@test.com", "password": "test1" }该请求的核心作用是向服务端提交用户凭证,由服务端判断账号密码是否正确。请求头中的 Content-Type: application/json 表明请求体数据格式为 JSON,后端会按照 JSON 格式解析其中的 email 和 password 字段。
需要注意的是,本次抓包中登录请求头里已经出现了 Authorization: Bearer <JWT_TOKEN> 字段。这说明当前浏览器环境中可能已经存在旧的登录状态,或者 Burp 捕获的是一次已有登录态下再次发送的登录请求。严格来说,普通登录请求本身并不依赖已有的 Bearer Token,认证身份主要依赖请求体中的邮箱和密码。
此外,请求中的 Cookie 主要包含语言、欢迎横幅、挑战进度等状态信息,例如 language、welcomebanner_status、continueCode 等。这些字段更多用于前端状态记录或靶场挑战进度维护,并不是本次登录认证的核心凭证。
综上,登录请求阶段的核心数据是请求路径、请求方法、JSON 请求体中的邮箱和密码。服务端会根据这些凭证完成 Authentication,即身份认证过程。
2.2 分析登录响应
将捕获得到得登录请求发送至Repeater模块。发送后查看相应:
HTTP/1.1 200 OK Access-Control-Allow-Origin: * X-Content-Type-Options: nosniff X-Frame-Options: SAMEORIGIN Feature-Policy: payment 'self' X-Recruiting: /#/jobs Content-Type: application/json; charset=utf-8 Content-Length: 787 ETag: W/"313-ixEfE1QZFnlZIWG3BLvZA/t8FFk" Vary: Accept-Encoding Date: Tue, 29 Sep 2026 08:05:15 GMT Connection: keep-alive Keep-Alive: timeout=5 {"authentication":{"token":"eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJkYXRhIjp7ImlkIjoyNCwidXNlcm5hbWUiOiIiLCJlbWFpbCI6InRlc3QxQHRlc3QuY29tIiwicGFzc3dvcmQiOiI1YTEwNWU4YjlkNDBlMTMyOTc4MGQ2MmVhMjI2NWQ4YSIsInJvbGUiOiJjdXN0b21lciIsImRlbHV4ZVRva2VuIjoiIiwibGFzdExvZ2luSXAiOiIwLjAuMC4wIiwicHJvZmlsZUltYWdlIjoiL2Fzc2V0cy9wdWJsaWMvaW1hZ2VzL3VwbG9hZHMvZGVmYXVsdC5zdmciLCJ0b3RwU2VjcmV0IjoiIiwiaXNBY3RpdmUiOnRydWUsImNyZWF0ZWRBdCI6IjIwMjYtMDktMjQgMDc6Mzg6NTUuMTQyICswMDowMCIsInVwZGF0ZWRBdCI6IjIwMjYtMDktMjQgMDc6Mzg6NTUuMTQyICswMDowMCIsImRlbGV0ZWRBdCI6bnVsbH0sImJpZCI6OSwiaWF0IjoxNzkwNjY5MTE2fQ.ZeNr9GwjClOcy_exp60SOm30oNDaUS5roImseWC9YxdWsoHuuuOdUs78GXAgxvLxgDyShbfH6HaAKCO9xKQh-uicGLBN-PvudsOSf_XAbI-7WVIAU81cw35Yd1Wcpl5RAL31FuNkYpj-Iuc0uScG4kLherv-1s01GsrFFSNJjFg","bid":9,"umail":"test1@test.com"}}将登录请求发送至 Repeater 后,服务端返回 HTTP/1.1 200 OK,说明账号密码验证通过,登录请求被成功处理。
响应体中最关键的字段是 authentication 对象:
{ "authentication": { "token": "<JWT_TOKEN>", "bid": 9, "umail": "test1@test.com" } }其中,token 字段是服务端签发给客户端的 JWT。客户端后续访问需要登录身份的接口时,会将该 Token 放入请求头中,例如:
Authorization: Bearer <JWT_TOKEN>这表示客户端正在使用该 Token 声明自己的登录身份。
JWT 通常由三部分组成:
Header.Payload.SignatureHeader 部分用于说明令牌类型和签名算法;Payload 部分用于存放用户相关声明信息;Signature 部分用于校验 Token 是否被篡改。
在本次实验中,将 Token 解码后可以看到 Payload 中包含用户 ID、邮箱、角色、购物车 ID、头像路径、创建时间等信息(解码后文本在后文揭晓)。需要特别注意的是,Payload 中还出现了 password 字段,虽然这里保存的是哈希值而不是明文密码,但仍然属于不应暴露给客户端的敏感字段。
JWT 的 Payload 只是经过 Base64URL 编码,并不是加密。任何拿到 Token 的人都可以解码查看 Payload 内容。因此,不应在 JWT Payload 中存放密码哈希、密钥、隐私数据或其他敏感字段。
从响应结果可以看出,登录成功后,服务端不仅返回了 JWT,还返回了 bid 和 umail 字段。其中 bid 可以理解为当前用户对应的购物车 ID,umail 表示当前登录用户邮箱。这些字段会被前端用于后续业务请求。
2.3 初步结论
结合登录请求和登录响应可以看出,OWASP Juice Shop 的正常登录认证流程大致如下:
客户端向 /rest/user/login 接口提交邮箱和密码。 服务端校验用户凭证是否正确。 如果认证成功,服务端生成并返回 JWT。 客户端保存 JWT,并在后续请求中通过 Authorization: Bearer <JWT_TOKEN> 携带该令牌。 服务端根据 Token 识别当前用户身份,并决定是否允许其访问对应资源。因此,JWT 在该流程中承担的是“登录成功后的身份凭证”作用。它不是用户密码本身,而是服务端签发给客户端的认证令牌。
需要区分的是,Authentication 和 Authorization 并不是同一个概念。
Authentication 解决的是“你是谁”的问题,例如用户通过邮箱和密码登录,服务端确认该用户身份。
Authorization 解决的是“你能访问什么”的问题,例如当前用户是否有权限访问某个购物车、订单、后台管理接口或其他用户的数据。
本次实验中,登录流程主要体现的是 Authentication。用户登录成功后拿到 JWT,说明身份认证通过。但这并不意味着该用户可以访问所有资源。后续接口仍然需要在服务端进行权限校验,否则就可能产生越权访问问题。
另外,本次实验也暴露出一个 JWT 设计上的安全问题:Token Payload 中包含了过多用户信息,甚至包含密码哈希字段。由于 JWT Payload 可以被客户端直接解码查看,因此服务端在设计 Token 内容时应遵循最小化原则,只放入必要的身份标识信息,例如用户 ID、角色、签发时间、过期时间等,不应放入密码哈希、隐私字段或其他敏感数据。
综上,Juice Shop 的登录流程可以概括为:账号密码完成身份认证,JWT 作为后续请求的身份凭证,Bearer Token 负责在请求头中传递该凭证,而真正的资源访问安全还依赖服务端后续的权限校验。
三、JWT 结构分析
3.1 JWT 三段结构
JWT 的基本结构由三部分组成:
Header.Payload.Signature本次登录响应中返回的 Token 同样符合该结构。按照.进行分割后,可以得到三段内容:
| 部分 | 观察结果 |
|---|---|
| Header | eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9 |
| Payload | eyJkYXRhIjp7ImlkIjoyNCwidXNlcm5hbWUiOiIiLCJlbWFpbCI6InRlc3QxQHRlc3QuY29tIiwicGFzc3dvcmQiOiI1YTEwNWU4YjlkNDBlMTMyOTc4MGQ2MmVhMjI2NWQ4YSIsInJvbGUiOiJjdXN0b21lciIsImRlbHV4ZVRva2VuIjoiIiwibGFzdExvZ2luSXAiOiIwLjAuMC4wIiwicHJvZmlsZUltYWdlIjoiL2Fzc2V0cy9wdWJsaWMvaW1hZ2VzL3VwbG9hZHMvZGVmYXVsdC5zdmciLCJ0b3RwU2VjcmV0IjoiIiwiaXNBY3RpdmUiOnRydWUsImNyZWF0ZWRBdCI6IjIwMjYtMDktMjQgMDc6Mzg6NTUuMTQyICswMDowMCIsInVwZGF0ZWRBdCI6IjIwMjYtMDktMjQgMDc6Mzg6NTUuMTQyICswMDowMCIsImRlbGV0ZWRBdCI6bnVsbH0sImJpZCI6OSwiaWF0IjoxNzkwNjY5MTE2fQ |
| Signature | ZeNr9GwjClOcy_exp60SOm30oNDaUS5roImseWC9YxdWsoHuuuOdUs78GXAgxvLxgDyShbfH6HaAKCO9xKQh-uicGLBN-PvudsOSf_XAbI-7WVIAU81cw35Yd1Wcpl5RAL31FuNkYpj-Iuc0uScG4kLherv-1s01GsrFFSNJjFg |
其中,Header 和 Payload 可以通过 Base64URL 解码直接查看内容;Signature 是服务端使用私钥对 Header 和 Payload 计算得到的签名值,主要用于校验 Token 是否被篡改,不能像普通 JSON 一样直接解码成可读字段。
3.2 Header 解码
对 JWT 第一段 Header 进行 Base64URL 解码后,可以得到如下内容:
{"typ":"JWT","alg":"RS256"}其中:
typ: JWT表示该令牌类型为 JWT。alg: RS256表示该 JWT 使用 RS256 签名算法。
RS256 属于非对称签名算法,服务端使用私钥生成签名,验证方可以使用对应公钥验证签名是否正确。这里需要注意,Header 中的算法字段只说明该 Token 声称使用的签名算法,真正判断 Token 是否可信,还需要服务端在验证阶段正确校验签名。
3.3 Payload 解码
对 JWT 第二段 Payload 进行 Base64URL 解码后,可以得到如下内容:
{"data":{"id":24,"username":"","email":"test1@test.com","password":"5a105e8b9d40e1329780d62ea2265d8a","role":"customer","deluxeToken":"","lastLoginIp":"0.0.0.0","profileImage":"/assets/public/images/uploads/default.svg","totpSecret":"","isActive":true,"createdAt":"2026-09-24 07:38:55.142 +00:00","updatedAt":"2026-09-24 07:38:55.142 +00:00","deletedAt":null},"bid":9,"iat":1790669116}可以看到,Payload 中主要包含三类信息:
第一类是用户身份信息,例如id、email、role。这些字段用于表示当前登录用户是谁,以及该用户在系统中的角色。
第二类是业务相关信息,例如bid。结合前文登录响应中的"bid": 9可以判断,该字段表示当前用户对应的 Basket ID,也就是购物车 ID。
第三类是用户账户状态与扩展字段,例如isActive、createdAt、updatedAt、deletedAt、profileImage、lastLoginIp、totpSecret等。这些字段并非每次接口鉴权都必须使用,但仍被放入了 JWT Payload 中。
其中,iat表示 issued at,即 Token 签发时间。这里的1790669116对应的时间约为2026-09-29 08:05:16 UTC,与前文响应头中的时间Tue, 29 Sep 2026 08:05:15 GMT基本一致,说明该 Token 是本次登录成功后由服务端新签发的。
需要重点注意的是,Payload 中出现了password字段,值为:
5a105e8b9d40e1329780d62ea2265d8a虽然这里不是明文密码,而是密码哈希值,但仍然不应该出现在 JWT Payload 中。因为 JWT 的 Payload 只是 Base64URL 编码,不是加密。任何拿到 Token 的人都可以直接解码查看其中内容。
3.4 敏感字段观察
| 字段 | 是否出现 | 风险说明 |
|---|---|---|
| 用户 ID | 是,id: 24 | 用户 ID 属于身份标识信息,暴露后可能被用于后续越权测试、用户枚举或接口参数猜测。 |
| 邮箱 | 是,email: test1@test.com | 邮箱属于用户身份信息,暴露后可能造成隐私泄露,也可能被用于撞库、钓鱼或用户枚举。 |
| 角色 | 是,role: customer | 角色字段会暴露当前用户权限级别。如果服务端错误信任客户端可控的角色字段,可能引发权限绕过。 |
| Basket ID | 是,bid: 9 | Basket ID 属于业务对象标识。若后端接口未校验购物车归属关系,可能引发水平越权访问。 |
| 密码哈希 | 是,password: 5a105e8b9d40e1329780d62ea2265d8a | 即使不是明文密码,密码哈希也不应返回给客户端。攻击者拿到哈希后可能尝试离线破解或彩虹表查询。 |
| TOTP / 二次验证字段 | 是,totpSecret: "" | 当前值为空,但字段本身不应暴露。如果真实环境中该字段存在有效值,会直接影响二次验证安全性。 |
| 其他敏感字段 | 是,lastLoginIp、deluxeToken、createdAt、updatedAt等 | 这些字段会暴露用户登录状态、账户时间信息或业务扩展信息。虽然部分字段当前为空,但仍不建议全部放入 JWT。 |
3.5 本节小结
通过对 JWT 的 Header 和 Payload 进行解码可以发现,Juice Shop 登录成功后返回的 Token 中包含了较完整的用户对象信息。
从认证流程角度看,该 JWT 可以作为客户端后续请求的身份凭证。客户端访问需要登录态的接口时,只需要在请求头中携带:
Authorization: Bearer <JWT_TOKEN>服务端即可根据该 Token 识别当前用户身份。
但是,从安全设计角度看,该 JWT Payload 中包含的信息过多,尤其是password密码哈希字段不应返回给客户端。JWT 的 Payload 只是编码,不是加密,因此不能把它当成安全存储空间。
更合理的做法是,JWT Payload 中只保留必要的最小身份声明,例如:
{"id":24,"role":"customer","iat":1790669116,"exp":1790672716}其中:
id用于标识用户身份。role用于辅助权限判断。iat表示签发时间。exp表示过期时间。
密码哈希、二次验证密钥、邮箱、登录 IP、完整用户对象、购物车 ID 等信息都不应直接放入 JWT Payload 中。
因此,本节可以得出初步结论:JWT 可以用于登录后的身份认证,但 JWT Payload 不应存放敏感数据。服务端应遵循最小化原则设计 Token 内容,并在后续业务接口中继续进行严格的权限校验。
四、Bearer Token 使用验证
本节选择当前登录用户的购物车接口作为验证目标:
GET /rest/basket/9 HTTP/1.1 Host: 127.0.0.1:3000其中,9来自前文 JWT Payload 中的bid字段,表示当前用户对应的 Basket ID。
为了验证服务端到底依赖哪个位置的 Token 识别登录身份,本节设计四组对照实验:
| 测试组 | Authorization Bearer Token | Cookie 中的 token | 测试目的 |
|---|---|---|---|
| 第一组 | 有 | 有 | 正常登录状态基线 |
| 第二组 | 无 | 无 | 验证无 Token 时是否被拒绝 |
| 第三组 | 有 | 无 | 验证 Authorization 头是否可单独完成认证 |
| 第四组 | 无 | 有 | 验证 Cookie 中的 token 是否可单独完成认证 |
需要说明的是,请求中的其他 Cookie,例如language、welcomebanner_status、continueCode等,主要与前端状态或靶场挑战进度有关,不作为本节认证测试的核心对象。本节重点观察的是Authorization: Bearer <JWT>和Cookie: token=<JWT>。
4.1 第一组:同时携带 Authorization 与 Cookie Token
第一组保留浏览器正常请求中的两处 Token:
GET /rest/basket/9 HTTP/1.1 Host: 127.0.0.1:3000 Authorization: Bearer <JWT_TOKEN> Cookie: ...; token=<JWT_TOKEN>服务端响应结果如下:
HTTP/1.1 200 OK Content-Type: application/json; charset=utf-8响应体中返回了购物车资源:
{"status":"success","data":{"id":9,"UserId":24,"Products":[{"id":51,"name":"浆果汁 (1000ml)","BasketItem":{"ProductId":51,"BasketId":9,"id":28,"quantity":1}}]}}| 检查项 | 结果 |
|---|---|
| HTTP 状态码 | 200 OK |
| 是否返回用户资源 | 是 |
| 返回资源归属 | Basket ID 为9,UserId 为24 |
| 结论 | 同时携带 Authorization 和 Cookie Token 时,请求可以正常访问当前用户购物车资源。 |
该组属于正常登录态下的基线请求,用于确认目标接口和 Token 本身有效。
4.2 第二组:不携带 Authorization,也不携带 Cookie Token
第二组删除Authorization请求头,同时删除 Cookie 中的token=<JWT>字段,仅保留其他普通 Cookie:
GET /rest/basket/9 HTTP/1.1 Host: 127.0.0.1:3000 Cookie: language=zh_CN; welcomebanner_status=dismiss; cookieconsent_status=dismiss; ...服务端响应结果如下:
HTTP/1.1 401 Unauthorized Content-Type: application/json; charset=utf-8响应体中返回错误信息:
{"error":{"message":"No Authorization header was found","name":"UnauthorizedError","code":"credentials_required","status":401,"inner":{"message":"No Authorization header was found"}}}| 检查项 | 结果 |
|---|---|
| HTTP 状态码 | 401 Unauthorized |
| 是否返回用户资源 | 否 |
| 错误信息 | No Authorization header was found |
| 结论 | 不携带有效认证凭证时,服务端拒绝访问购物车资源。 |
该结果说明/rest/basket/9不是公开接口,访问该接口需要认证凭证。服务端错误信息明确指出缺少Authorization请求头。
4.3 第三组:只携带 Authorization Bearer Token
第三组保留Authorization: Bearer <JWT>,但删除 Cookie 中的token=<JWT>字段:
GET /rest/basket/9 HTTP/1.1 Host: 127.0.0.1:3000 Authorization: Bearer <JWT_TOKEN> Cookie: language=zh_CN; welcomebanner_status=dismiss; cookieconsent_status=dismiss; ...服务端响应结果如下:
HTTP/1.1 200 OK Content-Type: application/json; charset=utf-8响应体中同样返回了当前用户购物车资源:
{"status":"success","data":{"id":9,"UserId":24,"Products":[{"id":51,"name":"浆果汁 (1000ml)","BasketItem":{"ProductId":51,"BasketId":9,"id":28,"quantity":1}}]}}| 检查项 | 结果 |
|---|---|
| HTTP 状态码 | 200 OK |
| 是否返回用户资源 | 是 |
| 返回资源归属 | Basket ID 为9,UserId 为24 |
| 结论 | 仅携带Authorization: Bearer <JWT>时,请求可以通过认证并访问当前用户资源。 |
该结果说明,对于该接口而言,Authorization请求头中的 Bearer Token 可以单独作为登录身份凭证使用。
4.4 第四组:只携带 Cookie Token
第四组删除Authorization请求头,仅保留 Cookie 中的token=<JWT>字段:
GET /rest/basket/9 HTTP/1.1 Host: 127.0.0.1:3000 Cookie: language=zh_CN; ...; token=<JWT_TOKEN>服务端响应结果如下:
HTTP/1.1 401 Unauthorized Content-Type: application/json; charset=utf-8响应体中返回错误信息:
{"error":{"message":"No Authorization header was found","name":"UnauthorizedError","code":"credentials_required","status":401,"inner":{"message":"No Authorization header was found"}}}| 检查项 | 结果 |
|---|---|
| HTTP 状态码 | 401 Unauthorized |
| 是否返回用户资源 | 否 |
| Cookie 中是否存在 token | 是 |
| 错误信息 | No Authorization header was found |
| 结论 | 仅 Cookie 中存在token=<JWT>时,服务端仍然拒绝访问,说明该接口没有使用 Cookie token 完成认证。 |
该组实验比较关键。虽然请求中确实携带了Cookie: token=<JWT>,但是服务端仍然返回401 Unauthorized,并提示No Authorization header was found。这说明在当前接口的认证逻辑中,后端主要检查的是Authorization请求头,而不是 Cookie 中的 token 字段。
4.5 四组结果汇总
| 测试组 | Authorization Bearer Token | Cookie token | HTTP 状态码 | 是否返回购物车资源 | 结论 |
|---|---|---|---|---|---|
| 第一组 | 有 | 有 | 200 OK | 是 | 正常登录态请求成功 |
| 第二组 | 无 | 无 | 401 Unauthorized | 否 | 无认证凭证,请求被拒绝 |
| 第三组 | 有 | 无 | 200 OK | 是 | Authorization Bearer Token 可以单独完成认证 |
| 第四组 | 无 | 有 | 401 Unauthorized | 否 | Cookie token 不能单独完成该接口认证 |
4.6 本节小结
通过四组对照实验可以得出以下结论:
第一,/rest/basket/9是需要认证后才能访问的用户资源接口。不携带有效认证凭证时,服务端会返回401 Unauthorized。
第二,Authorization: Bearer <JWT>可以单独完成该接口的身份认证。当请求头中存在有效 Bearer Token 时,服务端能够识别当前用户身份,并返回对应的购物车资源。
第三,Cookie 中的token=<JWT>不能单独完成该接口认证。第四组实验中,即使 Cookie 中存在 JWT,服务端仍然返回No Authorization header was found,说明当前接口的后端认证逻辑主要依赖Authorization请求头。
第四,第一组同时携带 Authorization 和 Cookie Token 能够请求成功,但结合第三组和第四组结果可以判断,请求成功的关键因素是Authorization: Bearer <JWT>,而不是 Cookie 中的token=<JWT>。
因此,在本实验中,Bearer Token 的作用可以概括为:客户端在登录成功后,将服务端签发的 JWT 放入Authorization请求头中,后端通过该 Token 识别当前登录用户。Cookie 中即使保存了 token,也不代表后端一定会使用它进行接口认证,具体是否生效取决于服务端认证中间件的实现方式。
这里也进一步说明了 Authentication 与 Authorization 的区别:
Authorization请求头是 HTTP 中传递认证凭证的位置。- Bearer Token 是客户端提交给服务端的身份凭证。
- Authentication 解决“你是谁”的问题。
- Authorization 解决“你是否有权限访问该资源”的问题。
在本节实验中,服务端首先通过 Bearer Token 完成 Authentication,识别当前用户为UserId: 24;随后返回该用户对应的Basket ID: 9资源。
五、Authentication 与 Authorization 区分
在分析 JWT 和 Bearer Token 时,需要区分两个概念:Authentication 和 Authorization。
Authentication:身份认证,解决“你是谁”的问题。 Authorization:权限校验,解决“你能访问什么”的问题。虽然请求头名称是Authorization,但其中携带的Bearer Token首先用于让服务端识别当前用户身份。服务端识别出用户之后,还需要继续判断该用户是否有权限访问具体资源。
5.1 Authentication
Authentication 指身份认证,即服务端确认当前请求来自哪个用户。
在本实验中,身份认证主要体现在两处:
第一,用户通过登录接口提交邮箱和密码,服务端校验成功后返回 JWT:
{"authentication":{"token":"<JWT_TOKEN>","bid":9,"umail":"test1@test.com"}}第二,客户端在访问购物车接口时携带:
Authorization: Bearer <JWT_TOKEN>第四章实验中,携带有效 Bearer Token 时,请求/rest/basket/9返回200 OK,并返回UserId: 24、Basket ID: 9等用户相关资源;不携带Authorization请求头时,服务端返回401 Unauthorized,错误信息为:
{"message":"No Authorization header was found","code":"credentials_required"}因此,本实验中可以判断:服务端通过Authorization: Bearer <JWT_TOKEN>识别当前登录用户。这个过程属于 Authentication。
5.2 Authorization
Authorization 指权限校验,即服务端判断当前用户是否有权访问某个具体资源。
认证成功只说明“当前请求来自哪个用户”,并不代表该用户可以访问所有资源。
例如,当前 JWT Payload 中可以观察到:
{"data":{"id":24,"email":"test1@test.com","role":"customer"},"bid":9}这说明当前登录用户是UserId: 24,角色为普通用户,对应购物车为Basket ID: 9。
当用户访问自己的购物车时:
GET /rest/basket/9 HTTP/1.1 Authorization: Bearer <JWT_TOKEN>服务端返回对应资源:
{"status":"success","data":{"id":9,"UserId":24}}这说明当前用户不仅完成了身份认证,而且访问的是与自己身份匹配的资源。
但是,如果将请求中的资源 ID 修改为其他用户的 Basket ID,例如:
GET /rest/basket/<OTHER_BASKET_ID> HTTP/1.1 Authorization: Bearer <JWT_TOKEN>此时服务端不能只检查 Token 是否有效,还必须继续判断:
当前 Token 对应的 UserId,是否有权访问这个 Basket ID?如果服务端只验证“用户已登录”,却没有校验“资源是否属于该用户”,就可能产生越权访问问题。
因此,Authorization 的核心不是有没有登录,而是:
登录用户是否有权限访问当前请求的具体资源。5.3 二者区别总结
| 对比项 | Authentication | Authorization |
|---|---|---|
| 中文含义 | 身份认证 | 权限校验 |
| 解决的问题 | 你是谁 | 你能访问什么 |
| 本实验中的体现 | Bearer Token 让服务端识别当前用户为UserId: 24 | 服务端应判断UserId: 24是否有权访问Basket ID: 9 |
| 失败时常见状态码 | 401 Unauthorized | 403 Forbidden或业务层权限拒绝 |
| 典型风险 | Token 泄露、JWT 敏感字段暴露、签名校验错误 | 水平越权、垂直越权、IDOR、角色权限绕过 |
5.4 本节小结
本实验中,Bearer Token 主要用于身份认证。服务端通过Authorization: Bearer <JWT_TOKEN>识别当前用户身份。
但身份认证成功并不等于拥有所有资源的访问权限。对于购物车、订单、用户信息、后台管理等接口,服务端还必须继续进行权限校验,确认当前用户是否有权访问目标资源。
可以简单概括为:
Authentication:证明“我是 UserId 24”。 Authorization:证明“UserId 24 可以访问 Basket ID 9”。如果系统只做 Authentication,而忽略 Authorization,就可能导致越权访问漏洞。
六、实验总结
本次实验围绕 OWASP Juice Shop 的登录认证流程和 JWT 使用方式展开,主要分析了登录成功后服务端如何返回 Token、客户端如何通过 Bearer Token 表示登录身份,以及 JWT Payload 中是否存在敏感字段。
通过实验可以确认,用户登录成功后,服务端会返回 JWT。客户端后续访问需要登录身份的接口时,需要在请求头中携带:
Authorization: Bearer <JWT_TOKEN>在/rest/basket/9接口的四组对照实验中,只有携带Authorization: Bearer <JWT_TOKEN>的请求可以正常返回购物车资源;不携带 Authorization 请求头时,即使 Cookie 中存在token=<JWT>,服务端仍然返回401 Unauthorized,并提示:
{"message":"No Authorization header was found","code":"credentials_required"}因此可以判断,在该接口中,请求成功的关键因素是Authorization请求头中的 Bearer Token,而不是 Cookie 中的 token 字段。
同时,通过 JWT 解码可以发现,JWT Payload 中包含了用户 ID、邮箱、角色、Basket ID、账户状态字段以及密码哈希字段。由于 JWT Payload 只是 Base64URL 编码,并不是加密,任何获得 Token 的人都可以解码查看其中内容。因此,JWT 中不应存放密码哈希、TOTP Secret、完整用户对象等敏感信息。
本实验还进一步说明了 Authentication 和 Authorization 的区别。Bearer Token 主要用于身份认证,即让服务端识别“当前请求来自哪个用户”;但用户完成认证并不代表可以访问所有资源。对于购物车、订单、用户信息、后台管理等接口,服务端仍然需要继续进行权限校验,判断当前用户是否有权访问目标资源。
综上,本次实验可以得出以下结论:
1. JWT 可以作为登录后的身份凭证。 2. Bearer Token 通过 Authorization 请求头传递。 3. JWT Payload 可以被解码查看,不应存放敏感信息。 4. Cookie 中存在 token 不代表接口一定会使用它完成认证。 5. Authentication 只解决“你是谁”的问题。 6. Authorization 还需要判断“你能访问什么”。 7. 只做身份认证、不做资源级权限校验,可能导致越权访问漏洞。针对上述问题,实际开发中应遵循以下安全原则:
1. JWT Payload 只保留必要字段,例如用户 ID、角色、签发时间和过期时间。 2. 不在 JWT 中存放密码哈希、二次验证密钥、完整用户对象等敏感信息。 3. 服务端必须校验 JWT 签名,不能只解码 Payload 后直接信任其中内容。 4. Token 应设置合理过期时间,降低泄露后的风险。 5. 每个敏感业务接口都应进行资源归属校验和权限校验。本次实验的核心收获是:JWT 和 Bearer Token 解决的是登录后的身份识别问题,但接口安全不能只依赖“用户已登录”。真正安全的后端接口还需要在认证之后继续进行严格的权限校验。