You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Go CLI中Auth0设备授权流轮询请求令牌遇400/401错误求助

问题分析与解决方案

核心问题根源

初始代码问题

重复复用同一个*http.Request对象是错误的——http.Client.Do()会读取请求体,第一次调用后strings.Reader的读取指针已移至末尾,后续请求的请求体为空,导致Cloudflare返回400 Bad Request。

修改后代码问题

虽然把请求构造移到了循环内,但复用了同一个*strings.Reader作为payload,同样触发了读取指针问题:第一次读取后指针到末尾,后续请求的body为空,Auth0因此返回401 Unauthorized。此外,代码错误地将400状态码直接判定为终止轮询的条件,但Auth0的authorization_pending(用户尚未授权)本身就是400状态,应该继续轮询而非停止。

修正方案

  1. 每次轮询重新创建请求体:不要复用strings.Reader,每次循环都重新构造strings.NewReader(),确保请求体完整。
  2. 精准处理Auth0错误响应:解析响应JSON内容,根据error字段判断状态:
    • authorization_pending:用户未授权,继续轮询
    • slow_down:将轮询间隔增加5秒后继续轮询
    • access_denied:用户拒绝授权,终止并返回错误
    • expired_token:设备码过期,终止并返回错误
  3. 复用HTTP Client:在循环外创建http.Client,复用连接池提升效率,但必须每次重新构造请求对象和请求体。

修正后的代码示例

import (
	"encoding/json"
	"fmt"
	"io"
	"log"
	"net/http"
	"strings"
	"time"
)

// 定义Auth0轮询错误响应结构体
type PollErrorResponse struct {
	Error            string `json:"error"`
	ErrorDescription string `json:"error_description"`
}

// Gets a request token.
func (loginJob *LoginJob) GetRequestToken(deviceCodeData loginResponse.LResponse) error {
	url := loginJob.Domain + "/oauth/token"
	method := "POST"
	authenticate := false
	currentInterval := deviceCodeData.Interval

	// 复用http.Client,避免重复创建连接
	client := &http.Client{
		Timeout: time.Second * 10,
	}

	for !authenticate {
		// 每次循环重新构造payload,确保请求体完整
		payload := strings.NewReader(fmt.Sprintf(
			"grant_type=urn:ietf:params:oauth:grant-type:device_code&device_code=%s&client_id=%s",
			deviceCodeData.DeviceCode,
			loginJob.ClientID,
		))

		req, reqErr := http.NewRequest(method, url, payload)
		if reqErr != nil {
			log.Printf("创建请求失败: %v", reqErr)
			return reqErr
		}
		req.Header.Add("Content-Type", "application/x-www-form-urlencoded")

		res, resErr := client.Do(req)
		if resErr != nil {
			log.Printf("请求失败: %v", resErr)
			return resErr
		}
		defer res.Body.Close()

		body, readErr := io.ReadAll(res.Body)
		if readErr != nil {
			log.Printf("读取响应失败: %v", readErr)
			return readErr
		}

		switch res.StatusCode {
		case http.StatusOK:
			fmt.Println("Authenticated!")
			// 解析并处理token
			var tokenResp struct {
				IDToken string `json:"id_token"`
				// 按需添加其他字段(如access_token等)
			}
			if err := json.Unmarshal(body, &tokenResp); err == nil {
				fmt.Printf("- Id Token: %s\n", tokenResp.IDToken)
			}
			authenticate = true
		case http.StatusBadRequest:
			var errResp PollErrorResponse
			if err := json.Unmarshal(body, &errResp); err == nil {
				switch errResp.Error {
				case "authorization_pending":
					fmt.Println("用户尚未授权,继续轮询...")
				case "slow_down":
					fmt.Println("收到slow_down指令,增加轮询间隔5秒")
					currentInterval += 5
				case "expired_token":
					return fmt.Errorf("设备码已过期: %s", errResp.ErrorDescription)
				case "access_denied":
					return fmt.Errorf("用户拒绝授权: %s", errResp.ErrorDescription)
				default:
					return fmt.Errorf("未知错误: %s - %s", errResp.Error, errResp.ErrorDescription)
				}
			} else {
				return fmt.Errorf("解析错误响应失败: %v", err)
			}
		default:
			return fmt.Errorf("意外状态码: %d, 响应内容: %s", res.StatusCode, string(body))
		}

		time.Sleep(time.Duration(currentInterval) * time.Second)
	}
	return nil
}

关键说明

  • 请求体重构:每次循环用strings.NewReader()生成新请求体,彻底避免读取指针位置问题。
  • 错误响应解析:不再仅依赖状态码,而是解析Auth0返回的错误字段,精准处理不同授权状态。
  • 动态调整轮询间隔:支持slow_down指令,符合Auth0的规范要求。
  • Client复用:循环外创建http.Client,复用连接池提升请求效率。

内容的提问来源于stack exchange,提问作者nicktsan

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.27 01:35:55