mirror of
https://github.com/OpenListTeam/OpenList.git
synced 2025-09-19 12:16:24 +08:00
feat(cloudreve_v4): enhance token management (#1171)
* fix(cloudreve_v4): improve error handling in request method Signed-off-by: MadDogOwner <xiaoran@xrgzs.top> * feat(cloudreve_v4): enhance token management with expiration checks and refresh logic Signed-off-by: MadDogOwner <xiaoran@xrgzs.top> * feat(cloudreve_v4): add JWT structures for access and refresh tokens; validate access token on initialization Signed-off-by: MadDogOwner <xiaoran@xrgzs.top> * fix(cloudreve_v4): improve error messages Signed-off-by: MadDogOwner <xiaoran@xrgzs.top> --------- Signed-off-by: MadDogOwner <xiaoran@xrgzs.top>
This commit is contained in:
@ -21,6 +21,8 @@ type CloudreveV4 struct {
|
|||||||
model.Storage
|
model.Storage
|
||||||
Addition
|
Addition
|
||||||
ref *CloudreveV4
|
ref *CloudreveV4
|
||||||
|
AccessExpires string
|
||||||
|
RefreshExpires string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *CloudreveV4) Config() driver.Config {
|
func (d *CloudreveV4) Config() driver.Config {
|
||||||
@ -44,13 +46,17 @@ func (d *CloudreveV4) Init(ctx context.Context) error {
|
|||||||
if d.ref != nil {
|
if d.ref != nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
if d.AccessToken == "" && d.RefreshToken != "" {
|
if d.canLogin() {
|
||||||
return d.refreshToken()
|
|
||||||
}
|
|
||||||
if d.Username != "" {
|
|
||||||
return d.login()
|
return d.login()
|
||||||
}
|
}
|
||||||
return nil
|
if d.RefreshToken != "" {
|
||||||
|
return d.refreshToken()
|
||||||
|
}
|
||||||
|
if d.AccessToken == "" {
|
||||||
|
return errors.New("no way to authenticate. At least AccessToken is required")
|
||||||
|
}
|
||||||
|
// ensure AccessToken is valid
|
||||||
|
return d.parseJWT(d.AccessToken, &AccessJWT{})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *CloudreveV4) InitReference(storage driver.Driver) error {
|
func (d *CloudreveV4) InitReference(storage driver.Driver) error {
|
||||||
|
@ -66,11 +66,27 @@ type CaptchaResp struct {
|
|||||||
Ticket string `json:"ticket"`
|
Ticket string `json:"ticket"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type AccessJWT struct {
|
||||||
|
TokenType string `json:"token_type"`
|
||||||
|
Sub string `json:"sub"`
|
||||||
|
Exp int64 `json:"exp"`
|
||||||
|
Nbf int64 `json:"nbf"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type RefreshJWT struct {
|
||||||
|
TokenType string `json:"token_type"`
|
||||||
|
Sub string `json:"sub"`
|
||||||
|
Exp int `json:"exp"`
|
||||||
|
Nbf int `json:"nbf"`
|
||||||
|
StateHash string `json:"state_hash"`
|
||||||
|
RootTokenID string `json:"root_token_id"`
|
||||||
|
}
|
||||||
|
|
||||||
type Token struct {
|
type Token struct {
|
||||||
AccessToken string `json:"access_token"`
|
AccessToken string `json:"access_token"`
|
||||||
RefreshToken string `json:"refresh_token"`
|
RefreshToken string `json:"refresh_token"`
|
||||||
AccessExpires time.Time `json:"access_expires"`
|
AccessExpires string `json:"access_expires"`
|
||||||
RefreshExpires time.Time `json:"refresh_expires"`
|
RefreshExpires string `json:"refresh_expires"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type TokenResponse struct {
|
type TokenResponse struct {
|
||||||
|
@ -28,6 +28,15 @@ import (
|
|||||||
|
|
||||||
// do others that not defined in Driver interface
|
// do others that not defined in Driver interface
|
||||||
|
|
||||||
|
const (
|
||||||
|
CodeLoginRequired = http.StatusUnauthorized
|
||||||
|
CodeCredentialInvalid = 40020 // Failed to issue token
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrorIssueToken = errors.New("failed to issue token")
|
||||||
|
)
|
||||||
|
|
||||||
func (d *CloudreveV4) getUA() string {
|
func (d *CloudreveV4) getUA() string {
|
||||||
if d.CustomUA != "" {
|
if d.CustomUA != "" {
|
||||||
return d.CustomUA
|
return d.CustomUA
|
||||||
@ -39,6 +48,23 @@ func (d *CloudreveV4) request(method string, path string, callback base.ReqCallb
|
|||||||
if d.ref != nil {
|
if d.ref != nil {
|
||||||
return d.ref.request(method, path, callback, out)
|
return d.ref.request(method, path, callback, out)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ensure token
|
||||||
|
if d.isTokenExpired() {
|
||||||
|
err := d.refreshToken()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return d._request(method, path, callback, out)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *CloudreveV4) _request(method string, path string, callback base.ReqCallback, out any) error {
|
||||||
|
if d.ref != nil {
|
||||||
|
return d.ref._request(method, path, callback, out)
|
||||||
|
}
|
||||||
|
|
||||||
u := d.Address + "/api/v4" + path
|
u := d.Address + "/api/v4" + path
|
||||||
req := base.RestyClient.R()
|
req := base.RestyClient.R()
|
||||||
req.SetHeaders(map[string]string{
|
req.SetHeaders(map[string]string{
|
||||||
@ -65,15 +91,17 @@ func (d *CloudreveV4) request(method string, path string, callback base.ReqCallb
|
|||||||
}
|
}
|
||||||
|
|
||||||
if r.Code != 0 {
|
if r.Code != 0 {
|
||||||
if r.Code == 401 && d.RefreshToken != "" && path != "/session/token/refresh" {
|
if r.Code == CodeLoginRequired && d.canLogin() && path != "/session/token/refresh" {
|
||||||
// try to refresh token
|
err = d.login()
|
||||||
err = d.refreshToken()
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return d.request(method, path, callback, out)
|
return d.request(method, path, callback, out)
|
||||||
}
|
}
|
||||||
return errors.New(r.Msg)
|
if r.Code == CodeCredentialInvalid {
|
||||||
|
return ErrorIssueToken
|
||||||
|
}
|
||||||
|
return fmt.Errorf("%d: %s", r.Code, r.Msg)
|
||||||
}
|
}
|
||||||
|
|
||||||
if out != nil && r.Data != nil {
|
if out != nil && r.Data != nil {
|
||||||
@ -91,14 +119,18 @@ func (d *CloudreveV4) request(method string, path string, callback base.ReqCallb
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (d *CloudreveV4) canLogin() bool {
|
||||||
|
return d.Username != "" && d.Password != ""
|
||||||
|
}
|
||||||
|
|
||||||
func (d *CloudreveV4) login() error {
|
func (d *CloudreveV4) login() error {
|
||||||
var siteConfig SiteLoginConfigResp
|
var siteConfig SiteLoginConfigResp
|
||||||
err := d.request(http.MethodGet, "/site/config/login", nil, &siteConfig)
|
err := d._request(http.MethodGet, "/site/config/login", nil, &siteConfig)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
var prepareLogin PrepareLoginResp
|
var prepareLogin PrepareLoginResp
|
||||||
err = d.request(http.MethodGet, "/session/prepare?email="+d.Addition.Username, nil, &prepareLogin)
|
err = d._request(http.MethodGet, "/session/prepare?email="+d.Addition.Username, nil, &prepareLogin)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -128,7 +160,7 @@ func (d *CloudreveV4) doLogin(needCaptcha bool) error {
|
|||||||
}
|
}
|
||||||
if needCaptcha {
|
if needCaptcha {
|
||||||
var config BasicConfigResp
|
var config BasicConfigResp
|
||||||
err = d.request(http.MethodGet, "/site/config/basic", nil, &config)
|
err = d._request(http.MethodGet, "/site/config/basic", nil, &config)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -136,7 +168,7 @@ func (d *CloudreveV4) doLogin(needCaptcha bool) error {
|
|||||||
return fmt.Errorf("captcha type %s not support", config.CaptchaType)
|
return fmt.Errorf("captcha type %s not support", config.CaptchaType)
|
||||||
}
|
}
|
||||||
var captcha CaptchaResp
|
var captcha CaptchaResp
|
||||||
err = d.request(http.MethodGet, "/site/captcha", nil, &captcha)
|
err = d._request(http.MethodGet, "/site/captcha", nil, &captcha)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -162,20 +194,22 @@ func (d *CloudreveV4) doLogin(needCaptcha bool) error {
|
|||||||
loginBody["captcha"] = captchaCode
|
loginBody["captcha"] = captchaCode
|
||||||
}
|
}
|
||||||
var token TokenResponse
|
var token TokenResponse
|
||||||
err = d.request(http.MethodPost, "/session/token", func(req *resty.Request) {
|
err = d._request(http.MethodPost, "/session/token", func(req *resty.Request) {
|
||||||
req.SetBody(loginBody)
|
req.SetBody(loginBody)
|
||||||
}, &token)
|
}, &token)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
d.AccessToken, d.RefreshToken = token.Token.AccessToken, token.Token.RefreshToken
|
d.AccessToken, d.RefreshToken = token.Token.AccessToken, token.Token.RefreshToken
|
||||||
|
d.AccessExpires, d.RefreshExpires = token.Token.AccessExpires, token.Token.RefreshExpires
|
||||||
op.MustSaveDriverStorage(d)
|
op.MustSaveDriverStorage(d)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *CloudreveV4) refreshToken() error {
|
func (d *CloudreveV4) refreshToken() error {
|
||||||
|
// if no refresh token, try to login if possible
|
||||||
if d.RefreshToken == "" {
|
if d.RefreshToken == "" {
|
||||||
if d.Username != "" {
|
if d.canLogin() {
|
||||||
err := d.login()
|
err := d.login()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("cannot login to get refresh token, error: %s", err)
|
return fmt.Errorf("cannot login to get refresh token, error: %s", err)
|
||||||
@ -183,20 +217,127 @@ func (d *CloudreveV4) refreshToken() error {
|
|||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// parse jwt to check if refresh token is valid
|
||||||
|
var jwt RefreshJWT
|
||||||
|
err := d.parseJWT(d.RefreshToken, &jwt)
|
||||||
|
if err != nil {
|
||||||
|
// if refresh token is invalid, try to login if possible
|
||||||
|
if d.canLogin() {
|
||||||
|
return d.login()
|
||||||
|
}
|
||||||
|
d.GetStorage().SetStatus(fmt.Sprintf("Invalid RefreshToken: %s", err.Error()))
|
||||||
|
op.MustSaveDriverStorage(d)
|
||||||
|
return fmt.Errorf("invalid refresh token: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// do refresh token
|
||||||
var token Token
|
var token Token
|
||||||
err := d.request(http.MethodPost, "/session/token/refresh", func(req *resty.Request) {
|
err = d._request(http.MethodPost, "/session/token/refresh", func(req *resty.Request) {
|
||||||
req.SetBody(base.Json{
|
req.SetBody(base.Json{
|
||||||
"refresh_token": d.RefreshToken,
|
"refresh_token": d.RefreshToken,
|
||||||
})
|
})
|
||||||
}, &token)
|
}, &token)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
if errors.Is(err, ErrorIssueToken) {
|
||||||
|
if d.canLogin() {
|
||||||
|
// try to login again
|
||||||
|
return d.login()
|
||||||
|
}
|
||||||
|
d.GetStorage().SetStatus("This session is no longer valid")
|
||||||
|
op.MustSaveDriverStorage(d)
|
||||||
|
return ErrorIssueToken
|
||||||
|
}
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
d.AccessToken, d.RefreshToken = token.AccessToken, token.RefreshToken
|
d.AccessToken, d.RefreshToken = token.AccessToken, token.RefreshToken
|
||||||
|
d.AccessExpires, d.RefreshExpires = token.AccessExpires, token.RefreshExpires
|
||||||
op.MustSaveDriverStorage(d)
|
op.MustSaveDriverStorage(d)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (d *CloudreveV4) parseJWT(token string, jwt any) error {
|
||||||
|
split := strings.Split(token, ".")
|
||||||
|
if len(split) != 3 {
|
||||||
|
return fmt.Errorf("invalid token length: %d, ensure the token is a valid JWT", len(split))
|
||||||
|
}
|
||||||
|
data, err := base64.RawURLEncoding.DecodeString(split[1])
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("invalid token encoding: %w, ensure the token is a valid JWT", err)
|
||||||
|
}
|
||||||
|
err = json.Unmarshal(data, &jwt)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("invalid token content: %w, ensure the token is a valid JWT", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// check if token is expired
|
||||||
|
// https://github.com/cloudreve/frontend/blob/ddfacc1c31c49be03beb71de4cc114c8811038d6/src/session/index.ts#L177-L200
|
||||||
|
func (d *CloudreveV4) isTokenExpired() bool {
|
||||||
|
if d.RefreshToken == "" {
|
||||||
|
// login again if username and password is set
|
||||||
|
if d.canLogin() {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
// no refresh token, cannot refresh
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if d.AccessToken == "" {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
var (
|
||||||
|
err error
|
||||||
|
expires time.Time
|
||||||
|
)
|
||||||
|
// check if token is expired
|
||||||
|
if d.AccessExpires != "" {
|
||||||
|
// use expires field if possible to prevent timezone issue
|
||||||
|
// only available after login or refresh token
|
||||||
|
// 2025-08-28T02:43:07.645109985+08:00
|
||||||
|
expires, err = time.Parse(time.RFC3339Nano, d.AccessExpires)
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// fallback to parse jwt
|
||||||
|
// if failed, disable the storage
|
||||||
|
var jwt AccessJWT
|
||||||
|
err = d.parseJWT(d.AccessToken, &jwt)
|
||||||
|
if err != nil {
|
||||||
|
d.GetStorage().SetStatus(fmt.Sprintf("Invalid AccessToken: %s", err.Error()))
|
||||||
|
op.MustSaveDriverStorage(d)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
// may be have timezone issue
|
||||||
|
expires = time.Unix(jwt.Exp, 0)
|
||||||
|
}
|
||||||
|
// add a 10 minutes safe margin
|
||||||
|
ddl := time.Now().Add(10 * time.Minute)
|
||||||
|
if expires.Before(ddl) {
|
||||||
|
// current access token expired, check if refresh token is expired
|
||||||
|
// warning: cannot parse refresh token from jwt, because the exp field is not standard
|
||||||
|
if d.RefreshExpires != "" {
|
||||||
|
refreshExpires, err := time.Parse(time.RFC3339Nano, d.RefreshExpires)
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if refreshExpires.Before(time.Now()) {
|
||||||
|
// This session is no longer valid
|
||||||
|
if d.canLogin() {
|
||||||
|
// try to login again
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
d.GetStorage().SetStatus("This session is no longer valid")
|
||||||
|
op.MustSaveDriverStorage(d)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
func (d *CloudreveV4) upLocal(ctx context.Context, file model.FileStreamer, u FileUploadResp, up driver.UpdateProgress) error {
|
func (d *CloudreveV4) upLocal(ctx context.Context, file model.FileStreamer, u FileUploadResp, up driver.UpdateProgress) error {
|
||||||
var finish int64 = 0
|
var finish int64 = 0
|
||||||
var chunk int = 0
|
var chunk int = 0
|
||||||
|
Reference in New Issue
Block a user