GO示例

Go 示例(标准库 + net/http)

go运行

package main

import (
	"crypto/rand"
	"encoding/base64"
	"encoding/json"
	"fmt"
	"io/ioutil"
	"net/http"
	"net/url"
	"strings"
	"sync"
)

// 全局配置
var (
	clientID     = "YOUR_CLIENT_ID"
	clientSecret = "YOUR_CLIENT_SECRET"
	redirectURI  = "https://your-domain.com/callback"
	authURL      = "https://id.rutno.com/rn-oauth/authorize"
	tokenURL     = "https://id.rutno.com/rn-oauth/token"
	userInfoURL  = "https://id.rutno.com/rn-oauth/userinfo"

	// 简易session存储(生产环境建议用Redis)
	stateStore = sync.Map{}
)

// TokenResponse Token响应结构体
type TokenResponse struct {
	AccessToken  string `json:"access_token"`
	TokenType    string `json:"token_type"`
	ExpiresIn    int    `json:"expires_in"`
	RefreshToken string `json:"refresh_token"`
	Scope        string `json:"scope"`
}

// UserInfo 用户信息结构体
type UserInfo struct {
	ID       string `json:"id"`
	Username string `json:"username"`
	Nickname string `json:"nickname"`
	Email    string `json:"email"`
	Avatar   string `json:"avatar"`
}

// 生成随机state
func generateState() string {
	b := make([]byte, 16)
	rand.Read(b)
	return base64.URLEncoding.EncodeToString(b)
}

// 步骤1:授权页面跳转
func authHandler(w http.ResponseWriter, r *http.Request) {
	// 生成并存储state
	state := generateState()
	stateStore.Store(state, true)

	// 构建授权URL
	params := url.Values{}
	params.Set("client_id", clientID)
	params.Set("redirect_uri", redirectURI)
	params.Set("response_type", "code")
	params.Set("scope", "username email avatar")
	params.Set("state", state)

	authFullURL := authURL + "?" + params.Encode()
	http.Redirect(w, r, authFullURL, http.StatusTemporaryRedirect)
}

// 步骤2:回调处理
func callbackHandler(w http.ResponseWriter, r *http.Request) {
	// 1. 校验state
	state := r.URL.Query().Get("state")
	if state == "" {
		http.Error(w, "State参数缺失", http.StatusBadRequest)
		return
	}

	// 检查state是否存在
	_, exists := stateStore.LoadAndDelete(state) // 一次性使用
	if !exists {
		http.Error(w, "State验证失败,可能是CSRF攻击", http.StatusForbidden)
		return
	}

	// 2. 处理错误
	if err := r.URL.Query().Get("error"); err != "" {
		http.Error(w, "授权失败:"+err, http.StatusBadRequest)
		return
	}

	// 3. 获取code
	code := r.URL.Query().Get("code")
	if code == "" {
		http.Error(w, "授权码缺失", http.StatusBadRequest)
		return
	}

	// 4. 换取Token
	tokenResp, err := getAccessToken(code)
	if err != nil {
		http.Error(w, "获取Token失败:"+err.Error(), http.StatusInternalServerError)
		return
	}

	// 5. 获取用户信息
	userInfo, err := getUserInfo(tokenResp.AccessToken)
	if err != nil {
		http.Error(w, "获取用户信息失败:"+err.Error(), http.StatusInternalServerError)
		return
	}

	// 6. 业务处理(示例:返回用户信息)
	w.Header().Set("Content-Type", "application/json")
	json.NewEncoder(w).Encode(userInfo)
}

// 换取Access Token
func getAccessToken(code string) (*TokenResponse, error) {
	// 构建POST参数
	params := url.Values{}
	params.Set("grant_type", "authorization_code")
	params.Set("client_id", clientID)
	params.Set("client_secret", clientSecret)
	params.Set("code", code)
	params.Set("redirect_uri", redirectURI)

	// 发送请求
	resp, err := http.PostForm(tokenURL, params)
	if err != nil {
		return nil, fmt.Errorf("请求Token接口失败:%v", err)
	}
	defer resp.Body.Close()

	// 检查状态码
	if resp.StatusCode != http.StatusOK {
		body, _ := ioutil.ReadAll(resp.Body)
		return nil, fmt.Errorf("Token接口返回异常:%d,响应:%s", resp.StatusCode, string(body))
	}

	// 解析响应
	var tokenResp TokenResponse
	if err := json.NewDecoder(resp.Body).Decode(&tokenResp); err != nil {
		return nil, fmt.Errorf("解析Token响应失败:%v", err)
	}

	if tokenResp.AccessToken == "" {
		return nil, fmt.Errorf("Token为空")
	}

	return &tokenResp, nil
}

// 获取用户信息
func getUserInfo(accessToken string) (*UserInfo, error) {
	// 构建请求
	req, err := http.NewRequest("GET", userInfoURL, nil)
	if err != nil {
		return nil, fmt.Errorf("创建请求失败:%v", err)
	}

	// 设置Authorization头
	req.Header.Set("Authorization", "Bearer "+accessToken)

	// 发送请求
	client := &http.Client{}
	resp, err := client.Do(req)
	if err != nil {
		return nil, fmt.Errorf("请求用户信息接口失败:%v", err)
	}
	defer resp.Body.Close()

	// 检查状态码
	if resp.StatusCode != http.StatusOK {
		body, _ := ioutil.ReadAll(resp.Body)
		return nil, fmt.Errorf("用户信息接口返回异常:%d,响应:%s", resp.StatusCode, string(body))
	}

	// 解析响应
	var userInfo UserInfo
	if err := json.NewDecoder(resp.Body).Decode(&userInfo); err != nil {
		return nil, fmt.Errorf("解析用户信息失败:%v", err)
	}

	return &userInfo, nil
}

func main() {
	// 注册路由
	http.HandleFunc("/auth/rutno", authHandler)
	http.HandleFunc("/callback", callbackHandler)

	// 启动服务
	fmt.Println("服务器启动在 :8080 端口")
	http.ListenAndServe(":8080", nil)
}

Go 使用说明

  1. Go 版本要求:1.18+
  2. 生产环境优化建议:使用 sync.Map 替换为 Redis 存储 state增加 HTTP 客户端超时配置增加重试机制使用 context 控制请求生命周期