351 lines
10 KiB
Go
351 lines
10 KiB
Go
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)
|
|
}
|