Files

127 lines
3.9 KiB
Go
Raw Permalink 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 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"
)
// 登录 tokenHMAC-SHA256 自签名,无状态):
// token = base64url(payload) + "." + hex(HMAC-SHA256(payload, auth.secret))
// payload = {"phone": "...", "exp": <unix 秒>}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 签发登录 tokensecret 未配置时报错,注册/登录不可用)
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 <token>
// 解出手机号注入请求上下文(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 注入登录手机号到 ctxAuthRequired 中间件与白盒测试共用)
func WithPhone(ctx context.Context, phone string) context.Context {
return context.WithValue(ctx, ctxKeyPhone, phone)
}