127 lines
3.9 KiB
Go
127 lines
3.9 KiB
Go
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": <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 签发登录 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 <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 注入登录手机号到 ctx(AuthRequired 中间件与白盒测试共用)
|
||
func WithPhone(ctx context.Context, phone string) context.Context {
|
||
return context.WithValue(ctx, ctxKeyPhone, phone)
|
||
}
|