diff --git a/server/api/v1/pet/user/enter.go b/server/api/v1/pet/user/enter.go new file mode 100644 index 00000000..b801d9c4 --- /dev/null +++ b/server/api/v1/pet/user/enter.go @@ -0,0 +1,19 @@ +package user + +import "github.com/flipped-aurora/gin-vue-admin/server/service" + +type ApiGroup struct { +} + +var ( + petAdoptionApplicationsService = service.ServiceGroupApp.PetServiceGroup.PetAdoptionApplicationsService + petAdoptionPostsService = service.ServiceGroupApp.PetServiceGroup.PetAdoptionPostsService + petAiAssistantConversationsService = service.ServiceGroupApp.PetServiceGroup.PetAiAssistantConversationsService + petAiConversationsService = service.ServiceGroupApp.PetServiceGroup.PetAiConversationsService + petFamiliesService = service.ServiceGroupApp.PetServiceGroup.PetFamiliesService + petFamilyInvitationsService = service.ServiceGroupApp.PetServiceGroup.PetFamilyInvitationsService + petFamilyMembersService = service.ServiceGroupApp.PetServiceGroup.PetFamilyMembersService + petFamilyPetsService = service.ServiceGroupApp.PetServiceGroup.PetFamilyPetsService + petPetsService = service.ServiceGroupApp.PetServiceGroup.PetPetsService + petRecordsService = service.ServiceGroupApp.PetServiceGroup.PetRecordsService +) diff --git a/server/initialize/router.go b/server/initialize/router.go index 11ebf420..4f447e5a 100644 --- a/server/initialize/router.go +++ b/server/initialize/router.go @@ -73,8 +73,10 @@ func Routers() *gin.Engine { PublicGroup := Router.Group(global.GVA_CONFIG.System.RouterPrefix) PrivateGroup := Router.Group(global.GVA_CONFIG.System.RouterPrefix) + UserGroup := Router.Group(global.GVA_CONFIG.System.RouterPrefix) PrivateGroup.Use(middleware.JWTAuth()).Use(middleware.CasbinHandler()) + UserGroup.Use(middleware.UserJWTAuth()) { // 健康监测 diff --git a/server/middleware/user_jwt.go b/server/middleware/user_jwt.go new file mode 100644 index 00000000..9f6b50ea --- /dev/null +++ b/server/middleware/user_jwt.go @@ -0,0 +1,89 @@ +package middleware + +import ( + "errors" + "strconv" + "time" + + "github.com/flipped-aurora/gin-vue-admin/server/global" + "github.com/flipped-aurora/gin-vue-admin/server/utils" + "github.com/golang-jwt/jwt/v5" + + "github.com/flipped-aurora/gin-vue-admin/server/model/common/response" + "github.com/gin-gonic/gin" +) + +func UserJWTAuth() gin.HandlerFunc { + return func(c *gin.Context) { + // 我们这里jwt鉴权取头部信息 user-token 登录时回返回token信息 这里前端需要把token存储到cookie或者本地localStorage中 不过需要跟后端协商过期时间 可以约定刷新令牌或者重新登录 + token := utils.GetUserToken(c) + if token == "" { + response.NoAuth("未登录或非法访问,请登录", c) + c.Abort() + return + } + if appUserIsBlacklist(token) { + response.NoAuth("您的帐户异地登陆或令牌失效", c) + utils.ClearUserToken(c) + c.Abort() + return + } + j := utils.NewJWT() + // parseToken 解析token包含的信息 + claims, err := j.ParseAppUserToken(token) + if err != nil { + if errors.Is(err, utils.TokenExpired) { + response.NoAuth("登录已过期,请重新登录", c) + utils.ClearUserToken(c) + c.Abort() + return + } + response.NoAuth(err.Error(), c) + utils.ClearUserToken(c) + c.Abort() + return + } + + // 已登录用户被管理员禁用 需要使该用户的jwt失效 此处比较消耗性能 如果需要 请自行打开 + // 用户被删除的逻辑 需要优化 此处比较消耗性能 如果需要 请自行打开 + + //if user, err := userService.FindUserByUuid(claims.UUID.String()); err != nil || user.Enable == 2 { + // _ = jwtService.JsonInBlacklist(system.JwtBlacklist{Jwt: token}) + // response.FailWithDetailed(gin.H{"reload": true}, err.Error(), c) + // c.Abort() + //} + c.Set("user_claims", claims) + if claims.ExpiresAt.Unix()-time.Now().Unix() < claims.BufferTime { + dr, _ := utils.ParseDuration(global.GVA_CONFIG.JWT.ExpiresTime) + claims.ExpiresAt = jwt.NewNumericDate(time.Now().Add(dr)) + newToken, _ := j.CreateAppUserTokenByOldToken(token, *claims) + newClaims, _ := j.ParseAppUserToken(newToken) + c.Header("new-token", newToken) + c.Header("new-expires-at", strconv.FormatInt(newClaims.ExpiresAt.Unix(), 10)) + utils.SetUserToken(c, newToken, int(dr.Seconds())) + if global.GVA_CONFIG.System.UseMultipoint { + // 记录新的活跃jwt + _ = utils.SetRedisJWT(newToken, newClaims.AppBaseClaims.OpenID) + } + } + c.Next() + + if newToken, exists := c.Get("new-token"); exists { + c.Header("new-token", newToken.(string)) + } + if newExpiresAt, exists := c.Get("new-expires-at"); exists { + c.Header("new-expires-at", newExpiresAt.(string)) + } + } +} + +//@author: [piexlmax](https://github.com/piexlmax) +//@function: appUserIsBlacklist +//@description: 判断用户端JWT是否在黑名单内部 +//@param: jwt string +//@return: bool + +func appUserIsBlacklist(jwt string) bool { + _, ok := global.BlackCache.Get("user_blacklist:" + jwt) + return ok +} diff --git a/server/model/system/request/app_jwt.go b/server/model/system/request/app_jwt.go new file mode 100644 index 00000000..fd3201b9 --- /dev/null +++ b/server/model/system/request/app_jwt.go @@ -0,0 +1,28 @@ +package request + +import ( + jwt "github.com/golang-jwt/jwt/v5" + "github.com/google/uuid" +) + +// AppUserClaims 小程序用户专用Claims结构 +type AppUserClaims struct { + AppBaseClaims + BufferTime int64 + jwt.RegisteredClaims +} + +// AppBaseClaims 小程序用户基础信息 +type AppBaseClaims struct { + UUID uuid.UUID `json:"uuid"` // 用户UUID + ID uint `json:"id"` // 用户ID + OpenID string `json:"openId"` // 微信openid + UnionID string `json:"unionId"` // 微信unionid(可选) + NickName string `json:"nickName"` // 用户昵称 + Avatar string `json:"avatar"` // 用户头像 + Phone string `json:"phone"` // 手机号(可选) + Gender int `json:"gender"` // 性别:0-未知,1-男,2-女 + City string `json:"city"` // 城市 + Province string `json:"province"` // 省份 + Country string `json:"country"` // 国家 +} diff --git a/server/utils/claims.go b/server/utils/claims.go index 2a6308e8..0744085c 100644 --- a/server/utils/claims.go +++ b/server/utils/claims.go @@ -39,6 +39,49 @@ func SetToken(c *gin.Context, token string, maxAge int) { } } +func ClearUserToken(c *gin.Context) { + // 增加cookie user-token 向来源的web添加 + host, _, err := net.SplitHostPort(c.Request.Host) + if err != nil { + host = c.Request.Host + } + + if net.ParseIP(host) != nil { + c.SetCookie("user-token", "", -1, "/", "", false, false) + } else { + c.SetCookie("user-token", "", -1, "/", host, false, false) + } +} + +func SetUserToken(c *gin.Context, token string, maxAge int) { + // 增加cookie user-token 向来源的web添加 + host, _, err := net.SplitHostPort(c.Request.Host) + if err != nil { + host = c.Request.Host + } + + if net.ParseIP(host) != nil { + c.SetCookie("user-token", token, maxAge, "/", "", false, false) + } else { + c.SetCookie("user-token", token, maxAge, "/", host, false, false) + } +} + +func GetUserToken(c *gin.Context) string { + token := c.Request.Header.Get("user-token") + if token == "" { + j := NewJWT() + token, _ = c.Cookie("user-token") + claims, err := j.ParseToken(token) + if err != nil { + global.GVA_LOG.Error("重新写入cookie token失败,未能成功解析token,请检查请求头是否存在user-token且claims是否为规定结构") + return token + } + SetUserToken(c, token, int((claims.ExpiresAt.Unix()-time.Now().Unix())/60)) + } + return token +} + func GetToken(c *gin.Context) string { token := c.Request.Header.Get("x-token") if token == "" { @@ -146,3 +189,134 @@ func LoginToken(user system.Login) (token string, claims systemReq.CustomClaims, token, err = j.CreateToken(claims) return } + +// AppUserLogin 小程序用户登录接口 +type AppUserLogin interface { + GetUUID() uuid.UUID + GetUserId() uint + GetOpenID() string + GetUnionID() string + GetNickname() string + GetAvatar() string + GetPhone() string + GetGender() int + GetCity() string + GetProvince() string + GetCountry() string +} + +// AppUserLoginToken 创建小程序用户登录token +func AppUserLoginToken(user AppUserLogin) (token string, claims systemReq.AppUserClaims, err error) { + j := NewJWT() + claims = j.CreateAppUserClaims(systemReq.AppBaseClaims{ + UUID: user.GetUUID(), + ID: user.GetUserId(), + OpenID: user.GetOpenID(), + UnionID: user.GetUnionID(), + NickName: user.GetNickname(), + Avatar: user.GetAvatar(), + Phone: user.GetPhone(), + Gender: user.GetGender(), + City: user.GetCity(), + Province: user.GetProvince(), + Country: user.GetCountry(), + }) + token, err = j.CreateAppUserToken(claims) + return +} + +// 用户端专用Claims获取函数 + +func GetAppUserClaims(c *gin.Context) (*systemReq.AppUserClaims, error) { + token := GetUserToken(c) + j := NewJWT() + claims, err := j.ParseAppUserToken(token) + if err != nil { + global.GVA_LOG.Error("从Gin的Context中获取从jwt解析信息失败, 请检查请求头是否存在user-token且claims是否为规定结构") + } + return claims, err +} + +// GetAppUserID 从Gin的Context中获取从jwt解析出来的用户ID +func GetAppUserID(c *gin.Context) uint { + if claims, exists := c.Get("user_claims"); !exists { + if cl, err := GetAppUserClaims(c); err != nil { + return 0 + } else { + return cl.AppBaseClaims.ID + } + } else { + waitUse := claims.(*systemReq.AppUserClaims) + return waitUse.AppBaseClaims.ID + } +} + +// GetAppUserUuid 从Gin的Context中获取从jwt解析出来的用户UUID +func GetAppUserUuid(c *gin.Context) uuid.UUID { + if claims, exists := c.Get("user_claims"); !exists { + if cl, err := GetAppUserClaims(c); err != nil { + return uuid.UUID{} + } else { + return cl.AppBaseClaims.UUID + } + } else { + waitUse := claims.(*systemReq.AppUserClaims) + return waitUse.AppBaseClaims.UUID + } +} + +// GetAppUserInfo 从Gin的Context中获取从jwt解析出来的用户信息 +func GetAppUserInfo(c *gin.Context) *systemReq.AppUserClaims { + if claims, exists := c.Get("user_claims"); !exists { + if cl, err := GetAppUserClaims(c); err != nil { + return nil + } else { + return cl + } + } else { + waitUse := claims.(*systemReq.AppUserClaims) + return waitUse + } +} + +// GetAppUserOpenID 从Gin的Context中获取从jwt解析出来的微信OpenID +func GetAppUserOpenID(c *gin.Context) string { + if claims, exists := c.Get("user_claims"); !exists { + if cl, err := GetAppUserClaims(c); err != nil { + return "" + } else { + return cl.AppBaseClaims.OpenID + } + } else { + waitUse := claims.(*systemReq.AppUserClaims) + return waitUse.AppBaseClaims.OpenID + } +} + +// GetAppUserNickName 从Gin的Context中获取从jwt解析出来的用户昵称 +func GetAppUserNickName(c *gin.Context) string { + if claims, exists := c.Get("user_claims"); !exists { + if cl, err := GetAppUserClaims(c); err != nil { + return "" + } else { + return cl.AppBaseClaims.NickName + } + } else { + waitUse := claims.(*systemReq.AppUserClaims) + return waitUse.AppBaseClaims.NickName + } +} + +// GetAppUserAvatar 从Gin的Context中获取从jwt解析出来的用户头像 +func GetAppUserAvatar(c *gin.Context) string { + if claims, exists := c.Get("user_claims"); !exists { + if cl, err := GetAppUserClaims(c); err != nil { + return "" + } else { + return cl.AppBaseClaims.Avatar + } + } else { + waitUse := claims.(*systemReq.AppUserClaims) + return waitUse.AppBaseClaims.Avatar + } +} diff --git a/server/utils/jwt.go b/server/utils/jwt.go index b4e6b3b2..5b3cccab 100644 --- a/server/utils/jwt.go +++ b/server/utils/jwt.go @@ -103,3 +103,64 @@ func SetRedisJWT(jwt string, userName string) (err error) { err = global.GVA_REDIS.Set(context.Background(), userName, jwt, timer).Err() return err } + +// 用户端专用JWT函数 + +// CreateAppUserClaims 创建小程序用户Claims +func (j *JWT) CreateAppUserClaims(baseClaims request.AppBaseClaims) request.AppUserClaims { + bf, _ := ParseDuration(global.GVA_CONFIG.JWT.BufferTime) + ep, _ := ParseDuration(global.GVA_CONFIG.JWT.ExpiresTime) + claims := request.AppUserClaims{ + AppBaseClaims: baseClaims, + BufferTime: int64(bf / time.Second), // 缓冲时间1天 缓冲时间内会获得新的token刷新令牌 + RegisteredClaims: jwt.RegisteredClaims{ + Audience: jwt.ClaimStrings{"GVA-APP"}, // 受众 + NotBefore: jwt.NewNumericDate(time.Now().Add(-1000)), // 签名生效时间 + ExpiresAt: jwt.NewNumericDate(time.Now().Add(ep)), // 过期时间 7天 配置文件 + Issuer: global.GVA_CONFIG.JWT.Issuer, // 签名的发行者 + }, + } + return claims +} + +// CreateAppUserToken 创建小程序用户token +func (j *JWT) CreateAppUserToken(claims request.AppUserClaims) (string, error) { + token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) + return token.SignedString(j.SigningKey) +} + +// CreateAppUserTokenByOldToken 旧token换新token(小程序用户专用) +func (j *JWT) CreateAppUserTokenByOldToken(oldToken string, claims request.AppUserClaims) (string, error) { + v, err, _ := global.GVA_Concurrency_Control.Do("APP-JWT:"+oldToken, func() (interface{}, error) { + return j.CreateAppUserToken(claims) + }) + return v.(string), err +} + +// ParseAppUserToken 解析小程序用户token +func (j *JWT) ParseAppUserToken(tokenString string) (*request.AppUserClaims, error) { + token, err := jwt.ParseWithClaims(tokenString, &request.AppUserClaims{}, func(token *jwt.Token) (i interface{}, e error) { + return j.SigningKey, nil + }) + + if err != nil { + switch { + case errors.Is(err, jwt.ErrTokenExpired): + return nil, TokenExpired + case errors.Is(err, jwt.ErrTokenMalformed): + return nil, TokenMalformed + case errors.Is(err, jwt.ErrTokenSignatureInvalid): + return nil, TokenSignatureInvalid + case errors.Is(err, jwt.ErrTokenNotValidYet): + return nil, TokenNotValidYet + default: + return nil, TokenInvalid + } + } + if token != nil { + if claims, ok := token.Claims.(*request.AppUserClaims); ok && token.Valid { + return claims, nil + } + } + return nil, TokenValid +}