feat(backend): jwt middleware, login with redis rate limit, /auth/me
This commit is contained in:
@@ -0,0 +1,78 @@
|
||||
package redispkg
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"log"
|
||||
"time"
|
||||
|
||||
"github.com/redis/go-redis/v9"
|
||||
)
|
||||
|
||||
type R struct{ c *redis.Client }
|
||||
|
||||
func New(url string) *R {
|
||||
if url == "" {
|
||||
return &R{}
|
||||
}
|
||||
opt, err := redis.ParseURL(url)
|
||||
if err != nil {
|
||||
log.Printf("bad REDIS_URL (%v): redis disabled", err)
|
||||
return &R{}
|
||||
}
|
||||
return &R{c: redis.NewClient(opt)}
|
||||
}
|
||||
|
||||
func (r *R) Get(ctx context.Context, key string) (string, bool) {
|
||||
if r.c == nil {
|
||||
return "", false
|
||||
}
|
||||
v, err := r.c.Get(ctx, key).Result()
|
||||
if err != nil {
|
||||
return "", false // 故障=miss
|
||||
}
|
||||
return v, true
|
||||
}
|
||||
|
||||
func (r *R) Set(ctx context.Context, key, val string, ttl time.Duration) {
|
||||
if r.c == nil {
|
||||
return
|
||||
}
|
||||
if err := r.c.Set(ctx, key, val, ttl).Err(); err != nil {
|
||||
log.Printf("redis set %s: %v", key, err)
|
||||
}
|
||||
}
|
||||
|
||||
func (r *R) IncrWindow(ctx context.Context, key string, ttl time.Duration) int {
|
||||
if r.c == nil {
|
||||
return 1
|
||||
}
|
||||
n, err := r.c.Incr(ctx, key).Result()
|
||||
if err != nil {
|
||||
return 1
|
||||
}
|
||||
if n == 1 {
|
||||
r.c.Expire(ctx, key, ttl)
|
||||
}
|
||||
return int(n)
|
||||
}
|
||||
|
||||
func (r *R) Lock(ctx context.Context, key string, ttl time.Duration) (func(), bool) {
|
||||
noop := func() {}
|
||||
if r.c == nil {
|
||||
return noop, true
|
||||
}
|
||||
b := make([]byte, 8)
|
||||
rand.Read(b)
|
||||
tok := hex.EncodeToString(b)
|
||||
ok, err := r.c.SetNX(ctx, key, tok, ttl).Result()
|
||||
if err != nil || !ok {
|
||||
return noop, false
|
||||
}
|
||||
return func() {
|
||||
r.c.Eval(ctx,
|
||||
"if redis.call('get',KEYS[1])==ARGV[1] then return redis.call('del',KEYS[1]) else return 0 end",
|
||||
[]string{key}, tok)
|
||||
}, true
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package redispkg
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestDisabledIsSafe(t *testing.T) {
|
||||
r := New("")
|
||||
ctx := context.Background()
|
||||
if _, ok := r.Get(ctx, "x"); ok {
|
||||
t.Fatal("disabled Get must miss")
|
||||
}
|
||||
r.Set(ctx, "x", "y", time.Second) // 不 panic
|
||||
if n := r.IncrWindow(ctx, "k", time.Second); n != 1 {
|
||||
t.Fatal("disabled IncrWindow must allow")
|
||||
}
|
||||
un, ok := r.Lock(ctx, "lk", time.Second)
|
||||
if !ok {
|
||||
t.Fatal("disabled Lock must always acquire")
|
||||
}
|
||||
un()
|
||||
}
|
||||
Reference in New Issue
Block a user