This commit is contained in:
2025-06-13 17:23:16 +08:00
parent 3150ba80bc
commit 1b72f51e4a
55 changed files with 3894 additions and 310 deletions

View File

@@ -1,83 +1,86 @@
package session
// import (
// "context"
// "time"
import (
"context"
"errors"
"time"
// "management/internal/pkg/redis"
// )
"github.com/redis/go-redis/v9"
)
// var (
// storePrefix = "scs:session:"
// ctx = context.Background()
// DefaultRedisStore = newRedisStore()
// )
// 为所有 Redis 操作定义一个合理的超时时间。
// 这个值应该根据你的服务SLA和网络状况来定通常在50-500毫秒之间是比较合理的。
// 最佳实践:这个值应该来自配置文件,而不是硬编码。
const redisTimeout = 200 * time.Millisecond
// type redisStore struct{}
// RedisStore 表示一个使用 go-redis/v9 客户端的 scs.Store 实现。
type RedisStore struct {
// 内嵌 go-redis 客户端
client *redis.Client
}
// func newRedisStore() *redisStore {
// return &redisStore{}
// }
// NewRedisStore 是 RedisStore 的构造函数。
func NewRedisStore(client *redis.Client) *RedisStore {
return &RedisStore{
client: client,
}
}
// // Delete should remove the session token and corresponding data from the
// // session store. If the token does not exist then Delete should be a no-op
// // and return nil (not an error).
// func (s *redisStore) Delete(token string) error {
// return redis.Del(ctx, storePrefix+token)
// }
// Find 方法根据 session token 从 Redis 中查找 session 数据。
// 如果 token 不存在或已过期exists 返回 false。
func (s *RedisStore) Find(token string) ([]byte, bool, error) {
// ✅ 最佳实践: 为数据库操作创建带超时的上下文
ctx, cancel := context.WithTimeout(context.Background(), redisTimeout)
// ✅ 必须: 无论函数如何返回,都调用 cancel() 来释放上下文资源
defer cancel()
// // Find should return the data for a session token from the store. If the
// // session token is not found or is expired, the found return value should
// // be false (and the err return value should be nil). Similarly, tampered
// // or malformed tokens should result in a found return value of false and a
// // nil err value. The err return value should be used for system errors only.
// func (s *redisStore) Find(token string) (b []byte, found bool, err error) {
// val, err := redis.GetBytes(ctx, storePrefix+token)
// if err != nil {
// return nil, false, err
// } else {
// return val, true, nil
// }
// }
// 使用 go-redis 的 Get 方法
data, err := s.client.Get(ctx, token).Bytes()
if err != nil {
// 如果 key 不存在go-redis 会返回 redis.Nil 错误
if errors.Is(err, redis.Nil) {
return nil, false, nil
}
return nil, false, err
}
// // Commit should add the session token and data to the store, with the given
// // expiry time. If the session token already exists, then the data and
// // expiry time should be overwritten.
// func (s *redisStore) Commit(token string, b []byte, expiry time.Time) error {
// // TODO: 这边可以调整时间
// exp, err := time.ParseInLocation(time.DateTime, time.Now().Format("2006-01-02")+" 23:59:59", time.Local)
// if err != nil {
// return err
// }
return data, true, nil
}
// t := time.Now()
// expired := exp.Sub(t)
// return redis.Set(ctx, storePrefix+token, b, expired)
// }
// Commit 方法将 session 数据和过期时间存入 Redis。
// 如果 token 已存在,则更新其数据和过期时间。
func (s *RedisStore) Commit(token string, b []byte, expiry time.Time) error {
// ✅ 最佳实践: 为数据库操作创建带超时的上下文
ctx, cancel := context.WithTimeout(context.Background(), redisTimeout)
// ✅ 必须: 无论函数如何返回,都调用 cancel() 来释放上下文资源
defer cancel()
// // All should return a map containing data for all active sessions (i.e.
// // sessions which have not expired). The map key should be the session
// // token and the map value should be the session data. If no active
// // sessions exist this should return an empty (not nil) map.
// func (s *redisStore) All() (map[string][]byte, error) {
// sessions := make(map[string][]byte)
// 计算 Redis 的 TTL (Time To Live)
// time.Until(expiry) 会计算出当前时间到 expiry 之间的时间差
ttl := time.Until(expiry)
// iter := redis.Scan(ctx, 0, storePrefix+"*", 0).Iterator()
// for iter.Next(ctx) {
// key := iter.Val()
// token := key[len(storePrefix):]
// data, exists, err := s.Find(token)
// if err != nil {
// return nil, err
// }
// 使用 go-redis 的 Set 方法,并设置过期时间
// 如果 expiry 时间已经过去ttl 会是负数Redis 会立即删除这个 key这正是我们期望的行为。
err := s.client.Set(ctx, token, b, ttl).Err()
if err != nil {
return err
}
// if exists {
// sessions[token] = data
// }
// }
// if err := iter.Err(); err != nil {
// return nil, err
// }
return nil
}
// return sessions, nil
// }
// Delete 方法根据 session token 从 Redis 中删除 session 数据。
func (s *RedisStore) Delete(token string) error {
// ✅ 最佳实践: 为数据库操作创建带超时的上下文
ctx, cancel := context.WithTimeout(context.Background(), redisTimeout)
// ✅ 必须: 无论函数如何返回,都调用 cancel() 来释放上下文资源
defer cancel()
// 使用 go-redis 的 Del 方法
err := s.client.Del(ctx, token).Err()
if err != nil {
return err
}
return nil
}

