package common import ( "context" "crypto/hmac" "crypto/sha256" "encoding/base64" "encoding/hex" "encoding/json" "net/http" "strings" "time" "github.com/gogf/gf/v2/errors/gcode" "github.com/gogf/gf/v2/errors/gerror" "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/net/ghttp" ) // 登录 token(HMAC-SHA256 自签名,无状态): // token = base64url(payload) + "." + hex(HMAC-SHA256(payload, auth.secret)) // payload = {"phone": "...", "exp": };secret 轮换即全员下线。 var ctxKeyPhone = struct{}{} // AuthTokenTtl 登录 token 有效期(秒),来自 config.yml auth.tokenTtl,缺失或非法回退 30 天。 func AuthTokenTtl(ctx context.Context) time.Duration { ttl := g.Cfg().MustGet(ctx, "auth.tokenTtl", 2592000).Int64() if ttl <= 0 { ttl = 2592000 } return time.Duration(ttl) * time.Second } // SignToken 签发登录 token(secret 未配置时报错,注册/登录不可用) func SignToken(ctx context.Context, phone string) (string, error) { secret := g.Cfg().MustGet(ctx, "auth.secret", "").String() if secret == "" { return "", gerror.New("登录未配置(检查 config.yml auth.secret)") } payload, err := json.Marshal(map[string]any{ "phone": phone, "exp": time.Now().Add(AuthTokenTtl(ctx)).Unix(), }) if err != nil { return "", gerror.Wrap(err, "序列化 token 载荷失败") } return tokenOf(payload, secret), nil } // ParseToken 校验并解析 token,返回 phone func ParseToken(ctx context.Context, token string) (string, error) { secret := g.Cfg().MustGet(ctx, "auth.secret", "").String() if secret == "" { return "", gerror.New("登录未配置") } i := strings.LastIndex(token, ".") if i <= 0 { return "", gerror.NewCode(gcode.CodeNotAuthorized, "token 格式错误") } payload, sig := token[:i], token[i+1:] raw, err := base64.RawURLEncoding.DecodeString(payload) if err != nil { return "", gerror.NewCode(gcode.CodeNotAuthorized, "token 载荷无效") } // 签名覆盖原始载荷字节(签发时对 raw 签名,非 base64 串) if !hmac.Equal([]byte(sig), []byte(signPayload(string(raw), secret))) { return "", gerror.NewCode(gcode.CodeNotAuthorized, "token 签名无效") } var body struct { Phone string `json:"phone"` Exp int64 `json:"exp"` } if err := json.Unmarshal(raw, &body); err != nil { return "", gerror.NewCode(gcode.CodeNotAuthorized, "token 载荷无效") } if body.Phone == "" || body.Exp < time.Now().Unix() { return "", gerror.NewCode(gcode.CodeNotAuthorized, "token 已过期") } return body.Phone, nil } func tokenOf(payload []byte, secret string) string { return base64.RawURLEncoding.EncodeToString(payload) + "." + signPayload(string(payload), secret) } func signPayload(payload, secret string) string { mac := hmac.New(sha256.New, []byte(secret)) mac.Write([]byte(payload)) return hex.EncodeToString(mac.Sum(nil)) } // AuthRequired 登录态鉴权中间件:校验 Authorization: Bearer , // 解出手机号注入请求上下文(service 经 PhoneFromCtx 读取)。 func AuthRequired(r *ghttp.Request) { phone, err := ParseToken(r.GetCtx(), bearerToken(r.Header.Get("Authorization"))) if err != nil { r.Response.WriteStatusExit(http.StatusUnauthorized, g.Map{ "code": gcode.CodeNotAuthorized.Code(), "message": "登录已失效,请重新登录", "data": nil, }) } r.SetCtx(WithPhone(r.GetCtx(), phone)) r.Middleware.Next() } func bearerToken(header string) string { if strings.HasPrefix(header, "Bearer ") { return strings.TrimPrefix(header, "Bearer ") } return "" } // PhoneFromCtx 取 AuthRequired 注入的登录手机号 func PhoneFromCtx(ctx context.Context) string { if v, ok := ctx.Value(ctxKeyPhone).(string); ok { return v } return "" } // WithPhone 注入登录手机号到 ctx(AuthRequired 中间件与白盒测试共用) func WithPhone(ctx context.Context, phone string) context.Context { return context.WithValue(ctx, ctxKeyPhone, phone) }