Skip to content

Commit 5fe2670

Browse files
authored
fix(123_open): infinite recursive call (#1854)
fix(123_open): token refresh logic Fix token handling logic to avoid deadlock. Token method took reference of Alist's implementation.
1 parent 2442e30 commit 5fe2670

File tree

5 files changed

+148
-46
lines changed

5 files changed

+148
-46
lines changed

drivers/123_open/driver.go

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ type Open123 struct {
1818
model.Storage
1919
Addition
2020
UID uint64
21+
tm *tokenManager
2122
}
2223

2324
func (d *Open123) Config() driver.Config {
@@ -33,6 +34,24 @@ func (d *Open123) Init(ctx context.Context) error {
3334
d.UploadThread = 3
3435
}
3536

37+
if d.RefreshToken != "" {
38+
// refresh token 直接主动刷新
39+
d.AccessToken = ""
40+
d.tm = &tokenManager{}
41+
} else {
42+
// 避免个人 token 刷新产生的多个登录,被动刷新
43+
// 默认过期时间90天,jwt exp 不可靠
44+
d.tm = &tokenManager{
45+
// accessToken: d.AccessToken,
46+
expiredAt: time.Now().Add(90 * 24 * time.Hour),
47+
}
48+
}
49+
50+
_, err := d.getAccessToken(false)
51+
if err != nil {
52+
return fmt.Errorf("init get access token error: %w", err)
53+
}
54+
3655
return nil
3756
}
3857

drivers/123_open/meta.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ type Addition struct {
1313
ClientID string `json:"ClientID" required:"false"`
1414
ClientSecret string `json:"ClientSecret" required:"false"`
1515

16-
// 直接写入AccessToken
16+
// 直接写入AccessToken, AccessToken有过期时间,不建议直接填写
1717
AccessToken string `json:"AccessToken" required:"false"`
1818

1919
// 用户名+密码方式登录的AccessToken可以兼容

drivers/123_open/token.go

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
package _123_open
2+
3+
import (
4+
"encoding/json"
5+
"errors"
6+
"fmt"
7+
"net/http"
8+
"sync"
9+
"time"
10+
11+
"github.com/OpenListTeam/OpenList/v4/drivers/base"
12+
"github.com/OpenListTeam/OpenList/v4/internal/op"
13+
)
14+
15+
var (
16+
AccessToken = "https://open-api.123pan.com/api/v1/access_token"
17+
RefreshToken = "https://open-api.123pan.com/api/v1/oauth2/access_token"
18+
)
19+
20+
type tokenManager struct {
21+
// accessToken string
22+
expiredAt time.Time
23+
mu sync.Mutex
24+
blockRefresh bool
25+
}
26+
27+
func (d *Open123) getAccessToken(forceRefresh bool) (string, error) {
28+
tm := d.tm
29+
tm.mu.Lock()
30+
defer tm.mu.Unlock()
31+
if tm.blockRefresh {
32+
return "", errors.New("Authentication expired")
33+
}
34+
if !forceRefresh && d.AccessToken != "" && time.Now().Before(tm.expiredAt.Add(-5*time.Minute)) {
35+
return d.AccessToken, nil
36+
}
37+
if err := d.flushAccessToken(); err != nil {
38+
// token expired and failed to refresh, block further refresh attempts
39+
tm.blockRefresh = true
40+
return "", err
41+
}
42+
return d.AccessToken, nil
43+
}
44+
45+
func (d *Open123) flushAccessToken() error {
46+
// directly send request to avoid deadlock
47+
req := base.RestyClient.R()
48+
req.SetHeaders(map[string]string{
49+
"authorization": "Bearer " + d.AccessToken,
50+
"platform": "open_platform",
51+
"Content-Type": "application/json",
52+
})
53+
54+
if d.ClientID != "" {
55+
if d.RefreshToken != "" {
56+
var resp RefreshTokenResp
57+
req.SetQueryParam("client_id", d.ClientID)
58+
if d.ClientSecret != "" {
59+
req.SetQueryParam("client_secret", d.ClientSecret)
60+
}
61+
req.SetQueryParam("grant_type", "refresh_token")
62+
req.SetQueryParam("refresh_token", d.RefreshToken)
63+
req.SetResult(&resp)
64+
res, err := req.Execute(http.MethodPost, RefreshToken)
65+
if err != nil {
66+
return err
67+
}
68+
body := res.Body()
69+
var baseResp BaseResp
70+
if err = json.Unmarshal(body, &baseResp); err != nil {
71+
return err
72+
}
73+
if baseResp.Code != 0 {
74+
return fmt.Errorf("get access token failed: %s", baseResp.Message)
75+
}
76+
77+
d.AccessToken = resp.AccessToken
78+
// add token expire time
79+
d.tm.expiredAt = time.Now().Add(time.Duration(resp.ExpiresIn) * time.Second)
80+
d.RefreshToken = resp.RefreshToken
81+
op.MustSaveDriverStorage(d)
82+
d.tm.blockRefresh = false
83+
return nil
84+
} else if d.ClientSecret != "" {
85+
var resp AccessTokenResp
86+
req.SetBody(base.Json{
87+
"clientID": d.ClientID,
88+
"clientSecret": d.ClientSecret,
89+
})
90+
req.SetResult(&resp)
91+
res, err := req.Execute(http.MethodPost, AccessToken)
92+
if err != nil {
93+
return err
94+
}
95+
body := res.Body()
96+
var baseResp BaseResp
97+
if err = json.Unmarshal(body, &baseResp); err != nil {
98+
return err
99+
}
100+
if baseResp.Code != 0 {
101+
return fmt.Errorf("get access token failed: %s", baseResp.Message)
102+
}
103+
d.AccessToken = resp.Data.AccessToken
104+
// parse token expire time
105+
d.tm.expiredAt, err = time.Parse(time.RFC3339, resp.Data.ExpiredAt)
106+
if err != nil {
107+
return fmt.Errorf("parse expire time failed: %w", err)
108+
}
109+
op.MustSaveDriverStorage(d)
110+
d.tm.blockRefresh = false
111+
return nil
112+
}
113+
}
114+
return errors.New("no valid authentication method available")
115+
}

drivers/123_open/upload.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -116,14 +116,18 @@ func (d *Open123) Upload(ctx context.Context, file model.FileStreamer, createRes
116116
head := bytes.NewReader(b.Bytes()[:headSize])
117117
tail := bytes.NewReader(b.Bytes()[headSize:])
118118
rateLimitedRd = driver.NewLimitedUploadStream(ctx, io.MultiReader(head, reader, tail))
119+
token, err := d.getAccessToken(false)
120+
if err != nil {
121+
return err
122+
}
119123
// 创建请求并设置header
120124
req, err := http.NewRequestWithContext(ctx, http.MethodPost, uploadDomain+"/upload/v2/file/slice", rateLimitedRd)
121125
if err != nil {
122126
return err
123127
}
124128

125129
// 设置请求头
126-
req.Header.Add("Authorization", "Bearer "+d.AccessToken)
130+
req.Header.Add("Authorization", "Bearer "+token)
127131
req.Header.Add("Content-Type", w.FormDataContentType())
128132
req.Header.Add("Platform", "open_platform")
129133

drivers/123_open/util.go

Lines changed: 8 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ import (
1313
"time"
1414

1515
"github.com/OpenListTeam/OpenList/v4/drivers/base"
16-
"github.com/OpenListTeam/OpenList/v4/internal/op"
1716
"github.com/go-resty/resty/v2"
1817
"github.com/google/uuid"
1918
log "github.com/sirupsen/logrus"
@@ -22,8 +21,6 @@ import (
2221
var ( // 不同情况下获取的AccessTokenQPS限制不同 如下模块化易于拓展
2322
Api = "https://open-api.123pan.com"
2423

25-
AccessToken = InitApiInfo(Api+"/api/v1/access_token", 1)
26-
RefreshToken = InitApiInfo(Api+"/api/v1/oauth2/access_token", 1)
2724
UserInfo = InitApiInfo(Api+"/api/v1/user/info", 1)
2825
FileList = InitApiInfo(Api+"/api/v2/file/list", 3)
2926
DownloadInfo = InitApiInfo(Api+"/api/v1/file/download_info", 5)
@@ -40,11 +37,14 @@ var ( // 不同情况下获取的AccessTokenQPS限制不同 如下模块化易
4037
)
4138

4239
func (d *Open123) Request(apiInfo *ApiInfo, method string, callback base.ReqCallback, resp interface{}) ([]byte, error) {
43-
retryToken := true
4440
for {
41+
token, err := d.getAccessToken(false)
42+
if err != nil {
43+
return nil, err
44+
}
4545
req := base.RestyClient.R()
4646
req.SetHeaders(map[string]string{
47-
"authorization": "Bearer " + d.AccessToken,
47+
"authorization": "Bearer " + token,
4848
"platform": "open_platform",
4949
"Content-Type": "application/json",
5050
})
@@ -74,9 +74,9 @@ func (d *Open123) Request(apiInfo *ApiInfo, method string, callback base.ReqCall
7474

7575
if baseResp.Code == 0 {
7676
return body, nil
77-
} else if baseResp.Code == 401 && retryToken {
78-
retryToken = false
79-
if err := d.flushAccessToken(); err != nil {
77+
} else if baseResp.Code == 401 {
78+
// 强制刷新Token, 有小概率会 race condition 导致多次刷新Token,但不影响正确运行
79+
if _, err := d.getAccessToken(true); err != nil {
8080
return nil, err
8181
}
8282
} else if baseResp.Code == 429 {
@@ -88,42 +88,6 @@ func (d *Open123) Request(apiInfo *ApiInfo, method string, callback base.ReqCall
8888
}
8989
}
9090

91-
func (d *Open123) flushAccessToken() error {
92-
if d.ClientID != "" {
93-
if d.RefreshToken != "" {
94-
var resp RefreshTokenResp
95-
_, err := d.Request(RefreshToken, http.MethodPost, func(req *resty.Request) {
96-
req.SetQueryParam("client_id", d.ClientID)
97-
if d.ClientSecret != "" {
98-
req.SetQueryParam("client_secret", d.ClientSecret)
99-
}
100-
req.SetQueryParam("grant_type", "refresh_token")
101-
req.SetQueryParam("refresh_token", d.RefreshToken)
102-
}, &resp)
103-
if err != nil {
104-
return err
105-
}
106-
d.AccessToken = resp.AccessToken
107-
d.RefreshToken = resp.RefreshToken
108-
op.MustSaveDriverStorage(d)
109-
} else if d.ClientSecret != "" {
110-
var resp AccessTokenResp
111-
_, err := d.Request(AccessToken, http.MethodPost, func(req *resty.Request) {
112-
req.SetBody(base.Json{
113-
"clientID": d.ClientID,
114-
"clientSecret": d.ClientSecret,
115-
})
116-
}, &resp)
117-
if err != nil {
118-
return err
119-
}
120-
d.AccessToken = resp.Data.AccessToken
121-
op.MustSaveDriverStorage(d)
122-
}
123-
}
124-
return nil
125-
}
126-
12791
func (d *Open123) SignURL(originURL, privateKey string, uid uint64, validDuration time.Duration) (newURL string, err error) {
12892
// 生成Unix时间戳
12993
ts := time.Now().Add(validDuration).Unix()

0 commit comments

Comments
 (0)