├── README.md ├── docker-compose.yml ├── init ├── Dockerfile └── init.sql ├── nginx ├── Dockerfile ├── lua │ └── check_token.lua └── nginx.conf └── sync ├── Dockerfile └── binlog_to_redis.go /README.md: -------------------------------------------------------------------------------- 1 | # SecureAPIProxy 2 | 3 | **SecureAPIProxy** is an efficient and reliable API protection system designed to prevent malicious attacks and ensure that only legitimate users can access your API. 4 | 5 | ## Features 6 | 7 | - **User Authentication**: Authenticate users using UUID and token stored in Redis. 8 | - **Request Verification**: Check the request token; if invalid, return 403 and log the failure. After three failures, add the IP to the Redis blacklist for 24 hours. 9 | - **IP Blacklisting**: Add IPs with repeated failures to a Redis blacklist, blocking them for 24 hours. 10 | - **Secure Forwarding**: Forward valid requests to the real API with a specific OAuth header. 11 | - **Real-time Sync**: Use MySQL Binlog to sync user data to Redis in real-time. 12 | 13 | ## Project Structure 14 | 15 | ```plaintext 16 | SecureAPIProxy/ 17 | ├── README.md 18 | ├── docker-compose.yml 19 | ├── nginx/ 20 | │ ├── Dockerfile 21 | │ ├── nginx.conf 22 | │ └── lua/ 23 | │ └── check_token.lua 24 | ├── sync/ 25 | │ ├── Dockerfile 26 | │ └── binlog_to_redis.go 27 | └── init/ 28 | ├── init.sql 29 | └── Dockerfile 30 | ``` 31 | 32 | ## Getting Started 33 | 34 | 1. Clone the Repository 35 | ```bash 36 | git clone https://github.com/yourusername/SecureAPIProxy.git 37 | cd SecureAPIProxy 38 | ``` 39 | 40 | 2. Build and Start the Services 41 | ```bash 42 | docker-compose up --build 43 | ``` 44 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.8' 2 | 3 | services: 4 | nginx: 5 | build: ./nginx 6 | ports: 7 | - "80:80" 8 | depends_on: 9 | - redis 10 | - mysql 11 | 12 | redis: 13 | image: redis:latest 14 | ports: 15 | - "6379:6379" 16 | 17 | mysql: 18 | image: mysql:5.7 19 | environment: 20 | MYSQL_ROOT_PASSWORD: rootpassword 21 | MYSQL_DATABASE: secureapi 22 | MYSQL_USER: user 23 | MYSQL_PASSWORD: password 24 | ports: 25 | - "3306:3306" 26 | volumes: 27 | - ./init:/docker-entrypoint-initdb.d 28 | 29 | sync: 30 | build: ./sync 31 | depends_on: 32 | - mysql 33 | - redis 34 | -------------------------------------------------------------------------------- /init/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM mysql:5.7 2 | COPY init.sql /docker-entrypoint-initdb.d/ 3 | -------------------------------------------------------------------------------- /init/init.sql: -------------------------------------------------------------------------------- 1 | -- Just for testing 2 | CREATE DATABASE IF NOT EXISTS secureapi; 3 | USE secureapi; 4 | 5 | CREATE TABLE IF NOT EXISTS v2_user ( 6 | id INT AUTO_INCREMENT PRIMARY KEY, 7 | uuid VARCHAR(255) NOT NULL, 8 | token VARCHAR(255) NOT NULL, 9 | created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP 10 | ); 11 | 12 | INSERT INTO v2_user (uuid, token) VALUES ('user1-uuid', 'user1-token'); 13 | INSERT INTO v2_user (uuid, token) VALUES ('user2-uuid', 'user2-token'); 14 | -------------------------------------------------------------------------------- /nginx/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM nginx:latest 2 | RUN apt-get update && apt-get install -y libnginx-mod-http-lua luarocks 3 | RUN luarocks install lua-resty-redis 4 | COPY nginx.conf /etc/nginx/nginx.conf 5 | COPY lua/check_token.lua /etc/nginx/lua/check_token.lua 6 | -------------------------------------------------------------------------------- /nginx/lua/check_token.lua: -------------------------------------------------------------------------------- 1 | local redis = require "resty.redis" 2 | local cjson = require "cjson.safe" 3 | 4 | -- Connect to Redis 5 | local function connect_redis() 6 | local red = redis:new() 7 | red:set_timeout(1000) -- 1 sec 8 | 9 | local ok, err = red:connect("redis", 6379) 10 | if not ok then 11 | ngx.log(ngx.ERR, "failed to connect to Redis: ", err) 12 | return nil 13 | end 14 | return red 15 | end 16 | 17 | local function check_blacklist(red, ip) 18 | local res, err = red:get("blacklist:" .. ip) 19 | if res == "1" then 20 | return true 21 | end 22 | return false 23 | end 24 | 25 | local function add_to_blacklist(red, ip) 26 | local res, err = red:setex("blacklist:" .. ip, 86400, "1") -- 24 hours 27 | if not res then 28 | ngx.log(ngx.ERR, "failed to add to blacklist: ", err) 29 | end 30 | end 31 | 32 | local function increment_failures(red, ip) 33 | local res, err = red:incr("failures:" .. ip) 34 | if not res then 35 | ngx.log(ngx.ERR, "failed to increment failures: ", err) 36 | return 0 37 | end 38 | red:expire("failures:" .. ip, 86400) -- 24 hours 39 | return res 40 | end 41 | 42 | local function get_user_id(red, token) 43 | local user_id, err = red:get(token) 44 | if not user_id or user_id == ngx.null then 45 | return nil 46 | end 47 | return user_id 48 | end 49 | 50 | -- Main logic 51 | local red = connect_redis() 52 | if not red then 53 | ngx.exit(ngx.HTTP_INTERNAL_SERVER_ERROR) 54 | return 55 | end 56 | 57 | local ip = ngx.var.remote_addr 58 | if check_blacklist(red, ip) then 59 | ngx.exit(ngx.HTTP_FORBIDDEN) 60 | return 61 | end 62 | 63 | local token = ngx.var.arg_token 64 | if not token then 65 | ngx.exit(ngx.HTTP_BAD_REQUEST) 66 | return 67 | end 68 | 69 | local user_id = get_user_id(red, token) 70 | if not user_id then 71 | local failures = increment_failures(red, ip) 72 | if failures >= 3 then 73 | add_to_blacklist(red, ip) 74 | end 75 | ngx.exit(ngx.HTTP_FORBIDDEN) 76 | return 77 | end 78 | 79 | ngx.var.user_id = user_id 80 | -------------------------------------------------------------------------------- /nginx/nginx.conf: -------------------------------------------------------------------------------- 1 | worker_processes 1; 2 | 3 | events { 4 | worker_connections 1024; 5 | } 6 | 7 | http { 8 | lua_package_path "/etc/nginx/lua/?.lua;;"; 9 | 10 | server { 11 | listen 80; 12 | 13 | location /api/v1/client/subscribe { 14 | access_by_lua_file /etc/nginx/lua/check_token.lua; 15 | 16 | set $cloudflare_ip_header $remote_addr; 17 | 18 | proxy_pass http://your_backend_server; 19 | proxy_set_header Host $host; 20 | proxy_set_header X-Real-IP $remote_addr; 21 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 22 | proxy_set_header X-Forwarded-Proto $scheme; 23 | proxy_set_header Authorization "Bearer your_specific_oauth_token"; 24 | } 25 | 26 | location / { 27 | return 403; 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /sync/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.16 2 | 3 | WORKDIR /app 4 | 5 | COPY . . 6 | 7 | RUN go mod download 8 | RUN go build -o binlog_to_redis 9 | 10 | CMD ["./binlog_to_redis"] 11 | -------------------------------------------------------------------------------- /sync/binlog_to_redis.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "log" 6 | 7 | "github.com/go-redis/redis/v8" 8 | "github.com/siddontang/go-mysql/canal" 9 | "github.com/siddontang/go-mysql/mysql" 10 | ) 11 | 12 | var ( 13 | ctx = context.Background() 14 | redisAddr = "redis:6379" 15 | redisDB = 0 16 | redisPass = "" 17 | ) 18 | 19 | type MyEventHandler struct { 20 | canal.DummyEventHandler 21 | redisClient *redis.Client 22 | } 23 | 24 | func (h *MyEventHandler) OnRow(e *canal.RowsEvent) error { 25 | if e.Table.Schema == "secureapi" && e.Table.Name == "v2_user" { 26 | for _, row := range e.Rows { 27 | uuid := row[0].(string) 28 | token := row[1].(string) 29 | err := h.redisClient.Set(ctx, token, uuid, 0).Err() 30 | if err != nil { 31 | log.Printf("Error setting key in Redis: %v", err) 32 | } else { 33 | log.Printf("UUID %s with token %s synced to Redis", uuid, token) 34 | } 35 | } 36 | } 37 | return nil 38 | } 39 | 40 | func main() { 41 | redisClient := redis.NewClient(&redis.Options{ 42 | Addr: redisAddr, 43 | Password: redisPass, 44 | DB: redisDB, 45 | }) 46 | 47 | cfg := canal.NewDefaultConfig() 48 | cfg.Addr = "mysql:3306" 49 | cfg.User = "user" 50 | cfg.Password = "password" 51 | cfg.Flavor = "mysql" 52 | 53 | c, err := canal.NewCanal(cfg) 54 | if err != nil { 55 | log.Fatal(err) 56 | } 57 | 58 | h := &MyEventHandler{redisClient: redisClient} 59 | c.SetEventHandler(h) 60 | 61 | pos, err := c.GetMasterPos() 62 | if err != nil { 63 | log.Fatal(err) 64 | } 65 | 66 | err = c.StartFrom(pos) 67 | if err != nil { 68 | log.Fatal(err) 69 | } 70 | 71 | select {} 72 | } 73 | --------------------------------------------------------------------------------