315 lines
8.4 KiB
Go
315 lines
8.4 KiB
Go
package authser
|
|
|
|
import (
|
|
"crypto/rand"
|
|
"encoding/hex"
|
|
"fmt"
|
|
"net/http"
|
|
"net/url"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
"videoweb/pub/common/log"
|
|
"videoweb/pub/common/useragent"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
)
|
|
|
|
const guestDevIDCookie = "guest_dev_id"
|
|
const guestSkipAutoCookie = "guest_skip_auto"
|
|
|
|
// EnsureGuestOnEnter 页面进入时:无 token(未自己登录)则走游客登录并写 cookie。
|
|
// 对应上游 POST /api/app/auth/guest。成功后会把会话补进当前 Request,便于同一次 SSR 读到登录态。
|
|
// 手动退出后带 guest_skip_auto 时跳过本次;清掉标记后,下次进页再自动游客。
|
|
func EnsureGuestOnEnter(c *gin.Context) *LoginResp {
|
|
if c == nil || c.Request == nil {
|
|
return nil
|
|
}
|
|
if user := freshRequestAuthUser(c); user != nil {
|
|
return user
|
|
}
|
|
if requestAutoAuthSuppressed(c) {
|
|
return nil
|
|
}
|
|
// 已有任意会话(游客或正式账号)则跳过
|
|
if useragent.TokenFromRequest(c.Request) != "" {
|
|
return nil
|
|
}
|
|
// 手动退出:本次进页不自动游客,并清除标记,供下次进页恢复
|
|
if IsGuestSkipAuto(c) {
|
|
ClearGuestSkipAuto(c)
|
|
dropRequestCookie(c, guestSkipAutoCookie)
|
|
suppressRequestAutoAuth(c)
|
|
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)
|
|
return user
|
|
}
|
|
|
|
// SyncUserOnEnter 是 SSR 页面统一的会话同步入口。同一请求已经通过游客登录、
|
|
// getInfo 或其他认证流程取得过最新用户时直接复用,避免再次访问上游和重复写 Cookie。
|
|
func SyncUserOnEnter(c *gin.Context) *LoginResp {
|
|
if c == nil || c.Request == nil {
|
|
return nil
|
|
}
|
|
if user := freshRequestAuthUser(c); user != nil {
|
|
return user
|
|
}
|
|
if requestAutoAuthSuppressed(c) {
|
|
return nil
|
|
}
|
|
if useragent.TokenFromRequest(c.Request) == "" {
|
|
return EnsureGuestOnEnter(c)
|
|
}
|
|
return RefreshUserOnEnter(c)
|
|
}
|
|
|
|
// RefreshUserOnEnter 页面进入时用 GET /api/app/auth/getInfo 刷新用户信息并写回 cookie。
|
|
// 保证刷新后头部昵称、头像、金币等与上游一致(如解锁后余额变化)。
|
|
func RefreshUserOnEnter(c *gin.Context) *LoginResp {
|
|
if c == nil || c.Request == nil {
|
|
return nil
|
|
}
|
|
if user := freshRequestAuthUser(c); user != nil {
|
|
return user
|
|
}
|
|
if requestAutoAuthSuppressed(c) {
|
|
return nil
|
|
}
|
|
token := useragent.TokenFromRequest(c.Request)
|
|
if token == "" {
|
|
return nil
|
|
}
|
|
|
|
env, user, err := GetInfo(c)
|
|
if err != nil {
|
|
log.Error("refresh user on enter failed", log.E(err))
|
|
return nil
|
|
}
|
|
if env != nil && IsSessionExpiredCode(env.Code) {
|
|
if retry, _ := OnSessionExpired(c, env.Code); retry {
|
|
// 游客重建已经返回完整用户并写好 Cookie,无需立刻再 getInfo 一次。
|
|
if fresh := freshRequestAuthUser(c); fresh != nil {
|
|
return fresh
|
|
}
|
|
env, user, err = GetInfo(c)
|
|
if err != nil {
|
|
log.Error("refresh user on enter retry failed", log.E(err))
|
|
return nil
|
|
}
|
|
} else {
|
|
return nil
|
|
}
|
|
}
|
|
if env == nil || env.Code != 200 || user == nil {
|
|
return nil
|
|
}
|
|
if strings.TrimSpace(user.Token) == "" {
|
|
user.Token = useragent.TokenFromRequest(c.Request)
|
|
if user.Token == "" {
|
|
user.Token = token
|
|
}
|
|
}
|
|
SetAuthCookies(c, user)
|
|
return user
|
|
}
|
|
|
|
// RefreshGuestSession 游客会话失效时(如上游 6001 用户不存在)重新游客登录。
|
|
// 正式注册账号不处理,避免把用户踢成游客。
|
|
func RefreshGuestSession(c *gin.Context) *LoginResp {
|
|
if c == nil || c.Request == nil {
|
|
return nil
|
|
}
|
|
if IsRegisteredFromCookies(c) {
|
|
return nil
|
|
}
|
|
forgetRequestAuthUser(c)
|
|
dropRequestCookie(c, "token")
|
|
dropRequestCookie(c, "_token_")
|
|
return EnsureGuestOnEnter(c)
|
|
}
|
|
|
|
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, "; "))
|
|
}
|
|
|
|
// MarkGuestSkipAuto 手动退出后标记:跳过紧接着的一次自动游客登录。
|
|
func MarkGuestSkipAuto(c *gin.Context) {
|
|
if c == nil {
|
|
return
|
|
}
|
|
suppressRequestAutoAuth(c)
|
|
secure := c.Request != nil && c.Request.TLS != nil
|
|
writeCookie(c, &http.Cookie{
|
|
Name: guestSkipAutoCookie,
|
|
Value: "1",
|
|
Path: "/",
|
|
MaxAge: 0, // session cookie
|
|
HttpOnly: false,
|
|
Secure: secure,
|
|
SameSite: http.SameSiteLaxMode,
|
|
})
|
|
}
|
|
|
|
// IsGuestSkipAuto 是否处于「手动退出后跳过自动游客」状态。
|
|
func IsGuestSkipAuto(c *gin.Context) bool {
|
|
return cookieValue(c, guestSkipAutoCookie) == "1"
|
|
}
|
|
|
|
// ClearGuestSkipAuto 清除手动退出跳过标记。
|
|
func ClearGuestSkipAuto(c *gin.Context) {
|
|
if c == nil {
|
|
return
|
|
}
|
|
writeCookie(c, &http.Cookie{
|
|
Name: guestSkipAutoCookie,
|
|
Value: "",
|
|
Path: "/",
|
|
MaxAge: -1,
|
|
HttpOnly: false,
|
|
SameSite: http.SameSiteLaxMode,
|
|
})
|
|
}
|
|
|
|
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)
|
|
}
|
|
|
|
// ResolveChannel 统一解析渠道码并按首次归因落盘。
|
|
// 优先级:已有 channel cookie > 调用方传入值 > dc/ch/pc 查询参数;已有渠道不覆盖。
|
|
func ResolveChannel(c *gin.Context, candidate string) string {
|
|
channel := strings.TrimSpace(candidate)
|
|
if c == nil || c.Request == nil {
|
|
return channel
|
|
}
|
|
if stored := cookieValue(c, "channel"); stored != "" {
|
|
return stored
|
|
}
|
|
if channel == "" {
|
|
for _, key := range []string{"dc", "ch", "pc"} {
|
|
if value := strings.TrimSpace(c.Query(key)); value != "" {
|
|
channel = value
|
|
break
|
|
}
|
|
}
|
|
}
|
|
if channel == "" {
|
|
return ""
|
|
}
|
|
|
|
secure := c.Request.TLS != nil
|
|
setCookie(c, "channel", channel, secure)
|
|
patchRequestCookie(c, "channel", channel)
|
|
return channel
|
|
}
|
|
|
|
// guestChannel 为首屏服务端自动游客登录补渠道;cookie 用于复用前端首次归因结果。
|
|
func guestChannel(c *gin.Context) string {
|
|
return ResolveChannel(c, "")
|
|
}
|
|
|
|
func patchAuthCookiesToRequest(c *gin.Context, user *LoginResp) {
|
|
if c == nil || user == nil {
|
|
return
|
|
}
|
|
if strings.TrimSpace(user.Token) != "" {
|
|
patchRequestCookie(c, "token", user.Token)
|
|
}
|
|
patchRequestCookie(c, "userId", strconv.FormatInt(user.UserID, 10))
|
|
patchRequestCookie(c, "accountNo", user.AccountNo)
|
|
patchRequestCookie(c, "nickName", user.NickName)
|
|
patchRequestCookie(c, "introduction", user.Introduction)
|
|
patchRequestCookie(c, "avatar", user.Avatar)
|
|
patchRequestCookie(c, "email", user.Email)
|
|
patchRequestCookie(c, "inviteCode", user.InviteCode)
|
|
patchRequestCookie(c, "recharge", strconv.FormatInt(user.Recharge, 10))
|
|
patchRequestCookie(c, "vipType", strconv.Itoa(user.VipType))
|
|
patchRequestCookie(c, "currentVipExpire", user.CurrentVipExpire)
|
|
patchRequestCookie(c, "showCardId", strconv.FormatInt(user.ShowCardID, 10))
|
|
patchRequestCookie(c, "showCardName", user.ShowCardName)
|
|
patchRequestCookie(c, "userType", strconv.Itoa(user.Type))
|
|
isReg := "0"
|
|
if user.IsRegister || user.Type == 1 {
|
|
isReg = "1"
|
|
}
|
|
patchRequestCookie(c, "isRegister", isReg)
|
|
}
|
|
|
|
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 ""
|
|
}
|