
本文详解 Xero API 中 offline_access 的实际应用,重点解决调用刷新令牌时出现 unsupported_grant_type 错误的原因,并演示如何通过 League OAuth2 客户端正确获取新的访问令牌(access token)及可复用的刷新令牌(refresh token)。
本文详解 xero api 中 `offline_access` 的实际应用,重点解决调用刷新令牌时出现 `unsupported_grant_type` 错误的原因,并演示如何通过 league oauth2 客户端正确获取新的访问令牌(access token)及**可复用的刷新令牌(refresh token)**。
在 Xero OAuth2 授权流程中,启用 offline_access 权限是获得长期有效刷新令牌(refresh token)的前提。该权限需在初始授权请求(Authorization URL)中显式声明,例如:
$authUrl = $provider->getAuthorizationUrl([
'scope' => 'openid profile email accounting.transactions offline_access'
]);⚠️ 注意:仅在应用设置中勾选 offline_access 并不生效——必须在每次授权请求的 scope 参数中包含 offline_access,Xero 才会返回可用于长期刷新的 refresh token。
你当前遇到的 unsupported_grant_type 错误,根本原因在于手动构造的 Guzzle 请求存在两个关键问题:
-
请求体格式错误:Xero 要求
application/x-www-form-urlencoded格式,但你传递的是 JSON 字符串('body' => '{"grant_type":...}'),服务器无法解析; -
缺少必要参数:除
grant_type和refresh_token外,还必须提供client_id(部分场景下需client_secret,取决于客户端类型);且Basic Auth头中的$authpas必须是base64_encode("client_id:client_secret")。
✅ 正确做法是优先使用已封装好的 OAuth2 客户端库(如 league/oauth2-client),它自动处理编码、头信息和参数校验:
use League\OAuth2\Client\Provider\GenericProvider;
$provider = new GenericProvider([
'clientId' => $client,
'clientSecret' => $secret,
'redirectUri' => 'https://site/xeroreports',
'urlAuthorize' => 'https://login.xero.com/identity/connect/authorize',
'urlAccessToken' => 'https://identity.xero.com/connect/token',
'urlResourceOwnerDetails' => 'https://identity.xero.com/resources',
]);
// ✅ 正确:使用 refresh_token 授权类型获取新令牌
try {
$newAccessToken = $provider->getAccessToken('refresh_token', [
'refresh_token' => $xero_refresh,
]);
// ? 关键:$newAccessToken 是对象,不是字符串!
$accessToken = $newAccessToken->getToken(); // 新的 access_token(JWT)
$refreshToken = $newAccessToken->getRefreshToken(); // 新的 refresh_token(可能轮换)
$expires = $newAccessToken->getExpires(); // 过期时间戳(秒级)
echo "Access Token: {$accessToken}\n";
echo "Refresh Token: {$refreshToken}\n"; // ✨ 这才是你要的新 refresh token
} catch (\Exception $e) {
error_log("Token refresh failed: " . $e->getMessage());
}? 重要说明:
- Xero 在每次成功刷新后可能返回一个新的 refresh token(并非总是不变),因此务必每次刷新后都持久化存储
getRefreshToken()的返回值; -
offline_access授予的 refresh token 默认有效期为 60 天,且每次刷新会重置过期时间(即“滚动过期”); - 若手动使用 Guzzle,请严格按 Xero 官方文档 构造请求:
$authHeader = base64_encode("{$client}:{$secret}");
$response = $client->request('POST', 'https://identity.xero.com/connect/token', [
'form_params' => [
'grant_type' => 'refresh_token',
'refresh_token' => $xero_refresh,
'client_id' => $client,
],
'headers' => [
'Authorization' => 'Basic ' . $authHeader,
'Content-Type' => 'application/x-www-form-urlencoded',
],
]);? 总结:避免 unsupported_grant_type 的核心是——不用 JSON body,用 form_params 提交,确保 grant_type=refresh_token 且认证头正确;而最佳实践是信任成熟 OAuth2 客户端,直接调用 getAccessToken('refresh_token', [...]) 并通过对象方法提取 getRefreshToken(),安全、简洁、符合规范。

















