在使用 Postman 调试 OAuth2 的 client_credentials 授权流程时,我遇到了一个经典但隐蔽的问题:服务端返回 {"error":"unauthorized_client"}。本文记录排查过程、定位根因以及完整解决方案,希望对同样在做 OAuth2 接入与调试的你有帮助。
背景
- 授权方式:OAuth2 client_credentials
- 工具:Postman
- 目标:在本地授权服务
http://localhost:9000/oauth2/token获取 access_token - 请求(初始版本):
- 方法:POST
- Headers:
Authorization: Basic Y2xpZW50LW1hbmFnZTpzZWNyZXQ=(即client-manage:secret) - Body(x-www-form-urlencoded):
- grant_type: client_credentials
- scope: client:read
- scope: client:write
问题现象
- 响应:
{ "error": "unauthorized_client" } - 服务端日志(Spring):
Securing POST /oauth2/token DEBUG ... OAuth2ClientAuthenticationFilter : Set SecurityContextHolder authentication to OAuth2ClientAuthenticationToken DEBUG ... DefaultAuthenticationEventPublisher : No event was found for the exception org.springframework.security.oauth2.core.OAuth2AuthenticationException
表面看来是客户端未被授权,但具体原因并不直观。
排查思路
从客户端请求与服务端授权配置两条线并行排查:
1) 客户端请求检查
- Authorization 头是否正确:Basic 后面应为
Base64(client_id:client_secret)。解码确认为client-manage:secret,无误。 - grant_type 是否正确:为
client_credentials,无误。 - scope 参数格式是否正确:发现 Postman 中将 scope 写成了两行:
这在 OAuth2 规范中并非标准用法。规范要求多个 scope 以空格分隔,放在一个参数里:scope=client:read scope=client:writescope=client:read client:write
许多授权服务器会只取第一个同名参数,导致实际生效的 scope 不完整或无效,从而在后续校验中触发 unauthorized_client 或 invalid_scope 等错误。
2) 服务端配置检查(以 Spring Authorization Server 为例)
- 客户端是否允许使用 client_credentials 授权类型
- 客户端是否允许请求的 scope(client:read、client:write)
- 客户端是否处于启用状态
- client_id / client_secret 是否匹配
示例注册代码(参考):
RegisteredClient registeredClient = RegisteredClient.withId(UUID.randomUUID().toString())
.clientId("client-manage")
.clientSecret("{noop}secret")
.authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS)
.scope("client:read")
.scope("client:write")
.build();
根因定位
最终确认根因是“scope 参数格式错误”:在 x-www-form-urlencoded 中分成了两行同名参数。修改为单行、以空格分隔后,请求立即正常返回。
正确请求示例(Postman)
- Headers:
- Authorization: Basic Y2xpZW50LW1hbmFnZTpzZWNyZXQ= (即 client-manage:secret)
- Content-Type: application/x-www-form-urlencoded(Postman 会自动设置)
- Body(x-www-form-urlencoded,仅两行):
- grant_type: client_credentials
- scope: client:read client:write
返回示例:
{
"access_token": "eyJraWQiOiJlMjYyYzVmMS1iZWE2LTQzMzQtYWE1...<truncated>",
"scope": "client:write client:read",
"token_type": "Bearer",
"expires_in": 3600
}
常见坑总结
- 不要在表单里重复 key 为 scope 多次传值;多个 scope 请用空格分隔,放在同一个 scope 参数中。
- 确认客户端配置允许所用的授权类型(client_credentials)。
- 确认客户端允许请求的 scopes。
- base64 编码一定是
client_id:client_secret的原始字节;不要额外带空格或换行。 - 出现
unauthorized_client或invalid_scope时,服务端日志往往能给出关键线索。适当提高日志级别能更快定位。
调试建议
- 在 Postman 中保存成功示例为 Example,便于回归与 Mock。
- 在请求的 Tests 脚本里添加断言,校验
status === 200且token_type === "Bearer",并把access_token存入环境变量,便于后续 API 复用:pm.test("Status is 200", () => pm.response.code === 200); const json = pm.response.json(); pm.environment.set("access_token", json.access_token); pm.test("Has bearer token", () => json.token_type === "Bearer"); - 后续请求直接用:
Authorization: Bearer {{access_token}}
结语
这次问题的关键在于“细节”:scope 参数的格式。OAuth2 的许多问题并不复杂,但很容易被这些细节绊倒。希望这篇排查记录能帮你在对接 OAuth2 时少走一些弯路。


8080

被折叠的 条评论
为什么被折叠?



