Files
alist/server/middlewares/auth.go
千石 c64f899a63 feat: implement session management (#9286)
* feat(auth): Added device session management

- Added the `handleSession` function to manage user device sessions and verify client identity
- Updated `auth.go` to call `handleSession` for device handling when a user logs in
- Added the `Session` model to database migrations
- Added `device.go` and `session.go` files to handle device session logic
- Updated `settings.go` to add device-related configuration items, such as the maximum number of devices, device eviction policy, and session TTL

* feat(session): Adds session management features

- Added `SessionInactive` error type in `device.go`
- Added session-related APIs in `router.go` to support listing and evicting sessions
- Added `ListSessionsByUser`, `ListSessions`, and `MarkInactive` methods in `session.go`
- Returns an appropriate error when the session state is `SessionInactive`

* feat(auth): Marks the device session as invalid.

- Import the `session` package into the `auth` module to handle device session status.
- Add a check in the login logic. If `device_key` is obtained, call `session.MarkInactive` to mark the device session as invalid.
- Store the invalid status in the context variable `session_inactive` for subsequent middleware checks.
- Add a check in the session refresh logic to abort the process if the current session has been marked invalid.

* feat(auth, session): Added device information processing and session management changes

- Updated device handling logic in `auth.go` to pass user agent and IP information
- Adjusted database queries in `session.go` to optimize session query fields and add `user_agent` and `ip` fields
- Modified the `Handle` method to add `ua` and `ip` parameters to store the user agent and IP address
- Added the `SessionResp` structure to return a session response containing `user_agent` and `ip`
- Updated the `/admin/user/create` and `/webdav` endpoints to pass the user agent and IP address to the device handler
2025-08-25 19:46:38 +08:00

205 lines
4.6 KiB
Go

package middlewares
import (
"crypto/subtle"
"fmt"
"github.com/alist-org/alist/v3/internal/conf"
"github.com/alist-org/alist/v3/internal/device"
"github.com/alist-org/alist/v3/internal/model"
"github.com/alist-org/alist/v3/internal/op"
"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/gin-gonic/gin"
log "github.com/sirupsen/logrus"
)
// Auth is a middleware that checks if the user is logged in.
// if token is empty, set user to guest
func Auth(c *gin.Context) {
token := c.GetHeader("Authorization")
if subtle.ConstantTimeCompare([]byte(token), []byte(setting.GetStr(conf.Token))) == 1 {
admin, err := op.GetAdmin()
if err != nil {
common.ErrorResp(c, err, 500)
c.Abort()
return
}
if !handleSession(c, admin) {
return
}
log.Debugf("use admin token: %+v", admin)
c.Next()
return
}
if token == "" {
guest, err := op.GetGuest()
if err != nil {
common.ErrorResp(c, err, 500)
c.Abort()
return
}
if guest.Disabled {
common.ErrorStrResp(c, "Guest user is disabled, login please", 401)
c.Abort()
return
}
if len(guest.Role) > 0 {
roles, err := op.GetRolesByUserID(guest.ID)
if err != nil {
common.ErrorStrResp(c, fmt.Sprintf("Fail to load guest roles: %v", err), 500)
c.Abort()
return
}
guest.RolesDetail = roles
}
if !handleSession(c, guest) {
return
}
log.Debugf("use empty token: %+v", guest)
c.Next()
return
}
userClaims, err := common.ParseToken(token)
if err != nil {
common.ErrorResp(c, err, 401)
c.Abort()
return
}
user, err := op.GetUserByName(userClaims.Username)
if err != nil {
common.ErrorResp(c, err, 401)
c.Abort()
return
}
// validate password timestamp
if userClaims.PwdTS != user.PwdTS {
common.ErrorStrResp(c, "Password has been changed, login please", 401)
c.Abort()
return
}
if user.Disabled {
common.ErrorStrResp(c, "Current user is disabled, replace please", 401)
c.Abort()
return
}
if len(user.Role) > 0 {
roles, err := op.GetRolesByUserID(user.ID)
if err != nil {
common.ErrorStrResp(c, fmt.Sprintf("Fail to load roles: %v", err), 500)
c.Abort()
return
}
user.RolesDetail = roles
}
if !handleSession(c, user) {
return
}
log.Debugf("use login token: %+v", user)
c.Next()
}
func handleSession(c *gin.Context, user *model.User) bool {
clientID := c.GetHeader("Client-Id")
if clientID == "" {
clientID = c.Query("client_id")
}
key := utils.GetMD5EncodeStr(fmt.Sprintf("%d-%s-%s-%s", user.ID, c.Request.UserAgent(), c.ClientIP(), clientID))
if err := device.Handle(user.ID, key, c.Request.UserAgent(), c.ClientIP()); err != nil {
common.ErrorResp(c, err, 403)
c.Abort()
return false
}
c.Set("device_key", key)
c.Set("user", user)
return true
}
func Authn(c *gin.Context) {
token := c.GetHeader("Authorization")
if subtle.ConstantTimeCompare([]byte(token), []byte(setting.GetStr(conf.Token))) == 1 {
admin, err := op.GetAdmin()
if err != nil {
common.ErrorResp(c, err, 500)
c.Abort()
return
}
c.Set("user", admin)
log.Debugf("use admin token: %+v", admin)
c.Next()
return
}
if token == "" {
guest, err := op.GetGuest()
if err != nil {
common.ErrorResp(c, err, 500)
c.Abort()
return
}
c.Set("user", guest)
log.Debugf("use empty token: %+v", guest)
c.Next()
return
}
userClaims, err := common.ParseToken(token)
if err != nil {
common.ErrorResp(c, err, 401)
c.Abort()
return
}
user, err := op.GetUserByName(userClaims.Username)
if err != nil {
common.ErrorResp(c, err, 401)
c.Abort()
return
}
// validate password timestamp
if userClaims.PwdTS != user.PwdTS {
common.ErrorStrResp(c, "Password has been changed, login please", 401)
c.Abort()
return
}
if user.Disabled {
common.ErrorStrResp(c, "Current user is disabled, replace please", 401)
c.Abort()
return
}
if len(user.Role) > 0 {
var roles []model.Role
for _, roleID := range user.Role {
role, err := op.GetRole(uint(roleID))
if err != nil {
common.ErrorStrResp(c, fmt.Sprintf("load role %d failed", roleID), 500)
c.Abort()
return
}
roles = append(roles, *role)
}
user.RolesDetail = roles
}
c.Set("user", user)
log.Debugf("use login token: %+v", user)
c.Next()
}
func AuthNotGuest(c *gin.Context) {
user := c.MustGet("user").(*model.User)
if user.IsGuest() {
common.ErrorStrResp(c, "You are a guest", 403)
c.Abort()
} else {
c.Next()
}
}
func AuthAdmin(c *gin.Context) {
user := c.MustGet("user").(*model.User)
if !user.IsAdmin() {
common.ErrorStrResp(c, "You are not an admin", 403)
c.Abort()
} else {
c.Next()
}
}