feat: support in-memory authentication
This commit is contained in:
@@ -85,6 +85,13 @@ func (data AuthData) complete() bool {
|
||||
return data.UA != "" && data.Ssecurity != "" && data.UserID != "" && data.CUserID != "" && data.ServiceToken != ""
|
||||
}
|
||||
|
||||
func (data AuthData) zero() bool {
|
||||
return data.UA == "" && data.DeviceID == "" && data.PassO == "" && data.Psecurity == "" &&
|
||||
data.Nonce == "" && data.Ssecurity == "" && data.PassToken == "" && data.UserID == "" &&
|
||||
data.CUserID == "" && data.ServiceToken == "" && data.YetAnotherServiceToken == "" &&
|
||||
data.ExpireTime == 0 && data.SaveTime == 0 && len(data.Extra) == 0
|
||||
}
|
||||
|
||||
func (data AuthData) yetAnotherServiceToken() string {
|
||||
if data.YetAnotherServiceToken != "" {
|
||||
return data.YetAnotherServiceToken
|
||||
@@ -177,9 +184,37 @@ func randomString(length int, alphabet string) string {
|
||||
}
|
||||
|
||||
func (client *Client) saveAuthData() error {
|
||||
authData := client.updateAuthData(func(authData *AuthData) {
|
||||
authData.SaveTime = time.Now().UnixMilli()
|
||||
})
|
||||
authData := client.AuthData()
|
||||
authData.SaveTime = time.Now().UnixMilli()
|
||||
if err := client.writeAuthData(authData); err != nil {
|
||||
return err
|
||||
}
|
||||
client.setAuthData(authData)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (client *Client) commitAuthData(authData AuthData) error {
|
||||
if !authData.complete() {
|
||||
return fmt.Errorf("incomplete auth data")
|
||||
}
|
||||
authData.SaveTime = time.Now().UnixMilli()
|
||||
if client.authPath != "" {
|
||||
if err := client.writeAuthData(authData); err != nil {
|
||||
return err
|
||||
}
|
||||
} else if client.authDataChanged != nil {
|
||||
if err := client.authDataChanged(authData.clone()); err != nil {
|
||||
return fmt.Errorf("persist changed auth data: %w", err)
|
||||
}
|
||||
}
|
||||
client.setAuthData(authData)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (client *Client) writeAuthData(authData AuthData) error {
|
||||
if client.authPath == "" {
|
||||
return nil
|
||||
}
|
||||
payload, err := json.MarshalIndent(authData, "", " ")
|
||||
if err != nil {
|
||||
return fmt.Errorf("encode auth data: %w", err)
|
||||
@@ -283,13 +318,14 @@ func (value *stringOrNumber) UnmarshalJSON(payload []byte) error {
|
||||
func (client *Client) Login(ctx context.Context) (AuthData, error) {
|
||||
client.loginMu.Lock()
|
||||
defer client.loginMu.Unlock()
|
||||
client.ensureIdentity()
|
||||
|
||||
location, refreshed, err := client.getLocation(ctx)
|
||||
location, refreshedAuthData, err := client.getLocation(ctx)
|
||||
if err != nil {
|
||||
return AuthData{}, err
|
||||
}
|
||||
if refreshed {
|
||||
if err := client.saveAuthData(); err != nil {
|
||||
if refreshedAuthData != nil {
|
||||
if err := client.commitAuthData(*refreshedAuthData); err != nil {
|
||||
return AuthData{}, err
|
||||
}
|
||||
return client.AuthData(), nil
|
||||
@@ -319,11 +355,11 @@ func (client *Client) Login(ctx context.Context) (AuthData, error) {
|
||||
return client.completeQRLogin(ctx, loginData)
|
||||
}
|
||||
|
||||
func (client *Client) getLocation(ctx context.Context) (url.Values, bool, error) {
|
||||
func (client *Client) getLocation(ctx context.Context) (url.Values, *AuthData, error) {
|
||||
httpClient := client.newSession()
|
||||
serviceURL, err := url.Parse(client.serviceLoginURL)
|
||||
if err != nil {
|
||||
return nil, false, fmt.Errorf("parse service login URL: %w", err)
|
||||
return nil, nil, fmt.Errorf("parse service login URL: %w", err)
|
||||
}
|
||||
query := serviceURL.Query()
|
||||
query.Set("_json", "true")
|
||||
@@ -332,52 +368,51 @@ func (client *Client) getLocation(ctx context.Context) (url.Values, bool, error)
|
||||
serviceURL.RawQuery = query.Encode()
|
||||
request, err := http.NewRequestWithContext(ctx, http.MethodGet, serviceURL.String(), nil)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
return nil, nil, err
|
||||
}
|
||||
client.setLoginHeaders(request, true)
|
||||
var data serviceLoginData
|
||||
if err := client.doLoginRequestWithClient(httpClient, request, false, &data); err != nil {
|
||||
return nil, false, err
|
||||
return nil, nil, err
|
||||
}
|
||||
if data.Location == "" {
|
||||
return nil, false, &LoginError{Code: data.Code, Message: "登录响应缺少 location"}
|
||||
return nil, nil, &LoginError{Code: data.Code, Message: "登录响应缺少 location"}
|
||||
}
|
||||
if data.Code == 0 {
|
||||
refreshRequest, err := http.NewRequestWithContext(ctx, http.MethodGet, data.Location, nil)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
return nil, nil, err
|
||||
}
|
||||
client.setLoginHeaders(refreshRequest, false)
|
||||
response, err := httpClient.Do(refreshRequest)
|
||||
if err != nil {
|
||||
return nil, false, fmt.Errorf("refresh login token: %w", err)
|
||||
return nil, nil, fmt.Errorf("refresh login token: %w", err)
|
||||
}
|
||||
body, readErr := readHTTPResponse(response)
|
||||
response.Body.Close()
|
||||
if readErr != nil {
|
||||
return nil, false, fmt.Errorf("read token refresh response: %w", readErr)
|
||||
return nil, nil, fmt.Errorf("read token refresh response: %w", readErr)
|
||||
}
|
||||
if response.StatusCode != http.StatusOK {
|
||||
return nil, false, &LoginError{Code: response.StatusCode, Message: string(body)}
|
||||
return nil, nil, &LoginError{Code: response.StatusCode, Message: string(body)}
|
||||
}
|
||||
if string(body) != "ok" {
|
||||
return nil, false, &LoginError{Code: -1, Message: string(body)}
|
||||
return nil, nil, &LoginError{Code: -1, Message: string(body)}
|
||||
}
|
||||
candidate := client.AuthData()
|
||||
serviceTokenReceived := updateAuthDataFromCookies(&candidate, httpClient, response.Request.URL)
|
||||
candidate.Ssecurity = data.Ssecurity
|
||||
if !serviceTokenReceived || !candidate.complete() {
|
||||
return nil, false, fmt.Errorf("%w: %w", ErrReauthenticationRequired, &LoginError{Code: -1, Message: "刷新Token响应认证信息不完整"})
|
||||
return nil, nil, fmt.Errorf("%w: %w", ErrReauthenticationRequired, &LoginError{Code: -1, Message: "刷新Token响应认证信息不完整"})
|
||||
}
|
||||
candidate.ExpireTime = time.Now().Add(30 * 24 * time.Hour).UnixMilli()
|
||||
client.setAuthData(candidate)
|
||||
return nil, true, nil
|
||||
return nil, &candidate, nil
|
||||
}
|
||||
locationURL, err := url.Parse(data.Location)
|
||||
if err != nil {
|
||||
return nil, false, fmt.Errorf("parse login location: %w", err)
|
||||
return nil, nil, fmt.Errorf("parse login location: %w", err)
|
||||
}
|
||||
return locationURL.Query(), false, nil
|
||||
return locationURL.Query(), nil, nil
|
||||
}
|
||||
|
||||
func (client *Client) getQRLoginData(ctx context.Context, location url.Values) (qrLoginData, error) {
|
||||
@@ -455,8 +490,7 @@ func (client *Client) completeQRLogin(ctx context.Context, loginData qrLoginData
|
||||
return AuthData{}, &LoginError{Code: -1, Message: "登录回调认证信息不完整"}
|
||||
}
|
||||
candidate.ExpireTime = time.Now().Add(30 * 24 * time.Hour).UnixMilli()
|
||||
client.setAuthData(candidate)
|
||||
if err := client.saveAuthData(); err != nil {
|
||||
if err := client.commitAuthData(candidate); err != nil {
|
||||
return AuthData{}, err
|
||||
}
|
||||
return client.AuthData(), nil
|
||||
@@ -549,14 +583,14 @@ func (client *Client) refreshToken(ctx context.Context) error {
|
||||
if available {
|
||||
return nil
|
||||
}
|
||||
_, refreshed, err := client.getLocation(ctx)
|
||||
_, refreshedAuthData, err := client.getLocation(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !refreshed {
|
||||
if refreshedAuthData == nil {
|
||||
return fmt.Errorf("%w: %w", ErrReauthenticationRequired, &LoginError{Code: -1, Message: "刷新Token失败,请重新登录"})
|
||||
}
|
||||
if err := client.saveAuthData(); err != nil {
|
||||
if err := client.commitAuthData(*refreshedAuthData); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
|
||||
Reference in New Issue
Block a user