@@ -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)
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
package checkPermission
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"91porn-server/common"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/storyicon/grbac"
|
||||
)
|
||||
|
||||
// 白名单,不需要验证token的api
|
||||
var whitelist = map[string]bool{
|
||||
"/swagger": true,
|
||||
}
|
||||
|
||||
func QueryRolesByHeaders(c *gin.Context) (roles []string, err error) {
|
||||
role, _ := common.GetAdminRole(c)
|
||||
roles = append(roles, role)
|
||||
return roles, err
|
||||
}
|
||||
|
||||
var rbac *grbac.Controller
|
||||
|
||||
func init() {
|
||||
var err error
|
||||
rbac, err = grbac.New(grbac.WithJSON("config/rules.json", 10*time.Minute))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
func CheckPermission(c *gin.Context) {
|
||||
for url, ok := range whitelist {
|
||||
if ok && strings.HasPrefix(c.Request.URL.Path, url) {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
roles, _ := QueryRolesByHeaders(c)
|
||||
state, _ := rbac.IsRequestGranted(c.Request, roles)
|
||||
if !state.IsGranted() {
|
||||
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{
|
||||
"code": http.StatusUnauthorized,
|
||||
"msg": "没有操作权限",
|
||||
})
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package limitHandler
|
||||
|
||||
import (
|
||||
"91porn-server/common"
|
||||
"91porn-server/common/stderr"
|
||||
"91porn-server/web/webg"
|
||||
"fmt"
|
||||
"github.com/gin-gonic/gin"
|
||||
"time"
|
||||
)
|
||||
|
||||
// FilterRequestByExport 限制用户短时间内的导出请求次数,limit决定限制时间
|
||||
func FilterRequestByExport() gin.HandlerFunc {
|
||||
return func(ctx *gin.Context) {
|
||||
manager, err := common.GetAdminAct(ctx)
|
||||
if err != nil {
|
||||
common.ServeJSON(ctx, stderr.AdminIDErr, err.Error())
|
||||
ctx.Abort()
|
||||
return
|
||||
}
|
||||
key := fmt.Sprintf("export_excel:%v", manager)
|
||||
if webg.Redis.IsExist(key) {
|
||||
common.ServeJSON(ctx, stderr.VisitLimit, err)
|
||||
ctx.Abort()
|
||||
return
|
||||
}
|
||||
go func() { _ = webg.Redis.Set(key, "-", time.Second*3) }()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
package limitHandler
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"91porn-server/common"
|
||||
"91porn-server/common/constant"
|
||||
"91porn-server/common/httputil"
|
||||
"91porn-server/common/stderr"
|
||||
"91porn-server/web/webg"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/patrickmn/go-cache"
|
||||
)
|
||||
|
||||
var limitURI = []string{
|
||||
"/api/web/admin/login",
|
||||
"/api/web/admin/slogin",
|
||||
"/api/web/admin/verify",
|
||||
}
|
||||
var (
|
||||
tgBotToken = "2013237388:AAFwvKvEmQfEvcI7wt_64Zkp9KPfgHo8sow"
|
||||
tgGroupId = "-1003735177636"
|
||||
)
|
||||
var (
|
||||
limitDuration = 3 * time.Minute
|
||||
limitCount = 5
|
||||
)
|
||||
|
||||
var memCache = cache.New(1*time.Minute, 10*time.Minute)
|
||||
|
||||
// LoginLimit 登陆频率限制
|
||||
func LoginLimit(ctx *gin.Context) {
|
||||
path := ctx.Request.URL.Path
|
||||
// 检查是否需要限制频率
|
||||
check := false
|
||||
for _, p := range limitURI {
|
||||
if strings.HasPrefix(path, p) {
|
||||
check = true
|
||||
}
|
||||
}
|
||||
if !check {
|
||||
return
|
||||
}
|
||||
// 测试环境不限制
|
||||
if webg.Conf.Base.Env != constant.ProdEnv {
|
||||
return
|
||||
}
|
||||
// 获取KEY:只按 path + 管理员账号(name),不含 IP
|
||||
// (对方会伪造 IP,若 key 含 IP 则每次换 IP 就重置计数,导致限流被绕过一直重试)
|
||||
ip := ctx.ClientIP() // ip 仅用于超限预警展示,不参与限流 key
|
||||
key := fmt.Sprintf("web-limit:%s", path)
|
||||
// 检查参数值
|
||||
var params map[string]interface{}
|
||||
if ctx.Request.Method == http.MethodPost {
|
||||
raw, _ := ctx.GetRawData()
|
||||
ctx.Request.Body = io.NopCloser(bytes.NewBuffer(raw))
|
||||
if err := json.Unmarshal(raw, ¶ms); err == nil {
|
||||
if a, ok := params["name"]; ok { // 做登陆频率限制只检查管理员账号
|
||||
key += ":" + a.(string)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 检查频率
|
||||
aCount, ok := memCache.Get(key)
|
||||
if !ok {
|
||||
memCache.Set(key, 1, limitDuration)
|
||||
return
|
||||
}
|
||||
count, ok2 := aCount.(int)
|
||||
if !ok2 {
|
||||
memCache.Set(key, 1, limitDuration)
|
||||
return
|
||||
}
|
||||
count++
|
||||
defer func() { memCache.Set(key, count, limitDuration) }()
|
||||
if count > limitCount {
|
||||
// 超出限制
|
||||
go func() {
|
||||
// 预警
|
||||
message := fmt.Sprintf("[91Porn]\n登陆请求频率过高!\n时间: %s\n请求地址: %s\n请求参数: %+v\nIP: %s\n错误次数: %d", time.Now().Format("2006-01-02 15:04:05"), path, params, ip, count)
|
||||
// 请求地址/参数为不可信输入,去掉非法 UTF-8 字节;不用 parse_mode,避免 Markdown 把内容解析成实体后报错
|
||||
message = strings.ToValidUTF8(message, "")
|
||||
url := fmt.Sprintf("https://api.telegram.org/bot%s/sendMessage", tgBotToken)
|
||||
req := map[string]interface{}{
|
||||
"chat_id": tgGroupId,
|
||||
"text": message,
|
||||
}
|
||||
resp, _ := httputil.DefaultClientPostJson(url, nil, req)
|
||||
_ = resp.Body.Close()
|
||||
}()
|
||||
ctx.Abort()
|
||||
common.ServeJSON(ctx, stderr.ErrReqForbidden, nil)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Next()
|
||||
}
|
||||
Reference in New Issue
Block a user