Files
past-go/past-go/internal/httpapi/auth.go
T

193 lines
5.0 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package httpapi
import (
"crypto/rand"
"encoding/hex"
"net/http"
"regexp"
"sync"
"time"
)
var bearerRe = regexp.MustCompile(`Bearer\s(\S+)`)
// Token 对应 getBearerToken() + $_POST['token'] + $_GET['token'] 三通道
func (c *Ctx) Token() string {
if m := bearerRe.FindStringSubmatch(c.G.GetHeader("Authorization")); m != nil {
return m[1]
}
if t := c.Form("token"); t != "" {
return t
}
if t := c.Q("token"); t != "" {
return t
}
return ""
}
// UID 对应 getUserId():通过 token 查 user_tokens;未登录返回 0。
func (c *Ctx) UID() int64 {
if c.uidResolved {
return c.uid
}
c.uidResolved = true
tok := c.Token()
if tok == "" {
return 0
}
row, err := c.DB.QueryOne("SELECT user_id FROM user_tokens WHERE token = ? AND expires_at > NOW()", tok)
if err != nil || row == nil {
return 0
}
c.uid = row.Int("user_id")
return c.uid
}
// CoupleID 对应 getCoupleId();无则返回 0
func (c *Ctx) CoupleID() int64 {
if c.coupleResolved {
return c.coupleID
}
c.coupleResolved = true
uid := c.UID()
if uid == 0 {
return 0
}
row, _ := c.DB.QueryOne("SELECT couple_id FROM users WHERE id = ?", uid)
if row == nil {
return 0
}
c.coupleID = row.Int("couple_id")
return c.coupleID
}
// NewToken 生成 32 位 hex token(与 PHP session_id 格式一致)
func NewToken() string {
b := make([]byte, 16)
rand.Read(b)
return hex.EncodeToString(b)
}
// SaveUserToken 对应 saveUserToken(),写入新 token(7 天)并清理过期 token。
// PHP 会删除该用户旧 token,但旧 session 文件仍然有效(实际上允许多端同时登录);
// Go 没有 session 文件,因此保留旧 token 以维持同样的多端登录效果。
func (c *Ctx) SaveUserToken(userID int64, token string) {
c.DB.Exec("DELETE FROM user_tokens WHERE expires_at < NOW()")
c.DB.Exec("INSERT INTO user_tokens (token, user_id, expires_at) VALUES (?, ?, DATE_ADD(NOW(), INTERVAL 7 DAY))", token, userID)
}
// requireUserLogin 对应 requireUserLogin():401/404,并做 5 分钟去重的登录记录
func (c *Ctx) requireUserLogin() error {
uid := c.UID()
if uid == 0 {
return c.Err(401, "请先登录")
}
if err := c.DB.AdvanceUnbindings(); err != nil {
return err
}
user, err := c.DB.QueryOne("SELECT couple_id, last_login_at FROM users WHERE id = ?", uid)
if err != nil {
return err
}
if user == nil {
return c.Err(404, "用户不存在")
}
needRecord := false
if user.IsNull("last_login_at") || user.Str("last_login_at") == "" {
needRecord = true
} else if t, err := time.ParseInLocation("2006-01-02 15:04:05", user.Str("last_login_at"), time.Local); err != nil || time.Since(t) > 300*time.Second {
needRecord = true
}
if needRecord {
c.DB.Exec("UPDATE users SET last_login_at = NOW() WHERE id = ?", uid)
if user.Int("couple_id") != 0 {
c.DB.Exec("INSERT INTO activity_log (couple_id, user_id, action_type, description, detail, created_at) VALUES (?, ?, 'login', '登录了小程序', '', NOW())", user.Int("couple_id"), uid)
}
}
return nil
}
// ---------- 后台管理员会话(对应 PHP Cookie Session)----------
const adminCookieName = "PHPSESSID"
const adminSessionTTL = 12 * time.Hour
type adminSession struct {
AdminID int64
Username string
Expires time.Time
}
type AdminSessions struct {
mu sync.Mutex
m map[string]*adminSession
}
func NewAdminSessions() *AdminSessions {
s := &AdminSessions{m: map[string]*adminSession{}}
go func() {
for range time.Tick(10 * time.Minute) {
s.mu.Lock()
now := time.Now()
for k, v := range s.m {
if v.Expires.Before(now) {
delete(s.m, k)
}
}
s.mu.Unlock()
}
}()
return s
}
func (s *AdminSessions) get(id string) *adminSession {
s.mu.Lock()
defer s.mu.Unlock()
v := s.m[id]
if v == nil || v.Expires.Before(time.Now()) {
delete(s.m, id)
return nil
}
v.Expires = time.Now().Add(adminSessionTTL)
return v
}
// AdminSession 当前请求的管理员会话;未登录返回 nil
func (c *Ctx) AdminSession() *adminSession {
id, err := c.G.Cookie(adminCookieName)
if err != nil || id == "" {
return nil
}
return c.App.Admin.get(id)
}
// AdminLogin 建立管理员会话并写 Cookie(对应 session_start + $_SESSION['admin_logged_in']=true)
func (c *Ctx) AdminLogin(adminID int64, username string) {
id := NewToken()
c.App.Admin.mu.Lock()
c.App.Admin.m[id] = &adminSession{AdminID: adminID, Username: username, Expires: time.Now().Add(adminSessionTTL)}
c.App.Admin.mu.Unlock()
c.G.SetSameSite(http.SameSiteLaxMode)
c.G.SetCookie(adminCookieName, id, 0, "/", "", false, true)
}
// AdminLogout 对应 session_destroy()
func (c *Ctx) AdminLogout() {
if id, err := c.G.Cookie(adminCookieName); err == nil {
c.App.Admin.mu.Lock()
delete(c.App.Admin.m, id)
c.App.Admin.mu.Unlock()
}
c.G.SetCookie(adminCookieName, "", -1, "/", "", false, true)
}
// requireAdminLogin 对应 requireAdminLogin()
func (c *Ctx) requireAdminLogin() error {
if c.AdminSession() == nil {
return c.Err(401, "未登录或登录已过期")
}
return nil
}
func (c *Ctx) SetCoupleID(cid int64) { c.coupleID = cid; c.coupleResolved = true }