新增防重放插件

This commit is contained in:
Liujian
2025-06-17 14:57:31 +08:00
parent e4d364c393
commit 8a623edf37
4 changed files with 213 additions and 12 deletions

View File

@@ -20,6 +20,7 @@ import (
params_check "github.com/eolinker/apinto/drivers/plugins/params-check"
params_check_v2 "github.com/eolinker/apinto/drivers/plugins/params-check-v2"
"github.com/eolinker/apinto/drivers/plugins/prometheus"
replay_attack_defender "github.com/eolinker/apinto/drivers/plugins/replay-attack-defender"
request_file_parse "github.com/eolinker/apinto/drivers/plugins/request-file-parse"
request_interception "github.com/eolinker/apinto/drivers/plugins/request-interception"
response_file_parse "github.com/eolinker/apinto/drivers/plugins/response-file-parse"
@@ -28,7 +29,7 @@ import (
rsa_filter "github.com/eolinker/apinto/drivers/plugins/rsa-filter"
script_handler "github.com/eolinker/apinto/drivers/plugins/script-handler"
data_mask "github.com/eolinker/apinto/drivers/plugins/strategy/data-mask"
access_log "github.com/eolinker/apinto/drivers/plugins/access-log"
body_check "github.com/eolinker/apinto/drivers/plugins/body-check"
circuit_breaker "github.com/eolinker/apinto/drivers/plugins/circuit-breaker"
@@ -52,12 +53,12 @@ import (
"github.com/eolinker/apinto/drivers/plugins/strategy/grey"
"github.com/eolinker/apinto/drivers/plugins/strategy/limiting"
"github.com/eolinker/apinto/drivers/plugins/strategy/visit"
"github.com/eolinker/eosc"
)
func pluginRegister(extenderRegister eosc.IExtenderDriverRegister) {
// 服务治理-策略相关插件
limiting.Register(extenderRegister)
cache.Register(extenderRegister)
@@ -65,17 +66,17 @@ func pluginRegister(extenderRegister eosc.IExtenderDriverRegister) {
visit.Register(extenderRegister)
fuse.Register(extenderRegister)
data_mask.Register(extenderRegister)
// Dubbo协议相关插件
dubbo2_proxy_rewrite.Register(extenderRegister)
http_to_dubbo2.Register(extenderRegister)
dubbo2_to_http.Register(extenderRegister)
// gRPC协议相关插件
http_to_grpc.Register(extenderRegister)
grpc_to_http.Register(extenderRegister)
grpc_proxy_rewrite.Register(extenderRegister)
// 请求处理相关插件
body_check.Register(extenderRegister)
extra_params.Register(extenderRegister)
@@ -89,7 +90,7 @@ func pluginRegister(extenderRegister eosc.IExtenderDriverRegister) {
data_transform.Register(extenderRegister)
request_interception.Register(extenderRegister)
request_file_parse.Register(extenderRegister)
// 响应处理插件
response_rewrite.Register(extenderRegister)
response_rewrite_v2.Register(extenderRegister)
@@ -97,7 +98,7 @@ func pluginRegister(extenderRegister eosc.IExtenderDriverRegister) {
gzip.Register(extenderRegister)
response_file_parse.Register(extenderRegister)
auto_redirect.Register(extenderRegister)
// 安全相关插件
ip_restriction.Register(extenderRegister)
rate_limiting.Register(extenderRegister)
@@ -109,23 +110,25 @@ func pluginRegister(extenderRegister eosc.IExtenderDriverRegister) {
js_inject.Register(extenderRegister)
acl.Register(extenderRegister)
access_relational.Register(extenderRegister)
replay_attack_defender.Register(extenderRegister)
// 可观测性(输出内容到第三方)
access_log.Register(extenderRegister)
prometheus.Register(extenderRegister)
monitor.Register(extenderRegister)
proxy_mirror.Register(extenderRegister)
// 计数插件
counter.Register(extenderRegister)
// 鉴权插件
oauth2.Register(extenderRegister)
oauth2_introspection.Register(extenderRegister)
// ai相关插件
ai_prompt.Register(extenderRegister)
ai_formatter.Register(extenderRegister)
//ai_balance.Register(extenderRegister)
script_handler.Register(extenderRegister)
}

View File

@@ -0,0 +1,12 @@
package replay_attack_defender
import "github.com/eolinker/eosc"
type Config struct {
NonceHeader string `json:"nonce_header" label:"API调用者生成的唯一UUID存放的请求头" default:"X-Ca-Nonce"`
TimestampHeader string `json:"timestamp_header" label:"10位时间戳存放的请求头" default:"X-Ca-Timestamp"`
SignHeader string `json:"sign_header" label:"防重放签名存放的请求头" default:"X-Ca-Signature"`
ReplayAttackToken string `json:"replay_attack_token" label:"重放攻击防御令牌" default:"apinto"`
TTL int `json:"ttl" label:"过期时间单位s" default:"600"`
Cache eosc.RequireId `json:"cache" skill:"github.com/eolinker/apinto/resources.resources.ICache" required:"false" label:"缓存位置"`
}

View File

@@ -0,0 +1,156 @@
package replay_attack_defender
import (
"errors"
"fmt"
"github.com/eolinker/apinto/drivers"
"github.com/eolinker/apinto/resources"
scope_manager "github.com/eolinker/apinto/scope-manager"
"github.com/eolinker/apinto/utils"
"github.com/eolinker/eosc"
"github.com/eolinker/eosc/eocontext"
http_service "github.com/eolinker/eosc/eocontext/http-context"
"github.com/go-redis/redis/v8"
"net/http"
"strconv"
"sync"
"time"
)
var _ http_service.HttpFilter = (*executor)(nil)
var _ eocontext.IFilter = (*executor)(nil)
var _ eosc.IWorker = (*executor)(nil)
type executor struct {
drivers.WorkerBase
signHeader string
timestampHeader string
nonceHeader string
token string
ttl time.Duration
cache scope_manager.IProxyOutput[resources.ICache]
once sync.Once
}
func (e *executor) DoFilter(ctx eocontext.EoContext, next eocontext.IChain) (err error) {
return http_service.DoHttpFilter(e, ctx, next)
}
func (e *executor) DoHttpFilter(ctx http_service.IHttpContext, next eocontext.IChain) (err error) {
e.once.Do(func() {
e.cache = scope_manager.Auto[resources.ICache]("", "redis")
})
timestamp, err := checkTimestamp(ctx.Request().Header().GetHeader(e.timestampHeader))
if err != nil {
ctx.Response().SetStatus(http.StatusBadRequest, "Bad Request")
ctx.Response().SetBody([]byte(err.Error()))
return err
}
nonce := ctx.Request().Header().GetHeader(e.nonceHeader)
if nonce == "" {
ctx.Response().SetStatus(http.StatusBadRequest, "Bad Request")
ctx.Response().SetBody([]byte("missing nonce"))
return fmt.Errorf("missing nonce")
}
sign := ctx.Request().Header().GetHeader(e.signHeader)
if sign == "" {
ctx.Response().SetStatus(http.StatusBadRequest, "Bad Request")
ctx.Response().SetBody([]byte("missing sign"))
return fmt.Errorf("missing sign")
}
signBefore := fmt.Sprintf("%d%s%s", timestamp, nonce, e.token)
signAfter := utils.Md5(signBefore)
if sign != signAfter {
ctx.Response().SetStatus(http.StatusForbidden, "Forbidden")
ctx.Response().SetBody([]byte("invalid sign"))
return fmt.Errorf("invalid sign")
}
key := "apinto:replay-attack-defender:" + sign
for _, c := range e.cache.List() {
if c == nil {
continue
}
_, err = c.Get(ctx.Context(), key).Result()
if err != nil {
if errors.Is(err, redis.Nil) {
c.SetNX(ctx.Context(), key, []byte(signBefore), e.ttl)
break
}
if err.Error() != "redis: nil" {
continue
}
}
err = fmt.Errorf("replay attack detected, nonce: %s, sign: %s, timestamp: %d", nonce, sign, timestamp)
ctx.Response().SetStatus(http.StatusForbidden, "Forbidden")
ctx.Response().SetBody([]byte(err.Error()))
return err
}
if next != nil {
return next.DoChain(ctx)
}
return nil
}
func (e *executor) Destroy() {
return
}
func (e *executor) Start() error {
return nil
}
func (e *executor) Reset(conf interface{}, workers map[eosc.RequireId]eosc.IWorker) error {
return nil
}
func (e *executor) reset(conf *Config) error {
if conf.NonceHeader == "" {
conf.NonceHeader = "X-Ca-Nonce"
}
if conf.SignHeader == "" {
conf.SignHeader = "X-Ca-Signature"
}
if conf.TimestampHeader == "" {
conf.TimestampHeader = "X-Ca-Timestamp"
}
if conf.TTL < 1 {
conf.TTL = 600
}
e.signHeader = conf.SignHeader
e.timestampHeader = conf.TimestampHeader
e.nonceHeader = conf.NonceHeader
e.token = conf.ReplayAttackToken
e.ttl = time.Duration(conf.TTL) * time.Second
return nil
}
func (e *executor) Stop() error {
e.Destroy()
return nil
}
func (e *executor) CheckSkill(skill string) bool {
return http_service.FilterSkillName == skill
}
func checkTimestamp(timestamp string, ) (int64, error) {
if timestamp == "" {
return 0, fmt.Errorf("missing timestamp")
}
ts, err := strconv.ParseInt(timestamp, 10, 64)
if err != nil {
return 0, fmt.Errorf("invalid timestamp %s, error: %v", timestamp, err)
}
now := time.Now()
t := time.Unix(ts, 0)
if now.Sub(t) > 10*time.Minute {
return 0, fmt.Errorf("timestamp %d is too old, current time is %d", ts, now.Unix())
} else if now.Sub(t) < -10*time.Minute {
return 0, fmt.Errorf("timestamp %d is too new, current time is %d", ts, now.Unix())
}
return ts, nil
}

View File

@@ -0,0 +1,30 @@
package replay_attack_defender
import (
"github.com/eolinker/apinto/drivers"
"github.com/eolinker/eosc"
)
const (
Name = "replay_attack_defender"
)
func Register(register eosc.IExtenderDriverRegister) {
register.RegisterExtenderDriver(Name, NewFactory())
}
func NewFactory() eosc.IExtenderDriverFactory {
return drivers.NewFactory[Config](Create)
}
func Create(id, name string, conf *Config, workers map[eosc.RequireId]eosc.IWorker) (eosc.IWorker, error) {
w := &executor{
WorkerBase: drivers.Worker(id, name),
}
err := w.reset(conf)
if err != nil {
return nil, err
}
return w, nil
}