Initial commit

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
谢宇宁
2026-10-22 14:47:17 +07:00
co-authored by Claude Opus 5.5
commit 3639d2184a
574 changed files with 61690 additions and 0 deletions
+113
View File
@@ -0,0 +1,113 @@
package authser
import (
"encoding/json"
"fmt"
"net/http"
"net/url"
"strings"
"time"
"videoweb/app/service/upstream"
"videoweb/pub/common/useragent"
"github.com/gin-gonic/gin"
)
const (
guestPath = "/api/app/auth/guest"
cookieMaxAge = 7 * 24 * 3600
)
// GuestLogin 调用上游游客登录,站点不提供注册/登录,上游内容接口依赖游客会话。
func GuestLogin(c *gin.Context, req GuestLoginReq) (*APIEnvelope, *LoginResp, error) {
var env APIEnvelope
if _, err := upstream.PostJSON(c, guestPath, req, &env); err != nil {
return nil, nil, err
}
if env.Code != 200 {
return &env, nil, nil
}
user, err := decodeLoginData(env.Data)
if err != nil {
return &env, nil, err
}
return &env, user, nil
}
func decodeLoginData(raw json.RawMessage) (*LoginResp, error) {
if len(raw) == 0 || string(raw) == "null" || string(raw) == `""` {
return nil, fmt.Errorf("empty login data")
}
var user LoginResp
if err := json.Unmarshal(raw, &user); err != nil {
return nil, err
}
return &user, nil
}
// SetAuthCookies 写入游客会话 cookie。
func SetAuthCookies(c *gin.Context, user *LoginResp) {
if c == nil || user == nil {
return
}
secure := c.Request != nil && c.Request.TLS != nil
if strings.TrimSpace(user.Token) != "" {
setCookie(c, "token", user.Token, secure)
}
if sid := useragent.SIDFromRequest(c.Request); sid != "" {
setCookie(c, "sid", sid, secure)
}
}
func setCookie(c *gin.Context, name, value string, secure bool) {
// Cookie Value 仅允许 ASCII;使用 PathEscape(空格→%20),与前端 encodeURIComponent 一致。
http.SetCookie(c.Writer, &http.Cookie{
Name: name,
Value: url.PathEscape(value),
Path: "/",
MaxAge: cookieMaxAge,
HttpOnly: false,
Secure: secure,
SameSite: http.SameSiteLaxMode,
Expires: time.Now().Add(7 * 24 * time.Hour),
})
}
func cookieValue(c *gin.Context, name string) string {
ck, err := c.Request.Cookie(name)
if err != nil || ck == nil {
return ""
}
raw := strings.TrimSpace(ck.Value)
if raw == "" {
return ""
}
// QueryUnescape 同时兼容历史 QueryEscape(+)与 PathEscape(%20)
if decoded, err := url.QueryUnescape(raw); err == nil {
return strings.TrimSpace(decoded)
}
return raw
}
// ClearAuthCookies 清除会话 cookie,同时清理旧版账号体系遗留的用户信息 cookie。
func ClearAuthCookies(c *gin.Context) {
for _, name := range authCookieNames() {
http.SetCookie(c.Writer, &http.Cookie{
Name: name,
Value: "",
Path: "/",
MaxAge: -1,
HttpOnly: false,
SameSite: http.SameSiteLaxMode,
})
}
}
func authCookieNames() []string {
return []string{
"token", "_token_", "sid", "_s_token_",
"userId", "accountNo", "nickName", "introduction", "avatar",
"email", "inviteCode", "recharge", "vipType", "currentVipExpire", "showCardId", "showCardName",
"userType", "isRegister", "guest_skip_auto",
}
}
+137
View File
@@ -0,0 +1,137 @@
package authser
import (
"crypto/rand"
"encoding/hex"
"fmt"
"net/url"
"strconv"
"strings"
"time"
"videoweb/pub/common/log"
"videoweb/pub/common/useragent"
"github.com/gin-gonic/gin"
)
const guestDevIDCookie = "guest_dev_id"
// EnsureGuestOnEnter 页面进入时:无 token 则走游客登录并写 cookie。
// 对应上游 POST /api/app/auth/guest。成功后会把会话补进当前 Request,便于同一次 SSR 直接使用。
func EnsureGuestOnEnter(c *gin.Context) *LoginResp {
if c == nil || c.Request == nil {
return nil
}
// 已有会话则跳过
if useragent.TokenFromRequest(c.Request) != "" {
return nil
}
devID := ensureGuestDevID(c)
env, user, err := GuestLogin(c, GuestLoginReq{
DevID: devID,
DeviceID: devID,
Channel: guestChannel(c),
})
if err != nil {
log.Error("ensure guest on enter failed", log.E(err))
return nil
}
if env == nil || env.Code != 200 || user == nil || strings.TrimSpace(user.Token) == "" {
tip := ""
if env != nil {
tip = firstNonEmptyTip(env.Tip, env.Msg)
}
log.Error("ensure guest on enter rejected", log.E(fmt.Errorf("code=%v tip=%s", envCode(env), tip)))
return nil
}
SetAuthCookies(c, user)
patchRequestCookie(c, "token", user.Token)
return user
}
func dropRequestCookie(c *gin.Context, name string) {
if c == nil || c.Request == nil || name == "" {
return
}
var parts []string
for _, ck := range c.Request.Cookies() {
if ck == nil || ck.Name == name {
continue
}
parts = append(parts, ck.Name+"="+ck.Value)
}
c.Request.Header.Set("Cookie", strings.Join(parts, "; "))
}
func ensureGuestDevID(c *gin.Context) string {
if id := cookieValue(c, guestDevIDCookie); id != "" {
return id
}
id := newGuestDevID()
secure := c.Request != nil && c.Request.TLS != nil
setCookie(c, guestDevIDCookie, id, secure)
patchRequestCookie(c, guestDevIDCookie, id)
return id
}
func newGuestDevID() string {
b := make([]byte, 8)
if _, err := rand.Read(b); err != nil {
now := time.Now()
return fmt.Sprintf("%016X%d", uint64(now.UnixNano()), now.UnixMilli())
}
return strings.ToUpper(hex.EncodeToString(b)) + strconv.FormatInt(time.Now().UnixMilli(), 10)
}
// guestChannel 为首屏服务端自动游客登录补渠道;cookie 用于复用前端首次归因结果。
func guestChannel(c *gin.Context) string {
if c == nil || c.Request == nil {
return ""
}
if channel := cookieValue(c, "channel"); channel != "" {
return channel
}
for _, key := range []string{"dc", "ch", "pc"} {
if channel := strings.TrimSpace(c.Query(key)); channel != "" {
secure := c.Request.TLS != nil
setCookie(c, "channel", channel, secure)
patchRequestCookie(c, "channel", channel)
return channel
}
}
return ""
}
func patchRequestCookie(c *gin.Context, name, value string) {
if c == nil || c.Request == nil || name == "" {
return
}
escaped := url.PathEscape(value)
var parts []string
for _, ck := range c.Request.Cookies() {
if ck == nil || ck.Name == name {
continue
}
parts = append(parts, ck.Name+"="+ck.Value)
}
parts = append(parts, name+"="+escaped)
c.Request.Header.Set("Cookie", strings.Join(parts, "; "))
}
func envCode(env *APIEnvelope) int {
if env == nil {
return 0
}
return env.Code
}
func firstNonEmptyTip(values ...string) string {
for _, v := range values {
if strings.TrimSpace(v) != "" {
return v
}
}
return ""
}
+28
View File
@@ -0,0 +1,28 @@
package authser
import "github.com/gin-gonic/gin"
// TokenExpiredCode 上游约定:token 失效。
const TokenExpiredCode = 1000
// IsSessionExpiredCode 是否为会话失效类业务码。
func IsSessionExpiredCode(code int) bool {
switch code {
case TokenExpiredCode, 1002, 1012, 6001: // InvalidToken / NoToken / InvalidateToken / 用户不存在
return true
default:
return false
}
}
// RecoverContentSession 会话失效后清理旧 cookie 并重新建立游客会话,调用方随后重试一次原请求。
func RecoverContentSession(c *gin.Context, code int) bool {
if c == nil || !IsSessionExpiredCode(code) {
return false
}
ClearAuthCookies(c)
for _, name := range authCookieNames() {
dropRequestCookie(c, name)
}
return EnsureGuestOnEnter(c) != nil
}
+26
View File
@@ -0,0 +1,26 @@
package authser
import "encoding/json"
// GuestLoginReq 对应 swagger authser.GuestLoginReq
// POST /api/app/auth/guest
type GuestLoginReq struct {
DevID string `json:"devID"`
DeviceID string `json:"deviceId,omitempty"`
Mac string `json:"mac,omitempty"`
Channel string `json:"channel,omitempty"`
}
// LoginResp 游客登录成功 data(只取会话所需字段)。
type LoginResp struct {
Token string `json:"token"`
UserID int64 `json:"userId"`
}
// APIEnvelope 上游统一响应壳。Data 在失败时可能是 string/null。
type APIEnvelope struct {
Code int `json:"code"`
Msg string `json:"msg"`
Tip string `json:"tip"`
Data json.RawMessage `json:"data"`
}