add:
This commit is contained in:
@@ -5,8 +5,14 @@ import (
|
||||
"github.com/zeromicro/go-zero/zrpc"
|
||||
)
|
||||
|
||||
type JwtConfig struct {
|
||||
SecretKey string `json:"secretKey"`
|
||||
Issuer string `json:"issuer"`
|
||||
}
|
||||
|
||||
type Config struct {
|
||||
zrpc.RpcServerConf
|
||||
DataSource string `json:"dataSource"`
|
||||
CacheConf cache.CacheConf
|
||||
Jwt JwtConfig `json:"jwt"`
|
||||
}
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
package logic
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"juwan-backend/app/users/rpc/internal/svc"
|
||||
"juwan-backend/app/users/rpc/pb"
|
||||
|
||||
"github.com/zeromicro/go-zero/core/logx"
|
||||
)
|
||||
|
||||
type CheckPermissionLogic struct {
|
||||
ctx context.Context
|
||||
svcCtx *svc.ServiceContext
|
||||
logx.Logger
|
||||
}
|
||||
|
||||
func NewCheckPermissionLogic(ctx context.Context, svcCtx *svc.ServiceContext) *CheckPermissionLogic {
|
||||
return &CheckPermissionLogic{
|
||||
ctx: ctx,
|
||||
svcCtx: svcCtx,
|
||||
Logger: logx.WithContext(ctx),
|
||||
}
|
||||
}
|
||||
|
||||
func (l *CheckPermissionLogic) CheckPermission(in *pb.CheckPermissionReq) (*pb.CheckPermissionResp, error) {
|
||||
// todo: add your logic here and delete this line
|
||||
|
||||
return &pb.CheckPermissionResp{}, nil
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
|
||||
"juwan-backend/app/users/rpc/internal/svc"
|
||||
"juwan-backend/app/users/rpc/pb"
|
||||
"juwan-backend/common/converter"
|
||||
|
||||
"github.com/zeromicro/go-zero/core/logx"
|
||||
)
|
||||
@@ -23,8 +24,19 @@ func NewGetUserByUsernameLogic(ctx context.Context, svcCtx *svc.ServiceContext)
|
||||
}
|
||||
}
|
||||
|
||||
func (l *GetUserByUsernameLogic) GetUserByUsername(in *pb.GetUsersByIdReq) (*pb.GetUsersByIdResp, error) {
|
||||
func (l *GetUserByUsernameLogic) GetUserByUsername(in *pb.GetUserByUsernameReq) (*pb.GetUserByUsernameResp, error) {
|
||||
// todo: add your logic here and delete this line
|
||||
|
||||
return &pb.GetUsersByIdResp{}, nil
|
||||
user, err := l.svcCtx.UsersModel.FindOneByUsername(l.ctx, in.Username)
|
||||
pbUsers := &pb.Users{}
|
||||
converter.StructToStruct(user, pbUsers)
|
||||
if err == nil || user != nil {
|
||||
return &pb.GetUserByUsernameResp{
|
||||
Users: pbUsers,
|
||||
}, nil
|
||||
}
|
||||
if err.Error() != "not found" {
|
||||
return nil, err
|
||||
}
|
||||
return &pb.GetUserByUsernameResp{}, nil
|
||||
}
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
package logic
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"juwan-backend/app/users/rpc/internal/svc"
|
||||
"juwan-backend/app/users/rpc/pb"
|
||||
|
||||
"github.com/zeromicro/go-zero/core/logx"
|
||||
)
|
||||
|
||||
type LoginLogic struct {
|
||||
ctx context.Context
|
||||
svcCtx *svc.ServiceContext
|
||||
logx.Logger
|
||||
}
|
||||
|
||||
func NewLoginLogic(ctx context.Context, svcCtx *svc.ServiceContext) *LoginLogic {
|
||||
return &LoginLogic{
|
||||
ctx: ctx,
|
||||
svcCtx: svcCtx,
|
||||
Logger: logx.WithContext(ctx),
|
||||
}
|
||||
}
|
||||
|
||||
func (l *LoginLogic) Login(in *pb.LoginReq) (*pb.LoginResp, error) {
|
||||
// todo: add your logic here and delete this line
|
||||
|
||||
return &pb.LoginResp{}, nil
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package logic
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"juwan-backend/app/users/rpc/internal/svc"
|
||||
"juwan-backend/app/users/rpc/pb"
|
||||
|
||||
"github.com/zeromicro/go-zero/core/logx"
|
||||
)
|
||||
|
||||
type ValidateTokenLogic struct {
|
||||
ctx context.Context
|
||||
svcCtx *svc.ServiceContext
|
||||
logx.Logger
|
||||
}
|
||||
|
||||
func NewValidateTokenLogic(ctx context.Context, svcCtx *svc.ServiceContext) *ValidateTokenLogic {
|
||||
return &ValidateTokenLogic{
|
||||
ctx: ctx,
|
||||
svcCtx: svcCtx,
|
||||
Logger: logx.WithContext(ctx),
|
||||
}
|
||||
}
|
||||
|
||||
func (l *ValidateTokenLogic) ValidateToken(in *pb.ValidateTokenReq) (*pb.ValidateTokenResp, error) {
|
||||
// todo: add your logic here and delete this line
|
||||
|
||||
return &pb.ValidateTokenResp{}, nil
|
||||
}
|
||||
@@ -53,3 +53,18 @@ func (s *UsercenterServer) SearchUsers(ctx context.Context, in *pb.SearchUsersRe
|
||||
l := logic.NewSearchUsersLogic(ctx, s.svcCtx)
|
||||
return l.SearchUsers(in)
|
||||
}
|
||||
|
||||
func (s *UsercenterServer) Login(ctx context.Context, in *pb.LoginReq) (*pb.LoginResp, error) {
|
||||
l := logic.NewLoginLogic(ctx, s.svcCtx)
|
||||
return l.Login(in)
|
||||
}
|
||||
|
||||
func (s *UsercenterServer) ValidateToken(ctx context.Context, in *pb.ValidateTokenReq) (*pb.ValidateTokenResp, error) {
|
||||
l := logic.NewValidateTokenLogic(ctx, s.svcCtx)
|
||||
return l.ValidateToken(in)
|
||||
}
|
||||
|
||||
func (s *UsercenterServer) CheckPermission(ctx context.Context, in *pb.CheckPermissionReq) (*pb.CheckPermissionResp, error) {
|
||||
l := logic.NewCheckPermissionLogic(ctx, s.svcCtx)
|
||||
return l.CheckPermission(in)
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"juwan-backend/app/users/rpc/internal/config"
|
||||
"juwan-backend/app/users/rpc/internal/models"
|
||||
"juwan-backend/app/users/rpc/internal/utils"
|
||||
"time"
|
||||
|
||||
"github.com/redis/go-redis/v9"
|
||||
@@ -15,6 +16,7 @@ type ServiceContext struct {
|
||||
Config config.Config
|
||||
UsersModel models.UsersModel
|
||||
RedisCluster *redis.ClusterClient
|
||||
JwtManager *utils.JwtManager
|
||||
}
|
||||
|
||||
func NewServiceContext(c config.Config) *ServiceContext {
|
||||
@@ -39,9 +41,13 @@ func NewServiceContext(c config.Config) *ServiceContext {
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize JWT Manager
|
||||
jwtManager := utils.NewJwtManager(redisCluster, c.Jwt.SecretKey, c.Jwt.Issuer)
|
||||
|
||||
return &ServiceContext{
|
||||
Config: c,
|
||||
UsersModel: models.NewUsersModel(conn, c.CacheConf),
|
||||
RedisCluster: redisCluster,
|
||||
JwtManager: jwtManager,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/golang-jwt/jwt/v4"
|
||||
)
|
||||
|
||||
// JWKS (JSON Web Key Set) 结构
|
||||
type JWKSKey struct {
|
||||
Kty string `json:"kty"`
|
||||
Use string `json:"use"`
|
||||
Kid string `json:"kid"`
|
||||
N string `json:"n,omitempty"`
|
||||
E string `json:"e,omitempty"`
|
||||
K string `json:"k,omitempty"` // 对称密钥
|
||||
Alg string `json:"alg"`
|
||||
}
|
||||
|
||||
type JWKS struct {
|
||||
Keys []JWKSKey `json:"keys"`
|
||||
}
|
||||
|
||||
// GenerateJWKSFromSecret 从密钥生成 JWKS(用于对称加密 HS256)
|
||||
func GenerateJWKSFromSecret(secretKey string, keyID string) *JWKS {
|
||||
// 对于 HS256,将密钥进行 base64 编码
|
||||
encodedSecret := base64.RawURLEncoding.EncodeToString([]byte(secretKey))
|
||||
|
||||
return &JWKS{
|
||||
Keys: []JWKSKey{
|
||||
{
|
||||
Kty: "oct",
|
||||
Use: "sig",
|
||||
Kid: keyID,
|
||||
K: encodedSecret,
|
||||
Alg: "HS256",
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// GenerateJWKSEndpoint 生成可以被 Envoy 使用的 JWKS JSON
|
||||
// 此端点应在 user-rpc 中暴露,URL 为 /.well-known/jwks.json
|
||||
func GenerateJWKSEndpoint(secretKey string, keyID string) (string, error) {
|
||||
if secretKey == "" {
|
||||
return "", fmt.Errorf("secret key cannot be empty")
|
||||
}
|
||||
|
||||
jwks := GenerateJWKSFromSecret(secretKey, keyID)
|
||||
|
||||
jsonData, err := json.MarshalIndent(jwks, "", " ")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return string(jsonData), nil
|
||||
}
|
||||
|
||||
// TokenPayload 令牌负载
|
||||
type TokenMetadata struct {
|
||||
IssuedAt time.Time
|
||||
ExpiresAt time.Time
|
||||
Subject string // userId
|
||||
Issuer string
|
||||
Audience string
|
||||
}
|
||||
|
||||
// ExtractTokenMetadata 从 token 中提取元数据(不验证签名)
|
||||
func ExtractTokenMetadata(tokenString string) (*TokenMetadata, error) {
|
||||
token, _, err := new(jwt.Parser).ParseUnverified(tokenString, &Claims{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
claims, ok := token.Claims.(*Claims)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("invalid token claims type")
|
||||
}
|
||||
|
||||
return &TokenMetadata{
|
||||
IssuedAt: claims.IssuedAt.Time,
|
||||
ExpiresAt: claims.ExpiresAt.Time,
|
||||
Subject: claims.UserId,
|
||||
Issuer: claims.Issuer,
|
||||
Audience: "", // 如果需要,可以增加到 Claims 中
|
||||
}, nil
|
||||
}
|
||||
@@ -1,8 +1,14 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/golang-jwt/jwt/v4"
|
||||
"github.com/redis/go-redis/v9"
|
||||
)
|
||||
|
||||
type TokenPayload struct {
|
||||
@@ -10,11 +16,16 @@ type TokenPayload struct {
|
||||
IsAdmin bool
|
||||
}
|
||||
|
||||
type Claims struct {
|
||||
TokenPayload
|
||||
jwt.RegisteredClaims
|
||||
}
|
||||
|
||||
const (
|
||||
tokenCachePrefixUser = "jwt:user:"
|
||||
tokenCachePrefixToken = "jwt:token:"
|
||||
tokenCacheTTL = 60 * 24 * time.Hour
|
||||
tokenLifetime = 5 * 24 * time.Hour
|
||||
tokenCacheTTL = 30 * 24 * time.Hour
|
||||
tokenLifetime = 7 * 24 * time.Hour
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -22,9 +33,211 @@ var (
|
||||
errInvalidToken = errors.New("invalid token claims")
|
||||
errTokenNotInCache = errors.New("token not found in cache")
|
||||
errNoRedisClient = errors.New("redis client not configured")
|
||||
// errExpiredToken = errors.New("token expired")
|
||||
)
|
||||
|
||||
func NewToken(payload TokenPayload) (string, error) {
|
||||
|
||||
return "", nil
|
||||
type JwtManager struct {
|
||||
redisCluster *redis.ClusterClient
|
||||
secretKey string
|
||||
issuer string
|
||||
}
|
||||
|
||||
func NewJwtManager(redisCluster *redis.ClusterClient, secretKey, issuer string) *JwtManager {
|
||||
return &JwtManager{
|
||||
redisCluster: redisCluster,
|
||||
secretKey: secretKey,
|
||||
issuer: issuer,
|
||||
}
|
||||
}
|
||||
|
||||
// New 生成新的 JWT token
|
||||
func (m *JwtManager) New(ctx context.Context, payload *TokenPayload) (string, error) {
|
||||
if m.redisCluster == nil {
|
||||
return "", errNoRedisClient
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
expiresAt := now.Add(tokenLifetime)
|
||||
|
||||
claims := &Claims{
|
||||
TokenPayload: *payload,
|
||||
RegisteredClaims: jwt.RegisteredClaims{
|
||||
ExpiresAt: jwt.NewNumericDate(expiresAt),
|
||||
IssuedAt: jwt.NewNumericDate(now),
|
||||
Issuer: m.issuer,
|
||||
},
|
||||
}
|
||||
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
||||
tokenString, err := token.SignedString([]byte(m.secretKey))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// 存储 token 到 Redis,TTL 为 30 天
|
||||
userKey := tokenCachePrefixUser + payload.UserId
|
||||
tokenKey := tokenCachePrefixToken + tokenString
|
||||
|
||||
tokenData, _ := json.Marshal(payload)
|
||||
|
||||
// 同时存储两个 key:用户 -> token 和 token -> payload
|
||||
pipe := m.redisCluster.Pipeline()
|
||||
pipe.Set(ctx, userKey, tokenString, tokenCacheTTL)
|
||||
pipe.Set(ctx, tokenKey, string(tokenData), tokenCacheTTL)
|
||||
_, err = pipe.Exec(ctx)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return tokenString, nil
|
||||
}
|
||||
|
||||
// Valid 验证 token 有效性,支持自动换票
|
||||
func (m *JwtManager) Valid(ctx context.Context, tokenString string) (*TokenPayload, error) {
|
||||
if m.redisCluster == nil {
|
||||
return nil, errNoRedisClient
|
||||
}
|
||||
|
||||
if tokenString == "" {
|
||||
return nil, errMissingToken
|
||||
}
|
||||
|
||||
// 检查 token 是否在 Redis 中
|
||||
tokenKey := tokenCachePrefixToken + tokenString
|
||||
tokenData, err := m.redisCluster.Get(ctx, tokenKey).Result()
|
||||
if err != nil && err != redis.Nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var payload TokenPayload
|
||||
if err == redis.Nil {
|
||||
return nil, errTokenNotInCache
|
||||
}
|
||||
|
||||
err = json.Unmarshal([]byte(tokenData), &payload)
|
||||
if err != nil {
|
||||
return nil, errInvalidToken
|
||||
}
|
||||
|
||||
// 解析 JWT 并验证签名和过期时间
|
||||
token, err := jwt.ParseWithClaims(tokenString, &Claims{}, func(token *jwt.Token) (interface{}, error) {
|
||||
return []byte(m.secretKey), nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
claims, ok := token.Claims.(*Claims)
|
||||
if !ok || !token.Valid {
|
||||
return nil, errInvalidToken
|
||||
}
|
||||
|
||||
return &claims.TokenPayload, nil
|
||||
}
|
||||
|
||||
// Renew 换票逻辑:如果 token 过期但 Redis 中还存在,则生成新 token
|
||||
func (m *JwtManager) Renew(ctx context.Context, tokenString string) (string, error) {
|
||||
if m.redisCluster == nil {
|
||||
return "", errNoRedisClient
|
||||
}
|
||||
|
||||
// 检查 token 是否在 Redis 中(不检查过期时间)
|
||||
tokenKey := tokenCachePrefixToken + tokenString
|
||||
tokenData, err := m.redisCluster.Get(ctx, tokenKey).Result()
|
||||
if err != nil {
|
||||
if err == redis.Nil {
|
||||
return "", errTokenNotInCache
|
||||
}
|
||||
return "", err
|
||||
}
|
||||
|
||||
var payload TokenPayload
|
||||
err = json.Unmarshal([]byte(tokenData), &payload)
|
||||
if err != nil {
|
||||
return "", errInvalidToken
|
||||
}
|
||||
|
||||
// 删除旧 token 记录
|
||||
userKey := tokenCachePrefixUser + payload.UserId
|
||||
m.redisCluster.Del(ctx, tokenKey, userKey)
|
||||
|
||||
// 生成新 token
|
||||
return m.New(ctx, &payload)
|
||||
}
|
||||
|
||||
// extract payload from token without validating expiration (used for auto-renewal)
|
||||
func (m *JwtManager) Extract(ctx context.Context, tokenString string) (*TokenPayload, error) {
|
||||
token, err := jwt.ParseWithClaims(tokenString, &Claims{}, func(token *jwt.Token) (interface{}, error) {
|
||||
return []byte(m.secretKey), nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
claims, ok := token.Claims.(*Claims)
|
||||
if !ok {
|
||||
return nil, errInvalidToken
|
||||
}
|
||||
|
||||
return &claims.TokenPayload, nil
|
||||
}
|
||||
|
||||
// check if token exists in Redis (i.e. is valid and not revoked)
|
||||
func (m *JwtManager) Exists(ctx context.Context, tokenString string) (bool, error) {
|
||||
if m.redisCluster == nil {
|
||||
return false, errNoRedisClient
|
||||
}
|
||||
|
||||
tokenKey := tokenCachePrefixToken + tokenString
|
||||
exists, err := m.redisCluster.Exists(ctx, tokenKey).Result()
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
return exists > 0, nil
|
||||
}
|
||||
|
||||
// extract payload from JWT claims
|
||||
func (m *JwtManager) ClaimsToPayload(claims *Claims) *TokenPayload {
|
||||
return &claims.TokenPayload
|
||||
}
|
||||
|
||||
// revoke token by deleting both user -> token and token -> payload keys from Redis
|
||||
func (m *JwtManager) Revoke(ctx context.Context, tokenString string) error {
|
||||
if m.redisCluster == nil {
|
||||
return errNoRedisClient
|
||||
}
|
||||
|
||||
payload, err := m.Extract(ctx, tokenString)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
userKey := tokenCachePrefixUser + payload.UserId
|
||||
tokenKey := tokenCachePrefixToken + tokenString
|
||||
|
||||
pipe := m.redisCluster.Pipeline()
|
||||
pipe.Del(ctx, userKey)
|
||||
pipe.Del(ctx, tokenKey)
|
||||
_, err = pipe.Exec(ctx)
|
||||
return err
|
||||
}
|
||||
|
||||
func (m *JwtManager) GetUserToken(ctx context.Context, userID string) (string, error) {
|
||||
if m.redisCluster == nil {
|
||||
return "", errNoRedisClient
|
||||
}
|
||||
|
||||
userKey := tokenCachePrefixUser + userID
|
||||
token, err := m.redisCluster.Get(ctx, userKey).Result()
|
||||
if err != nil {
|
||||
if err == redis.Nil {
|
||||
return "", fmt.Errorf("user %s has no token", userID)
|
||||
}
|
||||
return "", err
|
||||
}
|
||||
|
||||
return token, nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user