@@ -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",
|
||||
}
|
||||
}
|
||||
@@ -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 ""
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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"`
|
||||
}
|
||||
Reference in New Issue
Block a user