面试题答案
一键面试实现OAuth 2.0认证流程的主要步骤
- 注册应用:在授权服务器上注册你的应用,获取客户端ID(
clientID
)和客户端密钥(clientSecret
)。 - 重定向用户到授权端点:构造一个URL,包含客户端ID、重定向URI、范围等参数,将用户引导至授权服务器的授权端点。用户在该端点登录并授权你的应用访问API。
- 获取授权码:授权服务器验证用户并授权后,会将用户重定向回你指定的重定向URI,并在URL中附带授权码(
authorizationCode
)。 - 用授权码换取访问令牌:使用授权码、客户端ID和客户端密钥向授权服务器的令牌端点发送POST请求,换取访问令牌(
accessToken
)和刷新令牌(refreshToken
,可选)。 - 使用访问令牌访问API:在HTTP请求的
Authorization
头中带上Bearer {accessToken}
来访问需要认证的API。
关键代码片段示例
package main
import (
"bytes"
"encoding/json"
"fmt"
"io/ioutil"
"log"
"net/http"
)
// TokenResponse 用于解析令牌响应
type TokenResponse struct {
AccessToken string `json:"access_token"`
RefreshToken string `json:"refresh_token"`
ExpiresIn int `json:"expires_in"`
}
// 获取访问令牌
func getAccessToken(clientID, clientSecret, authorizationCode, redirectURI, tokenEndpoint string) (string, string, error) {
data := map[string]string{
"grant_type": "authorization_code",
"client_id": clientID,
"client_secret": clientSecret,
"code": authorizationCode,
"redirect_uri": redirectURI,
}
jsonData, err := json.Marshal(data)
if err != nil {
return "", "", err
}
resp, err := http.Post(tokenEndpoint, "application/json", bytes.NewBuffer(jsonData))
if err != nil {
return "", "", err
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return "", "", err
}
var tokenResp TokenResponse
err = json.Unmarshal(body, &tokenResp)
if err != nil {
return "", "", err
}
return tokenResp.AccessToken, tokenResp.RefreshToken, nil
}
// 使用访问令牌访问API
func callAPI(accessToken, apiURL string) {
client := &http.Client{}
req, err := http.NewRequest("GET", apiURL, nil)
if err != nil {
log.Fatal(err)
}
req.Header.Set("Authorization", "Bearer "+accessToken)
resp, err := client.Do(req)
if err != nil {
log.Fatal(err)
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
log.Fatal(err)
}
fmt.Println(string(body))
}
你可以这样调用上述函数:
func main() {
clientID := "your_client_id"
clientSecret := "your_client_secret"
authorizationCode := "the_code_from_authorization_server"
redirectURI := "your_redirect_uri"
tokenEndpoint := "the_token_endpoint_url"
apiURL := "the_api_url_you_want_to_access"
accessToken, refreshToken, err := getAccessToken(clientID, clientSecret, authorizationCode, redirectURI, tokenEndpoint)
if err != nil {
log.Fatal(err)
}
fmt.Printf("Access Token: %s\nRefresh Token: %s\n", accessToken, refreshToken)
callAPI(accessToken, apiURL)
}
处理认证令牌的刷新
- 检测令牌过期:当API返回401 Unauthorized错误,或者你跟踪了令牌的过期时间(
expiresIn
字段),发现令牌已过期时,需要刷新令牌。 - 使用刷新令牌:向授权服务器的令牌端点发送POST请求,带上
grant_type=refresh_token
、客户端ID、客户端密钥和刷新令牌,获取新的访问令牌和(可能的)新刷新令牌。 - 更新令牌:用新的访问令牌替换旧的访问令牌,并保存新的刷新令牌(如果有更新)。
- 重试API请求:使用新的访问令牌重新发起之前失败的API请求。
以下是刷新令牌的代码示例:
// 刷新访问令牌
func refreshAccessToken(clientID, clientSecret, refreshToken, tokenEndpoint string) (string, string, error) {
data := map[string]string{
"grant_type": "refresh_token",
"client_id": clientID,
"client_secret": clientSecret,
"refresh_token": refreshToken,
}
jsonData, err := json.Marshal(data)
if err != nil {
return "", "", err
}
resp, err := http.Post(tokenEndpoint, "application/json", bytes.NewBuffer(jsonData))
if err != nil {
return "", "", err
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return "", "", err
}
var tokenResp TokenResponse
err = json.Unmarshal(body, &tokenResp)
if err != nil {
return "", "", err
}
return tokenResp.AccessToken, tokenResp.RefreshToken, nil
}
在callAPI
函数中处理401错误并刷新令牌:
func callAPIWithRefresh(accessToken, refreshToken, clientID, clientSecret, tokenEndpoint, apiURL string) {
client := &http.Client{}
req, err := http.NewRequest("GET", apiURL, nil)
if err != nil {
log.Fatal(err)
}
req.Header.Set("Authorization", "Bearer "+accessToken)
resp, err := client.Do(req)
if err != nil {
log.Fatal(err)
}
defer resp.Body.Close()
if resp.StatusCode == http.StatusUnauthorized {
newAccessToken, newRefreshToken, err := refreshAccessToken(clientID, clientSecret, refreshToken, tokenEndpoint)
if err != nil {
log.Fatal(err)
}
// 更新令牌
accessToken = newAccessToken
refreshToken = newRefreshToken
// 重试请求
req.Header.Set("Authorization", "Bearer "+accessToken)
resp, err = client.Do(req)
if err != nil {
log.Fatal(err)
}
defer resp.Body.Close()
}
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
log.Fatal(err)
}
fmt.Println(string(body))
}
在main
函数中调用:
func main() {
clientID := "your_client_id"
clientSecret := "your_client_secret"
accessToken := "your_initial_access_token"
refreshToken := "your_initial_refresh_token"
tokenEndpoint := "the_token_endpoint_url"
apiURL := "the_api_url_you_want_to_access"
callAPIWithRefresh(accessToken, refreshToken, clientID, clientSecret, tokenEndpoint, apiURL)
}