mirror of https://github.com/Xhofe/alist
fix: session invalid issue (#9301)
* feat(auth): Enhanced device login session management - Upon login, obtain and verify `Client-Id` to ensure unique device sessions. - If there are too many device sessions, clean up old ones according to the configured policy or return an error. - If a device session is invalid, deregister the old token and return a 401 error. - Added `EnsureActiveOnLogin` function to handle the creation and refresh of device sessions during login. * feat(session): Modified session deletion logic to mark sessions as inactive. - Changed session deletion logic to mark sessions as inactive using the `MarkInactive` method. - Adjusted error handling to ensure an error is returned if marking fails. * feat(session): Added device limits and eviction policies - Added a device limit, controlling the maximum number of devices using the `MaxDevices` configuration option. - If the number of devices exceeds the limit, the configured eviction policy is used. - If the policy is `evict_oldest`, the oldest device is evicted. - Otherwise, an error message indicating too many devices is returned. * refactor(session): Filter for the user's oldest active session - Renamed `GetOldestSession` to `GetOldestActiveSession` to more accurately reflect its functionality - Updated the SQL query to add the `status = SessionActive` condition to retrieve only active sessions - Replaced all callpoints and unified the new function name to ensure logical consistencymain beta
parent
63391a2091
commit
4b288a08ef
|
@ -38,10 +38,12 @@ func DeleteSessionsBefore(ts int64) error {
|
||||||
return errors.WithStack(db.Where("last_active < ?", ts).Delete(&model.Session{}).Error)
|
return errors.WithStack(db.Where("last_active < ?", ts).Delete(&model.Session{}).Error)
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetOldestSession(userID uint) (*model.Session, error) {
|
// GetOldestActiveSession returns the oldest active session for the specified user.
|
||||||
|
func GetOldestActiveSession(userID uint) (*model.Session, error) {
|
||||||
var s model.Session
|
var s model.Session
|
||||||
if err := db.Where("user_id = ?", userID).Order("last_active ASC").First(&s).Error; err != nil {
|
if err := db.Where("user_id = ? AND status = ?", userID, model.SessionActive).
|
||||||
return nil, errors.Wrap(err, "failed get oldest session")
|
Order("last_active ASC").First(&s).Error; err != nil {
|
||||||
|
return nil, errors.Wrap(err, "failed get oldest active session")
|
||||||
}
|
}
|
||||||
return &s, nil
|
return &s, nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,20 +23,68 @@ func Handle(userID uint, deviceKey, ua, ip string) error {
|
||||||
ip = utils.MaskIP(ip)
|
ip = utils.MaskIP(ip)
|
||||||
|
|
||||||
now := time.Now().Unix()
|
now := time.Now().Unix()
|
||||||
|
sess, err := db.GetSession(userID, deviceKey)
|
||||||
|
if err == nil {
|
||||||
|
if sess.Status == model.SessionInactive {
|
||||||
|
return errors.WithStack(errs.SessionInactive)
|
||||||
|
}
|
||||||
|
sess.Status = model.SessionActive
|
||||||
|
sess.LastActive = now
|
||||||
|
sess.UserAgent = ua
|
||||||
|
sess.IP = ip
|
||||||
|
return db.UpsertSession(sess)
|
||||||
|
}
|
||||||
|
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
max := setting.GetInt(conf.MaxDevices, 0)
|
||||||
|
if max > 0 {
|
||||||
|
count, err := db.CountActiveSessionsByUser(userID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if count >= int64(max) {
|
||||||
|
policy := setting.GetStr(conf.DeviceEvictPolicy, "deny")
|
||||||
|
if policy == "evict_oldest" {
|
||||||
|
if oldest, err := db.GetOldestActiveSession(userID); err == nil {
|
||||||
|
if err := db.MarkInactive(oldest.DeviceKey); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return errors.WithStack(errs.TooManyDevices)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
s := &model.Session{UserID: userID, DeviceKey: deviceKey, UserAgent: ua, IP: ip, LastActive: now, Status: model.SessionActive}
|
||||||
|
return db.CreateSession(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
// EnsureActiveOnLogin is used only in login flow:
|
||||||
|
// - If session exists (even Inactive): reactivate and refresh fields.
|
||||||
|
// - If not exists: apply max-devices policy, then create Active session.
|
||||||
|
func EnsureActiveOnLogin(userID uint, deviceKey, ua, ip string) error {
|
||||||
|
ip = utils.MaskIP(ip)
|
||||||
|
now := time.Now().Unix()
|
||||||
|
|
||||||
sess, err := db.GetSession(userID, deviceKey)
|
sess, err := db.GetSession(userID, deviceKey)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
if sess.Status == model.SessionInactive {
|
if sess.Status == model.SessionInactive {
|
||||||
max := setting.GetInt(conf.MaxDevices, 0)
|
max := setting.GetInt(conf.MaxDevices, 0)
|
||||||
if max > 0 {
|
if max > 0 {
|
||||||
count, cerr := db.CountActiveSessionsByUser(userID)
|
count, err := db.CountActiveSessionsByUser(userID)
|
||||||
if cerr != nil {
|
if err != nil {
|
||||||
return cerr
|
return err
|
||||||
}
|
}
|
||||||
if count >= int64(max) {
|
if count >= int64(max) {
|
||||||
policy := setting.GetStr(conf.DeviceEvictPolicy, "deny")
|
policy := setting.GetStr(conf.DeviceEvictPolicy, "deny")
|
||||||
if policy == "evict_oldest" {
|
if policy == "evict_oldest" {
|
||||||
if oldest, gerr := db.GetOldestSession(userID); gerr == nil {
|
if oldest, gerr := db.GetOldestActiveSession(userID); gerr == nil {
|
||||||
_ = db.DeleteSession(userID, oldest.DeviceKey)
|
if err := db.MarkInactive(oldest.DeviceKey); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
return errors.WithStack(errs.TooManyDevices)
|
return errors.WithStack(errs.TooManyDevices)
|
||||||
|
@ -63,9 +111,10 @@ func Handle(userID uint, deviceKey, ua, ip string) error {
|
||||||
if count >= int64(max) {
|
if count >= int64(max) {
|
||||||
policy := setting.GetStr(conf.DeviceEvictPolicy, "deny")
|
policy := setting.GetStr(conf.DeviceEvictPolicy, "deny")
|
||||||
if policy == "evict_oldest" {
|
if policy == "evict_oldest" {
|
||||||
oldest, err := db.GetOldestSession(userID)
|
if oldest, gerr := db.GetOldestActiveSession(userID); gerr == nil {
|
||||||
if err == nil {
|
if err := db.MarkInactive(oldest.DeviceKey); err != nil {
|
||||||
_ = db.DeleteSession(userID, oldest.DeviceKey)
|
return err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
return errors.WithStack(errs.TooManyDevices)
|
return errors.WithStack(errs.TooManyDevices)
|
||||||
|
@ -73,8 +122,14 @@ func Handle(userID uint, deviceKey, ua, ip string) error {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
s := &model.Session{UserID: userID, DeviceKey: deviceKey, UserAgent: ua, IP: ip, LastActive: now, Status: model.SessionActive}
|
return db.CreateSession(&model.Session{
|
||||||
return db.CreateSession(s)
|
UserID: userID,
|
||||||
|
DeviceKey: deviceKey,
|
||||||
|
UserAgent: ua,
|
||||||
|
IP: ip,
|
||||||
|
LastActive: now,
|
||||||
|
Status: model.SessionActive,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Refresh updates last_active for the session.
|
// Refresh updates last_active for the session.
|
||||||
|
|
|
@ -3,6 +3,8 @@ package handles
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
"image/png"
|
"image/png"
|
||||||
"path"
|
"path"
|
||||||
"strings"
|
"strings"
|
||||||
|
@ -10,12 +12,14 @@ import (
|
||||||
|
|
||||||
"github.com/Xhofe/go-cache"
|
"github.com/Xhofe/go-cache"
|
||||||
"github.com/alist-org/alist/v3/internal/conf"
|
"github.com/alist-org/alist/v3/internal/conf"
|
||||||
|
"github.com/alist-org/alist/v3/internal/device"
|
||||||
|
"github.com/alist-org/alist/v3/internal/errs"
|
||||||
"github.com/alist-org/alist/v3/internal/model"
|
"github.com/alist-org/alist/v3/internal/model"
|
||||||
"github.com/alist-org/alist/v3/internal/op"
|
"github.com/alist-org/alist/v3/internal/op"
|
||||||
"github.com/alist-org/alist/v3/internal/session"
|
"github.com/alist-org/alist/v3/internal/session"
|
||||||
"github.com/alist-org/alist/v3/internal/setting"
|
"github.com/alist-org/alist/v3/internal/setting"
|
||||||
|
"github.com/alist-org/alist/v3/pkg/utils"
|
||||||
"github.com/alist-org/alist/v3/server/common"
|
"github.com/alist-org/alist/v3/server/common"
|
||||||
"github.com/alist-org/alist/v3/server/middlewares"
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/pquerna/otp/totp"
|
"github.com/pquerna/otp/totp"
|
||||||
)
|
)
|
||||||
|
@ -83,17 +87,29 @@ func loginHash(c *gin.Context, req *LoginReq) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// generate device session
|
|
||||||
if !middlewares.HandleSession(c, user) {
|
clientID := c.GetHeader("Client-Id")
|
||||||
|
if clientID == "" {
|
||||||
|
clientID = c.Query("client_id")
|
||||||
|
}
|
||||||
|
key := utils.GetMD5EncodeStr(fmt.Sprintf("%d-%s",
|
||||||
|
user.ID, clientID))
|
||||||
|
|
||||||
|
if err := device.EnsureActiveOnLogin(user.ID, key, c.Request.UserAgent(), c.ClientIP()); err != nil {
|
||||||
|
if errors.Is(err, errs.TooManyDevices) {
|
||||||
|
common.ErrorResp(c, err, 403)
|
||||||
|
} else {
|
||||||
|
common.ErrorResp(c, err, 400, true)
|
||||||
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// generate token
|
// generate token
|
||||||
token, err := common.GenerateToken(user)
|
token, err := common.GenerateToken(user)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
common.ErrorResp(c, err, 400, true)
|
common.ErrorResp(c, err, 400, true)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
key := c.GetString("device_key")
|
|
||||||
common.SuccessResp(c, gin.H{"token": token, "device_key": key})
|
common.SuccessResp(c, gin.H{"token": token, "device_key": key})
|
||||||
loginCache.Del(ip)
|
loginCache.Del(ip)
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,10 +2,12 @@ package middlewares
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/subtle"
|
"crypto/subtle"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"github.com/alist-org/alist/v3/internal/conf"
|
"github.com/alist-org/alist/v3/internal/conf"
|
||||||
"github.com/alist-org/alist/v3/internal/device"
|
"github.com/alist-org/alist/v3/internal/device"
|
||||||
|
"github.com/alist-org/alist/v3/internal/errs"
|
||||||
"github.com/alist-org/alist/v3/internal/model"
|
"github.com/alist-org/alist/v3/internal/model"
|
||||||
"github.com/alist-org/alist/v3/internal/op"
|
"github.com/alist-org/alist/v3/internal/op"
|
||||||
"github.com/alist-org/alist/v3/internal/setting"
|
"github.com/alist-org/alist/v3/internal/setting"
|
||||||
|
@ -106,9 +108,15 @@ func HandleSession(c *gin.Context, user *model.User) bool {
|
||||||
if clientID == "" {
|
if clientID == "" {
|
||||||
clientID = c.Query("client_id")
|
clientID = c.Query("client_id")
|
||||||
}
|
}
|
||||||
key := utils.GetMD5EncodeStr(fmt.Sprintf("%d-%s-%s-%s", user.ID, c.Request.UserAgent(), c.ClientIP(), clientID))
|
key := utils.GetMD5EncodeStr(fmt.Sprintf("%d-%s", user.ID, clientID))
|
||||||
if err := device.Handle(user.ID, key, c.Request.UserAgent(), c.ClientIP()); err != nil {
|
if err := device.Handle(user.ID, key, c.Request.UserAgent(), c.ClientIP()); err != nil {
|
||||||
common.ErrorResp(c, err, 403)
|
token := c.GetHeader("Authorization")
|
||||||
|
if errors.Is(err, errs.SessionInactive) {
|
||||||
|
_ = common.InvalidateToken(token)
|
||||||
|
common.ErrorResp(c, err, 401)
|
||||||
|
} else {
|
||||||
|
common.ErrorResp(c, err, 403)
|
||||||
|
}
|
||||||
c.Abort()
|
c.Abort()
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue