feat: 新增访客拦截器
This commit is contained in:
173
utils/jwt.go
Normal file
173
utils/jwt.go
Normal file
@@ -0,0 +1,173 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
)
|
||||
|
||||
var (
|
||||
// ErrTokenInvalid token 无效
|
||||
ErrTokenInvalid = errors.New("token 无效")
|
||||
// ErrTokenExpired token 已过期
|
||||
ErrTokenExpired = errors.New("token 已过期")
|
||||
// ErrTokenNotValidYet token 尚未生效
|
||||
ErrTokenNotValidYet = errors.New("token 尚未生效")
|
||||
// ErrTokenMalformed token 格式错误
|
||||
ErrTokenMalformed = errors.New("token 格式错误")
|
||||
// ErrTokenSignatureInvalid 签名无效
|
||||
ErrTokenSignatureInvalid = errors.New("签名无效")
|
||||
)
|
||||
|
||||
// JWTConfig JWT 配置
|
||||
type JWTConfig struct {
|
||||
SecretKey string // 密钥
|
||||
ExpiresTime time.Duration // 过期时间
|
||||
Issuer string // 签发者
|
||||
}
|
||||
|
||||
// JWT jwt 工具
|
||||
type JWT struct {
|
||||
config JWTConfig
|
||||
}
|
||||
|
||||
// CustomClaims 自定义 claims
|
||||
type CustomClaims struct {
|
||||
UserID string `json:"user_id"`
|
||||
UserName string `json:"user_name"`
|
||||
Data map[string]interface{} `json:"data,omitempty"`
|
||||
jwt.RegisteredClaims
|
||||
}
|
||||
|
||||
// NewJWT 创建 JWT 实例
|
||||
func NewJWT(config JWTConfig) *JWT {
|
||||
return &JWT{config: config}
|
||||
}
|
||||
|
||||
// GenerateToken 生成 token
|
||||
func (j *JWT) GenerateToken(claims *CustomClaims) (string, error) {
|
||||
// 设置默认值
|
||||
if claims.Issuer == "" {
|
||||
claims.Issuer = j.config.Issuer
|
||||
}
|
||||
if claims.ExpiresAt == nil && j.config.ExpiresTime > 0 {
|
||||
claims.ExpiresAt = jwt.NewNumericDate(time.Now().Add(j.config.ExpiresTime))
|
||||
}
|
||||
if claims.IssuedAt == nil {
|
||||
claims.IssuedAt = jwt.NewNumericDate(time.Now())
|
||||
}
|
||||
if claims.NotBefore == nil {
|
||||
claims.NotBefore = jwt.NewNumericDate(time.Now())
|
||||
}
|
||||
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
||||
return token.SignedString([]byte(j.config.SecretKey))
|
||||
}
|
||||
|
||||
// ParseToken 解析 token
|
||||
func (j *JWT) ParseToken(tokenString string) (*CustomClaims, error) {
|
||||
token, err := jwt.ParseWithClaims(tokenString, &CustomClaims{}, func(token *jwt.Token) (interface{}, error) {
|
||||
return []byte(j.config.SecretKey), nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
switch {
|
||||
case errors.Is(err, jwt.ErrTokenMalformed):
|
||||
return nil, ErrTokenMalformed
|
||||
case errors.Is(err, jwt.ErrTokenExpired):
|
||||
return nil, ErrTokenExpired
|
||||
case errors.Is(err, jwt.ErrTokenNotValidYet):
|
||||
return nil, ErrTokenNotValidYet
|
||||
case errors.Is(err, jwt.ErrTokenSignatureInvalid):
|
||||
return nil, ErrTokenSignatureInvalid
|
||||
default:
|
||||
return nil, ErrTokenInvalid
|
||||
}
|
||||
}
|
||||
|
||||
if claims, ok := token.Claims.(*CustomClaims); ok && token.Valid {
|
||||
return claims, nil
|
||||
}
|
||||
|
||||
return nil, ErrTokenInvalid
|
||||
}
|
||||
|
||||
// RefreshToken 刷新 token
|
||||
func (j *JWT) RefreshToken(tokenString string) (string, error) {
|
||||
claims, err := j.ParseToken(tokenString)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// 重置过期时间
|
||||
claims.ExpiresAt = jwt.NewNumericDate(time.Now().Add(j.config.ExpiresTime))
|
||||
claims.IssuedAt = jwt.NewNumericDate(time.Now())
|
||||
|
||||
return j.GenerateToken(claims)
|
||||
}
|
||||
|
||||
// GetUserIDFromToken 从 token 中获取用户 ID
|
||||
func (j *JWT) GetUserIDFromToken(tokenString string) (string, error) {
|
||||
claims, err := j.ParseToken(tokenString)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return claims.UserID, nil
|
||||
}
|
||||
|
||||
// GetUserNameFromToken 从 token 中获取用户名
|
||||
func (j *JWT) GetUserNameFromToken(tokenString string) (string, error) {
|
||||
claims, err := j.ParseToken(tokenString)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return claims.UserName, nil
|
||||
}
|
||||
|
||||
// GetClaimFromToken 从 token 中获取指定字段
|
||||
func (j *JWT) GetClaimFromToken(tokenString string, key string) (interface{}, error) {
|
||||
claims, err := j.ParseToken(tokenString)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if claims.Data == nil {
|
||||
return nil, errors.New("未找到指定字段")
|
||||
}
|
||||
val, ok := claims.Data[key]
|
||||
if !ok {
|
||||
return nil, errors.New("未找到指定字段")
|
||||
}
|
||||
return val, nil
|
||||
}
|
||||
|
||||
// ValidateToken 验证 token 是否有效
|
||||
func (j *JWT) ValidateToken(tokenString string) bool {
|
||||
_, err := j.ParseToken(tokenString)
|
||||
return err == nil
|
||||
}
|
||||
|
||||
// CreateToken 快速创建 token(简化方法)
|
||||
// secretKey: 密钥
|
||||
// userID: 用户 ID
|
||||
// userName: 用户名
|
||||
// expires: 过期时间(小时)
|
||||
func CreateToken(secretKey, userID, userName string, expires int) (string, error) {
|
||||
jwtUtil := NewJWT(JWTConfig{
|
||||
SecretKey: secretKey,
|
||||
ExpiresTime: time.Duration(expires) * time.Hour,
|
||||
})
|
||||
|
||||
claims := &CustomClaims{
|
||||
UserID: userID,
|
||||
UserName: userName,
|
||||
}
|
||||
|
||||
return jwtUtil.GenerateToken(claims)
|
||||
}
|
||||
|
||||
// ParseTokenString 快速解析 token(简化方法)
|
||||
func ParseTokenString(secretKey, tokenString string) (*CustomClaims, error) {
|
||||
jwtUtil := NewJWT(JWTConfig{SecretKey: secretKey})
|
||||
return jwtUtil.ParseToken(tokenString)
|
||||
}
|
||||
101
utils/location.go
Normal file
101
utils/location.go
Normal file
@@ -0,0 +1,101 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"math/rand"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
// LocationResponse represents the response from IP location API
|
||||
type LocationResponse struct {
|
||||
IP string `json:"ip"`
|
||||
Pro string `json:"pro"`
|
||||
ProCode string `json:"proCode"`
|
||||
City string `json:"city"`
|
||||
CityCode string `json:"cityCode"`
|
||||
Region string `json:"region"`
|
||||
RegionCode string `json:"regionCode"`
|
||||
Addr string `json:"addr"`
|
||||
RegionNames string `json:"regionNames"`
|
||||
Err string `json:"err"`
|
||||
}
|
||||
|
||||
// userAgents is a list of common browser user agents for random selection
|
||||
var userAgents = []string{
|
||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
|
||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36",
|
||||
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
|
||||
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Safari/605.1.15",
|
||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:121.0) Gecko/20100101 Firefox/121.0",
|
||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36 Edg/120.0.0.0",
|
||||
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
|
||||
"Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Mobile/15E148 Safari/604.1",
|
||||
"Mozilla/5.0 (iPad; CPU OS 17_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Mobile/15E148 Safari/604.1",
|
||||
"Mozilla/5.0 (Linux; Android 14; SM-S918B) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Mobile Safari/537.36",
|
||||
}
|
||||
|
||||
// randomUserAgent returns a random user agent string
|
||||
func randomUserAgent() string {
|
||||
r := rand.New(rand.NewSource(time.Now().UnixNano()))
|
||||
return userAgents[r.Intn(len(userAgents))]
|
||||
}
|
||||
|
||||
// GetLocation 获取IP的地理位置信息
|
||||
// 调用太平洋网络IP地址查询API,返回省份和城市
|
||||
func GetLocation(ctx context.Context, ip string) (province string, city string, err error) {
|
||||
url := fmt.Sprintf("https://whois.pconline.com.cn/ipJson.jsp?ip=%s&json=true", ip)
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
|
||||
if err != nil {
|
||||
return "", "", fmt.Errorf("create request failed: %w", err)
|
||||
}
|
||||
|
||||
// Set random user agent to simulate browser
|
||||
req.Header.Set("User-Agent", randomUserAgent())
|
||||
req.Header.Set("Accept", "application/json, text/plain, */*")
|
||||
req.Header.Set("Accept-Language", "zh-CN,zh;q=0.9,en;q=0.8")
|
||||
req.Header.Set("Referer", "https://whois.pconline.com.cn/")
|
||||
|
||||
client := &http.Client{
|
||||
Timeout: 5 * time.Second,
|
||||
}
|
||||
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return "", "", fmt.Errorf("request failed: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return "", "", fmt.Errorf("request failed with status: %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return "", "", fmt.Errorf("read response failed: %w", err)
|
||||
}
|
||||
|
||||
var location LocationResponse
|
||||
if err := json.Unmarshal(body, &location); err != nil {
|
||||
return "", "", fmt.Errorf("parse response failed: %w", err)
|
||||
}
|
||||
|
||||
// Check for error in response
|
||||
if location.Err != "" {
|
||||
return "", "", fmt.Errorf("API error: %s", location.Err)
|
||||
}
|
||||
|
||||
return location.Pro, location.City, nil
|
||||
}
|
||||
|
||||
// GetLocationWithCache 获取IP的地理位置信息(带缓存)
|
||||
// 如果缓存中有则直接返回,否则调用API并缓存结果
|
||||
func GetLocationWithCache(ctx context.Context, ip string, cache func(key string, fetch func() (string, string, error)) (string, string, error)) (province string, city string, err error) {
|
||||
return cache(ip, func() (string, string, error) {
|
||||
return GetLocation(ctx, ip)
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user