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) }