View File

@@ -10,9 +10,8 @@ import (
"management/internal/erpserver/model/dto"
"management/internal/pkg/config"
"github.com/alexedwards/scs/postgresstore"
"github.com/alexedwards/scs/v2"
"gorm.io/gorm"
"github.com/redis/go-redis/v9"
)
var ErrNoSession = errors.New("session user not found")
@@ -20,8 +19,8 @@ var ErrNoSession = errors.New("session user not found")
// Manager 抽象核心会话操作
type Manager interface {
Load(next http.Handler) http.Handler
GetUser(ctx context.Context, key string) (*dto.AuthorizeUser, error)
PutUser(ctx context.Context, key string, user *dto.AuthorizeUser) error
GetUser(ctx context.Context, key string) (dto.AuthorizeUser, error)
PutUser(ctx context.Context, key string, user dto.AuthorizeUser) error
RenewToken(ctx context.Context) error
Destroy(ctx context.Context) error
}
@@ -30,7 +29,7 @@ type SCSSession struct {
manager *scs.SessionManager
}
func NewSCSManager(db *gorm.DB, config *config.Config) (Manager, error) {
func NewSCSManager(client *redis.Client, conf *config.Config) (Manager, error) {
sessionManager := scs.New()
sessionManager.Lifetime = 24 * time.Hour
sessionManager.IdleTimeout = 2 * time.Hour
@@ -38,21 +37,21 @@ func NewSCSManager(db *gorm.DB, config *config.Config) (Manager, error) {
sessionManager.Cookie.HttpOnly = true
sessionManager.Cookie.Persist = true
sessionManager.Cookie.SameSite = http.SameSiteStrictMode
sessionManager.Cookie.Secure = config.App.Prod
sessionManager.Cookie.Secure = conf.App.Prod
sqlDB, err := db.DB()
if err != nil {
return nil, err
}
//sqlDB, err := db.DB()
//if err != nil {
// return nil, err
//}
// postgres
// github.com/alexedwards/scs/postgresstore
sessionManager.Store = postgresstore.New(sqlDB)
// sessionManager.Store = postgresstore.New(sqlDB)
// pgx
// github.com/alexedwards/scs/pgxstore
// sessionManager.Store = pgxstore.New(pool)
// redis
// sessionManager.Store = newRedisStore()
sessionManager.Store = NewRedisStore(client)
return &SCSSession{manager: sessionManager}, nil
}
@@ -60,21 +59,21 @@ func (s *SCSSession) Load(next http.Handler) http.Handler {
return s.manager.LoadAndSave(next)
}
func (s *SCSSession) GetUser(ctx context.Context, key string) (*dto.AuthorizeUser, error) {
func (s *SCSSession) GetUser(ctx context.Context, key string) (dto.AuthorizeUser, error) {
data, ok := s.manager.Get(ctx, key).([]byte)
if !ok || len(data) == 0 {
return nil, ErrNoSession
return dto.AuthorizeUser{}, ErrNoSession
}
var user dto.AuthorizeUser
if err := json.Unmarshal(data, &user); err != nil {
return nil, err
return dto.AuthorizeUser{}, err
}
return &user, nil
return user, nil
}
func (s *SCSSession) PutUser(ctx context.Context, key string, user *dto.AuthorizeUser) error {
data, err := json.Marshal(user)
func (s *SCSSession) PutUser(ctx context.Context, key string, user dto.AuthorizeUser) error {
data, err := json.Marshal(&user)
if err != nil {
return err
}