## 实现OAuth2令牌存储接口 在上一个小节中,我们为客户端检索了一个令牌并执行了API请求。这种方法的缺点是我们的令牌没有长期存储空间。例如,在HTTP服务器中,我们希望在请求之间保持一致的令牌存储。 本节将探讨修改OAuth2客户端以在请求之间存储令牌并使用密钥检索它们。为简单起见,此密钥将是一个文件,但它也可以是数据库,Redis等。 ### 实践 1. 建立 config.go: ``` package oauthstore import ( "context" "net/http" "golang.org/x/oauth2" ) // Config 包含了 oauth2.Config和 Storage接口 type Config struct { *oauth2.Config Storage } // Exchange 在接收到令牌后将其存储 func (c *Config) Exchange(ctx context.Context, code string) (*oauth2.Token, error) { token, err := c.Config.Exchange(ctx, code) if err != nil { return nil, err } if err := c.Storage.SetToken(token); err != nil { return nil, err } return token, nil } // TokenSource 可以传递已被存储的令牌 // 或当新令牌被接收时将其转换为oauth2.TokenSource func (c *Config) TokenSource(ctx context.Context, t *oauth2.Token) oauth2.TokenSource { return StorageTokenSource(ctx, c, t) } // Client 附加到TokenSource func (c *Config) Client(ctx context.Context, t *oauth2.Token) *http.Client { return oauth2.NewClient(ctx, c.TokenSource(ctx, t)) } ``` 2. 建立 tokensource.go: ``` package oauthstore import ( "context" "golang.org/x/oauth2" ) type storageTokenSource struct { *Config oauth2.TokenSource } // Token满足TokenSource接口 func (s *storageTokenSource) Token() (*oauth2.Token, error) { if token, err := s.Config.Storage.GetToken(); err == nil && token.Valid() { return token, err } token, err := s.TokenSource.Token() if err != nil { return token, err } if err := s.Config.Storage.SetToken(token); err != nil { return nil, err } return token, nil } // StorageTokenSource 将由config.TokenSource方法调用 func StorageTokenSource(ctx context.Context, c *Config, t *oauth2.Token) oauth2.TokenSource { if t == nil || !t.Valid() { if tok, err := c.Storage.GetToken(); err == nil { t = tok } } ts := c.Config.TokenSource(ctx, t) return &storageTokenSource{c, ts} } ``` 3. 建立 storage.go: ``` package oauthstore import ( "context" "fmt" "golang.org/x/oauth2" ) // Storage 是我们的通用存储接口 type Storage interface { GetToken() (*oauth2.Token, error) SetToken(*oauth2.Token) error } // GetToken 检索github oauth2令牌 func GetToken(ctx context.Context, conf Config) (*oauth2.Token, error) { token, err := conf.Storage.GetToken() if err == nil && token.Valid() { return token, err } url := conf.AuthCodeURL("state") fmt.Printf("Type the following url into your browser and follow the directions on screen: %v\n", url) fmt.Println("Paste the code returned in the redirect URL and hit Enter:") var code string if _, err := fmt.Scan(&code); err != nil { return nil, err } return conf.Exchange(ctx, code) } ``` 4. 建立 filestorage.go: ``` package oauthstore import ( "encoding/json" "errors" "os" "sync" "golang.org/x/oauth2" ) // FileStorage 满足storage 接口 type FileStorage struct { Path string mu sync.RWMutex } // GetToken 从文件中检索令牌 func (f *FileStorage) GetToken() (*oauth2.Token, error) { f.mu.RLock() defer f.mu.RUnlock() in, err := os.Open(f.Path) if err != nil { return nil, err } defer in.Close() var t *oauth2.Token data := json.NewDecoder(in) return t, data.Decode(&t) } // SetToken 将令牌存储在文件中 func (f *FileStorage) SetToken(t *oauth2.Token) error { if t == nil || !t.Valid() { return errors.New("bad token") } f.mu.Lock() defer f.mu.Unlock() out, err := os.OpenFile(f.Path, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0755) if err != nil { return err } defer out.Close() data, err := json.Marshal(&t) if err != nil { return err } _, err = out.Write(data) return err } ``` 5. 建立 main.go: ``` package main import ( "context" "io" "os" "github.com/agtorre/go-cookbook/chapter6/oauthstore" "golang.org/x/oauth2" "golang.org/x/oauth2/github" ) func main() { conf := oauthstore.Config{ Config: &oauth2.Config{ ClientID: os.Getenv("GITHUB_CLIENT"), ClientSecret: os.Getenv("GITHUB_SECRET"), Scopes: []string{"repo", "user"}, Endpoint: github.Endpoint, }, Storage: &oauthstore.FileStorage{Path: "token.txt"}, } ctx := context.Background() token, err := oauthstore.GetToken(ctx, conf) if err != nil { panic(err) } cli := conf.Client(ctx, token) resp, err := cli.Get("https://api.github.com/user") if err != nil { panic(err) } defer resp.Body.Close() io.Copy(os.Stdout, resp.Body) } ``` 6. 这会输出: ``` $ go run main.go Visit the URL for the auth dialog: https://github.com/login/oauth/authorize? access_type=offline&client_id= <your_id>&response_type=code&scope=repo+user&state=state Paste the code returned in the redirect URL and hit Enter: <your_code> {<json_payload>} $ go run main.go {<json_payload>} ``` ### 说明 本节将令牌的内容存储到文件中并从文件中检索令牌的内容。 如果是第一次运行,它必须执行整个代码交换,但后续运行将重用访问令牌,如果有可用,它将使用刷新令牌刷新。 此代码中目前无法区分用户/令牌,但这可以通过使用cookie作为文件名或数据库中的行的键来实现。让我们来看看这段代码的作用: * config.go文件封装了标准的OAuth2配置。对于涉及检索令牌的每种方法,首先检查本地存储中是否有有效令牌。 如果没有,使用标准配置检索一个,然后存储它。 * tokensource.go文件实现了与Config配对的自定义TokenSource接口。 与Config类似,总是首先尝试从文件中检索令牌,否则使用新令牌设置它。 * storage.go文件是Config和TokenSource使用的存储接口。 它只定义了两个方法和辅助函数来引导OAuth2基于代码的流程,类似于我们在上一个方法中所做的那样,但是如果已经存在具有有效令牌的文件,则将使用它。 * filestorage.go文件实现存储接口。 当我们存储新令牌时,首先截断该文件并编写令牌结构的JSON表示。 否则,我们解析文件并返回令牌。 * * * * 学识浅薄,错误在所难免。欢迎在群中就本书提出修改意见,以飨后来者,长风拜谢。 Golang中国(211938256) beego实战(258969317) Go实践(386056972)