Initial commit

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-15 13:57:10 +08:00
co-authored by Claude Opus 5
commit 8679200f41
1897 changed files with 257900 additions and 0 deletions
+350
View File
@@ -0,0 +1,350 @@
package authweb
import (
"errors"
"net/http"
"strings"
"time"
"91porn-server/common"
"91porn-server/common/constant"
"91porn-server/common/constant/redisconst"
"91porn-server/common/crypt"
"91porn-server/common/log"
"91porn-server/common/stderr"
"91porn-server/models/v/adminmod"
"91porn-server/models/v/ipwhitemod"
"91porn-server/web/webg"
"github.com/gin-gonic/gin"
)
var (
// 白名单,不需要验证token的api
whitelist = map[string]bool{
"/swagger": true,
"/api/web/admin/vid/pullFileInfoFromAws": true,
"/api/web/admin/vid/listDetail": true,
"/api/web/admin/vid/uploadStaticBatch": true,
"/api/web/channel/admin/login": true,
"/api/web/channel/admin/sms/captcha": true,
"/api/web/admin/cdn/sync": true,
"/api/web/admin/extend/syncServerFile": true,
"/api/web/admin/login": true,
"/api/web/admin/slogin": true,
"/api/web/admin/verify": true,
"/api/web/admin/vid/sec": true,
"/api/web/admin/vid/pms/sec": true,
"/api/web/admin/vid/pms/mt_sec": true,
"/api/web/admin/vid/sp/sec": true,
"/api/web/admin/vid/sp/lsjsec": true,
"/api/web/admin/vid/sp/m3u8sec": true,
"/api/web/admin/export/rechargeOrder": true,
"/api/web/admin/export/withdrawOrder": true,
"/api/web/admin/export/goldTurnover": true,
"/api/web/admin/export/videoIncome": true,
"/api/web/admin/export/orderStat": true,
"/api/web/admin/export/exchangecode": true,
"/api/web/district/agent/captcha": true,
"/api/web/district/agent/login": true,
"/api/web/admin/vid/syncNewVideo": true,
"/api/web/admin/newactivity/importModels": true,
"/api/web/admin/vid/spiderSyncSubmit": true,
"/api/web/admin/stats/export": true,
"/api/web/admin/ai/undress/callback": true,
"/api/web/admin/ai/changeface/callback": true,
"/api/web/admin/ai/change_face_img/callback": true,
"/api/web/admin/ai/image_to_video/callback": true,
"/api/web/admin/ai/text_to_image/callback": true,
"/api/web/admin/ai/text_to_novel/callback": true,
"/api/web/laosiji/post/search": true,
"/api/web/laosiji/post/detail": true,
"/api/web/laosiji/comics/search": true,
"/api/web/laosiji/comics/detail": true,
"/api/web/laosiji/novel/search": true,
"/api/web/laosiji/novel/detail": true,
}
InvalidTokenErr = errors.New(" invalid token err")
AccessForbidErr = errors.New(stderr.ErrAccessForbid.Msg())
noTokenMsg = gin.H{
"code": stderr.ErrNoToken,
"msg": stderr.ErrNoToken.Msg(),
}
)
const WarnTokenExpire = 10 * time.Minute
type ActType = string
const (
Admin ActType = "admin"
Channel ActType = "channel"
District ActType = "district"
)
type AdminRole = string
const (
JuShang AdminRole = "聚商"
NudeChatMerchant AdminRole = "裸聊商家"
)
func GetTokenSecret() string {
return webg.Conf.Base.JwtKey
}
type Claims struct {
Type ActType
Act string `json:"act"`
Role string `json:"role"`
CID string `json:"cid"`
}
type claimsWithExp struct {
Claims
Exp int64 `json:"exp"`
}
func genToken(claims *Claims) (string, error) {
c := claimsWithExp{Claims: *claims, Exp: time.Now().Add(redisconst.WebTokenExpire).Unix()}
secret := GetTokenSecret()
args, _ := common.JSONStruct2Map(c)
token, err := crypt.CreateToken(secret, args)
if err != nil {
log.Error("auth web GenToken error", log.Any("claims", claims), log.E(err))
}
return token, err
}
func GenAndSaveToken(claims *Claims) (string, error) {
token, err := genToken(claims)
if err != nil {
return "", err
}
if err = saveToken(claims.Type, claims.Act, token); err != nil {
return "", err
}
return token, nil
}
func ParseToken(token string) (*claimsWithExp, error) {
secret := GetTokenSecret()
claims, err := crypt.ParseToken(secret, token)
if err != nil {
return nil, err
}
c := claimsWithExp{}
return &c, common.Map2JSONStruct(&c, claims)
}
func tokenRedisKey(typ ActType, act string) string {
return redisconst.WebTokenKey(typ, act)
}
func saveToken(typ ActType, act string, token string) error {
key := tokenRedisKey(typ, act)
return webg.Redis.Set(key, token, redisconst.WebTokenExpire)
}
func RevokeDistrictToken(acts ...string) {
keys := make([]string, len(acts))
for i, act := range acts {
keys[i] = tokenRedisKey(District, act)
}
_, _ = webg.Redis.Del(keys...)
}
func RevokeChannelToken(acts ...string) {
keys := make([]string, len(acts))
for i, act := range acts {
keys[i] = tokenRedisKey(Channel, act)
}
_, _ = webg.Redis.Del(keys...)
}
func RevokeAdminToken(acts ...string) {
keys := make([]string, len(acts))
for i, act := range acts {
keys[i] = tokenRedisKey(Admin, act)
}
_, _ = webg.Redis.Del(keys...)
}
func auth(token string) (string, AdminRole, ActType, string, bool, error) {
claims, err := ParseToken(token)
if err != nil {
return "", "", "", "", false, InvalidTokenErr
}
act := claims.Act
typ := claims.Type
role := claims.Role
cid := claims.CID
exp := time.Unix(claims.Exp, 0)
redisKey := tokenRedisKey(typ, act)
redisToken, err := webg.Redis.Get(redisKey)
if err != nil || redisToken == nil {
return "", "", "", "", false, InvalidTokenErr
}
if token != *redisToken { //Redis token过期,或者错误
return "", "", "", "", false, InvalidTokenErr
}
now := time.Now()
if now.After(exp) { //token过期
return "", "", "", "", false, InvalidTokenErr
}
warn := exp.Sub(now) < WarnTokenExpire
return cid, role, typ, act, warn, nil
}
var accessForbidMsg = gin.H{
"code": stderr.ErrAccessForbid,
"msg": stderr.ErrAccessForbid.Msg(),
}
func Auth(ctx *gin.Context) {
for url, ok := range whitelist {
if ok && strings.HasPrefix(ctx.Request.URL.Path, url) {
return
}
}
//检查ip是否在白名单中
// ipFlag, err := webg.Redis.SISMember(constant.IPWhiteRedisKey, constant.CtxIP)
// if webg.Conf.EnableIPWhite.IsEnable && (err != nil || !ipFlag) {
if webg.Conf.EnableIPWhite.IsEnable {
iPWhite, err := ipwhitemod.FindOneByIp(ctx.ClientIP())
if err != nil || iPWhite.IP == "" {
ctx.AbortWithStatusJSON(http.StatusOK, accessForbidMsg)
return
}
}
var token string
t1 := ctx.Request.Header.Get("Authorization")
t2 := ctx.Query("token") //为兼容m3u8
if t1 != "" {
token = t1
}
if t2 != "" {
token = t2
}
if token == "" {
ctx.AbortWithStatusJSON(http.StatusOK, noTokenMsg)
return
}
cid, role, typ, act, warn, err := auth(token)
if err != nil {
ctx.Writer.Header().Set("Refresh-Authorization", "false")
ctx.AbortWithStatusJSON(http.StatusOK, gin.H{
"code": stderr.ErrAuthInvalid,
"msg": err.Error(),
})
return
}
// 检查管理员账号是否被删除或禁用
adm, err := adminmod.FindOneByName(act)
if err != nil || adm.ID.IsZero() || adm.HasLocked {
RevokeAdminToken(act)
ctx.Writer.Header().Set("Refresh-Authorization", "false")
ctx.AbortWithStatusJSON(http.StatusOK, gin.H{
"code": stderr.ErrAuthInvalid,
"msg": "账号已被禁用或删除",
})
return
}
if warn {
ctx.Writer.Header().Set("Refresh-Authorization", "true")
}
ctx.Set(constant.CtxAdminRole, role)
switch typ {
case District:
ctx.Set(constant.CtxDistrictName, act)
default:
ctx.Set(constant.CtxAdminAct, act)
switch role {
case JuShang:
if cid == "" {
ctx.AbortWithStatusJSON(http.StatusOK, gin.H{
"code": stderr.ErrAuthInvalid,
"msg": err.Error(),
})
}
ctx.Set(constant.CtxJuShangCID, cid)
if !strings.Contains(ctx.Request.URL.Path, "/jushang") && !strings.Contains(ctx.Request.URL.Path, "/api/web/admin/refresh") {
ctx.AbortWithStatusJSON(http.StatusOK,
gin.H{
"code": stderr.ErrAccessForbid,
"msg": "权限出错",
})
}
case NudeChatMerchant:
if cid == "" {
ctx.AbortWithStatusJSON(http.StatusOK, gin.H{
"code": stderr.ErrAuthInvalid,
"msg": err.Error(),
})
}
ctx.Set(constant.CtxNudeChatMerchant, cid)
}
}
}
func tokenAppRedisKey(uid uint64) string {
return redisconst.UserTokenKey(uid)
}
// RevokeTokenCache 吊销redis用户token
func RevokeTokenCache(uids ...uint64) {
keys := make([]string, len(uids))
for i, uid := range uids {
keys[i] = tokenAppRedisKey(uid)
}
_, _ = webg.Redis.Del(keys...)
}
func getWebTokenSecret() string {
return webg.Conf.Base.WebJwtKey
}
// https://tools.ietf.org/html/rfc7519#section-4.1
// See examples for how to use this with your own claim types
type WebClaims struct {
//用户UID
UID uint64 `json:"uid,omitempty"`
//The "exp" (expiration time) claim identifies the expiration time on
//or after which the JWT MUST NOT be accepted for processing. The
//processing of the "exp" claim requires that the current date/time
//MUST be before the expiration date/time listed in the "exp" claim.
//Implementers MAY provide for some small leeway, usually no more than
//a few minutes, to account for clock skew. Its value MUST be a number
//containing a NumericDate value. Use of this claim is OPTIONAL.
ExpiresAt int64 `json:"exp,omitempty"`
//The "iat" (issued at) claim identifies the time at which the JWT was
//issued. This claim can be used to determine the age of the JWT. Its
//value MUST be a number containing a NumericDate value. Use of this
//claim is OPTIONAL.
IssuedAt int64 `json:"iat,omitempty"`
}
func GenWebToken(claims WebClaims) (string, error) {
secret := getWebTokenSecret()
m, _ := common.JSONStruct2Map(claims)
token, err := crypt.CreateToken(secret, m)
if err != nil {
log.Error("GenWebToken error", log.Any("claims", claims), log.E(err))
}
return token, err
}
// ParseWebClaims
// 如果token过期会返回error
func ParseWebClaims(token string) (WebClaims, error) {
secret := getWebTokenSecret()
claims, err := crypt.ParseToken(secret, token)
if err != nil {
return WebClaims{}, err
}
webClaims := WebClaims{}
return webClaims, common.Map2JSONStruct(&webClaims, claims)
}