package authuser import ( "net/http" "strings" "91porn-server/app/appg" "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/usermod" "github.com/gin-gonic/gin" ) var ( // 白名单,不需要验证token的api // 在新的Auth中间件逻辑下,配置了白名单后,用户未登录,context中的uid会被设为0;否则,会绑定用户真实uid。 // 需要白名单中API的handler自己对用户登录状态进行判断。 whitelist = map[string]bool{ "/swagger": true, "/api/app/mine/login": true, "/api/app/im/whiteSign": true, "/api/app/im/sign": true, "/api/app/mine/mobileLoginOnly": true, //"/api/app/ping/domain": true, "/api/app/mine/ver": true, "/api/app/ping/check": true, "/api/app/static/faq/html/index": true, "/api/app/static/faq/tmpl/index": true, //"/api/app/vid/m3u8": true, //前端测试 下载m3u8 添加到白名单 ,生产环境中需要从白名单中剔除 "/api/app/vid/h5/light/m3u8": true, // 现有 H5 轻量播放接口 // H.265 云转码使用 Handler 自己的路径绑定、限时 HMAC 鉴权。 "/api/app/vid/transcode/m3u8": true, "/sources": true, "/defray/callback/shark": true, "/defray/callback/goldfish": true, "/api/app/vid/sec": true, "/api/app/vid/pms/sec": true, "/api/app/vid/pms/mt_sec": true, "/api/app/vid/lsjsec": true, "/api/app/vid/m3u8sec": true, //"/api/app/notification/captcha": true, "/api/app/newactivity": true, "/api/app/sge": true, "/api/app/code/webexchange": true, "/api/app/game/userInfo": true, "/api/app/game/deduct": true, "/api/app/game/code": true, "/api/app/userinvite/callback/recharge": true, "/api/app/statcenter/sync": true, "/api/app/avcomment/info": true, "/api/app/ping/v": true, //"/api/app/recommend": true, // "/api/app/modules": true, // "/api/app/tag/conf/list": true, "/api/app/avcomment/list": true, // 兼容登录、未登录状态的API // "/api/app/vid/module/:subModuleID": true, // "/api/app/vid/section/:sectionID": true, "/api/app/mine/resetpassword": true, "/api/app/mine/resetpassword/mobileverify": true, "/api/app/mine/login/h5": true, //"/api/app/comment/list": true, //"/api/app/mine/info": true, "/api/app/recommend/light/vids": true, "/api/app/store_wallet": true, "/api/app/aimate/sync": true, "/api/app/health/ping": true, } ) func GetTokenSecret() string { return appg.Conf.Base.JwtKey } type Claims struct { UID uint64 `json:"uid"` Timestamp int64 `json:"timestamp"` Type uint8 `json:"type"` } func GenToken(claims *Claims) (string, error) { secret := GetTokenSecret() args, _ := common.JSONStruct2Map(claims) token, err := crypt.CreateToken(secret, args) if err != nil { log.Error("genUserToken error", log.Any("claims", claims), log.E(err)) } return token, err } func ParseToken(token string) (*Claims, error) { secret := GetTokenSecret() claims, err := crypt.ParseToken(secret, token) if err != nil { return nil, err } c := Claims{} return &c, common.Map2JSONStruct(&c, claims) } func tokenRedisKey(uid uint64) string { return redisconst.UserTokenKey(uid) } func cacheToken(uid uint64, token string) { key := tokenRedisKey(uid) _ = appg.Redis.Set(key, token, redisconst.UserTokenExpire) } func RevokeTokenCache(uids ...uint64) { keys := make([]string, len(uids)) for i, uid := range uids { keys[i] = tokenRedisKey(uid) } _, _ = appg.Redis.Del(keys...) } func auth(token string) (uint64, stderr.Code) { claims, err := ParseToken(token) if err != nil { return 0, stderr.InvalidToken } uid := claims.UID redisKey := tokenRedisKey(uid) redisToken, err := appg.Redis.Get(redisKey) if redisToken == nil || err != nil { //redis没有获取到token u, err := usermod.FindUserByUID(uid) if err != nil { log.Warn("[InvalidToken] cause by find user by uid error", log.Any("uid", uid), log.E(err)) return 0, stderr.ErrNetWorkBusy } if u == nil { log.Warn("[InvalidToken] cause by not find user info", log.Any("uid", uid)) return 0, stderr.UserIsNotExists } if u.Token != token { log.Warn("[InvalidToken] cause by current token not equal user's token", log.Any("uid", uid), log.Any("user's token", u.Token), log.Any("current token", token)) return 0, stderr.InvalidToken } if u.HasLocked { return uid, stderr.ErrAccessForbid } cacheToken(uid, u.Token) } else { if *redisToken != token { log.Warn("[InvalidToken] cause by current token not equal redis's token", log.Any("uid", uid), log.Any("current token", token), log.Any("redis token", *redisToken)) return 0, stderr.InvalidToken } } return uid, stderr.Success } func Auth(ctx *gin.Context) { inWhiteList := false for url, ok := range whitelist { if ok && (strings.HasPrefix(ctx.Request.URL.Path, url) || strings.HasPrefix(ctx.FullPath(), url)) { inWhiteList = true break } } var token string var uid uint64 var code stderr.Code t1 := ctx.Request.Header.Get("Authorization") t2 := ctx.Query("token") //为兼容m3u8 if t1 != "" { token = t1 } if t2 != "" { token = t2 } if token == "" && !inWhiteList { // 请求的api未进入白名单且请求未带token,则视为非法请求,直接返回错误 ctx.AbortWithStatusJSON(http.StatusOK, stderr.ErrNoToken.Struct()) return } if token != "" { // 带token说明用户已登录,则解析并校验其token uid, code = auth(token) if code != stderr.Success { // 白名单接口允许客户端携带失效token继续按未登录态访问,例如用户被删除后重新登录。 if inWhiteList { ctx.Set(constant.CtxUserID, 0) return } //兼容用户封禁时调用客服接口 if code == stderr.ErrAccessForbid && ctx.Request.URL.Path == "/api/app/im/newSign" { ctx.Set(constant.CtxUserID, uid) return } handleErrCode(ctx, uid, code) return } ctx.Set(constant.CtxUserID, uid) return } ctx.Set(constant.CtxUserID, 0) } // 处理返回的错误码 func handleErrCode(ctx *gin.Context, uid uint64, code stderr.Code) { var data interface{} switch code { case stderr.ErrAccessForbid: var reason string u, err := usermod.FindUserByUID(uid) if err == nil && u != nil { reason = u.LockReason } data = gin.H{"uid": uid, "reason": reason} } common.ServeJSON(ctx, code, data) ctx.Abort() } func getWebTokenSecret() string { return appg.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) }