Files
dudu/server/internal/auth/jwt.go
T
wangjia 40760aa884
ci / server (push) Failing after 14s
ci / design-tokens (push) Failing after 11s
dudu MVP:五端语音输入法初始提交
- server:Go 网关(WS 流式识别中继/计费配额/微信登录支付 mock/反馈/埋点),gummy provider 已真实联调
- desktop:Tauri 2(全局快捷键 push-to-talk/浮层/托盘/设置/登录购买/反馈/首启引导)
- android:Compose 主 App + IME(键盘内录音直传)
- ios:App + 键盘扩展(1A spike 实证键盘内不可录音,走 deep link 听写)
- design/design-pipeline:设计系统 + token 导出 iOS/Android 主题
- doc:前后端设计文档(HTML);web:官网宣传页;todo:任务看板

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 00:38:37 +08:00

96 lines
2.5 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Package auth JWT 签发/校验 + jti 黑名单。微信 OAuth 在 wechat.go5B/5C)。
package auth
import (
"context"
"net/http"
"strings"
"time"
"github.com/gin-gonic/gin"
"github.com/golang-jwt/jwt/v5"
"github.com/google/uuid"
"github.com/redis/go-redis/v9"
"dudu/server/internal/store"
"dudu/server/pkg/protocol"
)
type JWT struct {
secret []byte
ttl time.Duration
rdb *redis.Client
}
func NewJWT(secret string, ttl time.Duration, rdb *redis.Client) *JWT {
return &JWT{secret: []byte(secret), ttl: ttl, rdb: rdb}
}
type Claims struct {
jwt.RegisteredClaims
}
func (j *JWT) Sign(userID string) (string, error) {
now := time.Now()
claims := Claims{RegisteredClaims: jwt.RegisteredClaims{
Subject: userID,
ID: uuid.NewString(),
IssuedAt: jwt.NewNumericDate(now),
ExpiresAt: jwt.NewNumericDate(now.Add(j.ttl)),
}}
return jwt.NewWithClaims(jwt.SigningMethodHS256, claims).SignedString(j.secret)
}
// Parse 校验签名、过期与黑名单,返回 userID 与 jti。
func (j *JWT) Parse(ctx context.Context, token string) (userID, jti string, err error) {
var claims Claims
_, err = jwt.ParseWithClaims(token, &claims, func(t *jwt.Token) (any, error) {
return j.secret, nil
}, jwt.WithValidMethods([]string{"HS256"}))
if err != nil {
return "", "", err
}
if j.rdb != nil {
if n, _ := j.rdb.Exists(ctx, store.KeyJwtBlock(claims.ID)).Result(); n == 1 {
return "", "", jwt.ErrTokenExpired
}
}
return claims.Subject, claims.ID, nil
}
// Revoke 将 jti 拉黑至 token 自然过期(logout / 踢出)。
func (j *JWT) Revoke(ctx context.Context, jti string, expiresAt time.Time) error {
ttl := time.Until(expiresAt)
if ttl <= 0 {
return nil
}
return j.rdb.Set(ctx, store.KeyJwtBlock(jti), 1, ttl).Err()
}
const (
CtxUserID = "auth.user_id"
CtxJTI = "auth.jti"
)
// Middleware gin 鉴权中间件:Authorization: Bearer <JWT>。
func (j *JWT) Middleware() gin.HandlerFunc {
return func(c *gin.Context) {
token := strings.TrimPrefix(c.GetHeader("Authorization"), "Bearer ")
if token == "" {
c.AbortWithStatusJSON(http.StatusUnauthorized, protocol.NewAPIError(protocol.ErrUnauthorized))
return
}
uid, jti, err := j.Parse(c.Request.Context(), token)
if err != nil {
c.AbortWithStatusJSON(http.StatusUnauthorized, protocol.NewAPIError(protocol.ErrUnauthorized))
return
}
c.Set(CtxUserID, uid)
c.Set(CtxJTI, jti)
c.Next()
}
}
// UserID 从 gin 上下文取当前用户。
func UserID(c *gin.Context) string { return c.GetString(CtxUserID) }