forked from alterminal/alterminal
77 lines
2.4 KiB
Go
77 lines
2.4 KiB
Go
package main
|
|
|
|
import (
|
|
"crypto/rand"
|
|
"encoding/base64"
|
|
"errors"
|
|
"fmt"
|
|
"time"
|
|
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
// Session 為使用者瀏覽器 Session(SSO 核心):ID 為加密安全亂數,
|
|
// 存於 HttpOnly Cookie,效期內使用者再經任何 RP 發起授權請求時
|
|
// 無須重新輸入帳密。
|
|
type Session struct {
|
|
ID string `gorm:"primaryKey;size:43"` // 32 bytes 亂數的 base64url
|
|
UserID uint `gorm:"not null;index"`
|
|
User User
|
|
ExpiresAt time.Time `gorm:"not null"`
|
|
CreatedAt time.Time
|
|
UpdatedAt time.Time
|
|
}
|
|
|
|
// sessionTTL 為 Session 有效時間,到期後 Cookie 失效、列為可清除。
|
|
const sessionTTL = 24 * time.Hour
|
|
|
|
// ErrSessionExpired 表示 Session 不存在或已過期。
|
|
var ErrSessionExpired = errors.New("session 不存在或已過期")
|
|
|
|
// newRandomToken 產生 n bytes 加密安全亂數的 base64url 字串(無填充;
|
|
// n=32 時為 43 字元),供 Session ID 與 CSRF token 共用。
|
|
func newRandomToken(n int) (string, error) {
|
|
b := make([]byte, n)
|
|
if _, err := rand.Read(b); err != nil {
|
|
return "", fmt.Errorf("read random: %w", err)
|
|
}
|
|
return base64.RawURLEncoding.EncodeToString(b), nil
|
|
}
|
|
|
|
// createSession 為使用者建立新 Session,順帶刪除所有已過期 Session
|
|
// (最佳清除,失敗不影響登入結果)。
|
|
func createSession(db *gorm.DB, userID uint) (*Session, error) {
|
|
id, err := newRandomToken(32)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
s := &Session{ID: id, UserID: userID, ExpiresAt: time.Now().Add(sessionTTL)}
|
|
if err := db.Create(s).Error; err != nil {
|
|
return nil, fmt.Errorf("create session: %w", err)
|
|
}
|
|
db.Where("expires_at < ?", time.Now()).Delete(&Session{})
|
|
return s, nil
|
|
}
|
|
|
|
// deleteSession 以 ID 刪除 Session(登出用)。查無該 Session 不視為
|
|
// 錯誤,讓登出維持冪等。
|
|
func deleteSession(db *gorm.DB, id string) error {
|
|
if err := db.Delete(&Session{}, "id = ?", id).Error; err != nil {
|
|
return fmt.Errorf("delete session: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// getSession 以 ID 查詢效期內的 Session(含所屬使用者)。
|
|
func getSession(db *gorm.DB, id string) (*Session, error) {
|
|
var s Session
|
|
err := db.Preload("User").Where("id = ? AND expires_at > ?", id, time.Now()).First(&s).Error
|
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
return nil, ErrSessionExpired
|
|
}
|
|
if err != nil {
|
|
return nil, fmt.Errorf("query session: %w", err)
|
|
}
|
|
return &s, nil
|
|
}
|