最近无意中查看nginx发现好多疑似爬虫ip,遂临时起意能不能搞个频率限制。
一. 配置文件更改baseURL
// hugo config.toml
baseURL = "/public"
avatar = "/public/images/me/avatar.jpeg"
Wechat = "/public/images/me/wechat.png"
二. 更改部分模板html中的href
// 更改themes-leaveit-layouts-partials-home_profile.html
<div class="avatar">
<a href="/public/posts/"> <img src="{{ (printf "%s%s" $cdn_url $avatar)}}"> </a>
</div>
以上修改保证可以设置一个public静态文件夹可以访问所有内容(如果有上传图片保存在服务器本地的0.0,请记得修改markdown中的图片url)*
三. 编写网关代码
/*
目录结构:
blog_ratelimit
- apigw
- common
- code.go
- conf
- config.go
- config.json
- middleware
- ratelimit.go
- redisdb
- redisdb.go
- public
- main.go
*/
code.go
package common
const (
_ int32 = iota +999
StatusBlackList // 1000 黑名单限制
)
config.go
package conf
import (
"encoding/json"
"log"
"os"
)
type Configuration struct {
RedisAddr string `json:"redis_addr"` // redis地址
RedisPass string `json:"redis_pass"` // redis密码
}
var DefaultConfig *Configuration
func InitConfig(configLoc string){
_, err := os.Stat(configLoc)
if err != nil {
log.Panic("配置文件路径不存在")
}
file,_ := os.Open(configLoc)
defer file.Close()
decoder := json.NewDecoder(file)
DefaultConfig = &Configuration{}
err = decoder.Decode(DefaultConfig)
if err != nil {
log.Panic(err)
}
}
config.json
{
"redis_addr": "192.168.50.15:6379",
"redis_pass": ""
}
ratelimit.go
package middleware
import (
"blog_ratelimit/apigw/common"
"blog_ratelimit/apigw/redisdb"
"errors"
"fmt"
"github.com/gin-gonic/gin"
"github.com/go-redis/redis"
"net/http"
"strconv"
"time"
)
const (
blackListKey = "blacklist"
)
func IpVerify() gin.HandlerFunc {
return func(ctx *gin.Context) {
visitorIP := ctx.Request.Header.Get("X-real-ip")
// fmt.Println(visitorIP)
rdb := redisdb.GetRatePool()
// 判断是否在黑名单
timeOrigin := rdb.HGet(blackListKey, visitorIP).Val()
err := blackListVerify(timeOrigin,visitorIP, rdb)
if err != nil {
ctx.JSON(http.StatusOK, gin.H{
"code": common.StatusBlackList,
"msg": err.Error(),
})
ctx.Abort()
return
}
// 如果不存在于黑名单
lenList, _ := rdb.LLen(visitorIP).Result()
// 如果第一次登陆或频率限制外
if lenList == 0 {
// 如果为空跳过,设置后跳过
// 添加IP list
rdb.LPush(visitorIP, visitorIP)
// 设置过期时间
rdb.Expire(visitorIP, 1000*1000*1000)
ctx.Next()
}else if lenList >0 && lenList <3 {
rdb.LPush(visitorIP, visitorIP)
ctx.Next()
}else {
// 加入黑名单
rdb.HSet(blackListKey,visitorIP,time.Now().Local().Unix())
ctx.Abort()
return
}
}
}
func blackListVerify(ot,visitorIP string,rdb *redis.Client) error {
// 如果有值,进一步判断
if ot != "" {
// 如果value的时间和当前时间差超过十分钟,解除限制
timeOriginInt, _ := strconv.Atoi(ot)
oTimeUnix := time.Unix(int64(timeOriginInt),0).Local()
subTime := time.Now().Sub(oTimeUnix)
if subTime > 10*time.Minute {
// 超过限制时间 解除限制
rdb.HDel(blackListKey,visitorIP)
return nil
}else {
//
return errors.New(fmt.Sprintf("您已被加入黑名单,剩余限制时间:%v",10*time.Minute-subTime))
}
}
// 不存在返回nil
return nil
}
redisdb.go
package redisdb
import (
"blog_ratelimit/apigw/conf"
"github.com/go-redis/redis"
"log"
"time"
)
var ratePool *redis.Client
func init(){
conf.InitConfig("./apigw/conf/config.json")
rateRedisInit()
}
func rateRedisInit(){
client := redis.NewClient(&redis.Options{
Addr: conf.DefaultConfig.RedisAddr,
Password: conf.DefaultConfig.RedisPass,
DB: 1,
PoolSize: 50,
MinIdleConns: 20,
IdleTimeout: 30*time.Second,
})
_, err := client.Ping().Result()
if err != nil {
log.Panic("failed to connect redis.")
}
ratePool = client
}
func GetRatePool() * redis.Client {
return ratePool
}
main.go
package main
import (
"blog_ratelimit/apigw/middleware"
"github.com/gin-gonic/gin"
"net/http"
)
func main() {
router := gin.Default()
router.Static("/public/","./public")
router.Use(middleware.IpVerify())
router.GET("/", func(context *gin.Context) {
context.Redirect(http.StatusFound,"/public/index.html")
})
router.Run(":8080")
}
四. 修改markdown图片链接脚本
package main
import (
"errors"
"fmt"
"io"
"io/ioutil"
"log"
"os"
"path/filepath"
"regexp"
"strings"
"time"
)
const (
DirLoc = "/Users/huangxuchen/dvpos/content/posts"
OldWord = "/images/blog/"
NewWord = "/public/images/blog/"
)
func replaceLogs(errorInfo string) {
file, _ := os.OpenFile("./utils/replacewords/logs.log", os.O_CREATE|os.O_RDWR|os.O_APPEND, 0666)
log.New(io.MultiWriter(file, os.Stderr), "[Failed]", log.Ldate|log.Ltime|log.Llongfile).Println(errorInfo)
}
func getFileList() ([]string, error) {
var fileList []string
// 获取所有文件路径
err := filepath.Walk(DirLoc, func(path string, info os.FileInfo, err error) error {
if info == nil {
return err
}
// 匹配以md结尾文件加入slice
isMatch, _ := regexp.MatchString("^*.md$", info.Name())
if isMatch{
fileList = append(fileList, path)
}
return nil
})
return fileList, err
}
func replaceWord(fileList []string) error {
for _, v := range fileList {
//删除原文件替换后逐行写入新文件
//file, err := os.OpenFile(v,os.O_RDWR,0766)
//if err != nil {
// return errors.New(fmt.Sprintf("failed to open file:%v,Error:%v", v, err.Error()))
//}
//reader := bufio.NewReader(file)
//defer file.Close()
删除源文件
//os.Remove(v)
//newFile, err := os.OpenFile(v,os.O_RDWR|os.O_CREATE,0766)
//if err != nil {
// return errors.New(fmt.Sprintf("failed to create new file:%v,Error:%v", v, err.Error()))
//}
//defer newFile.Close()
//for {
// line, _, err := reader.ReadLine()
// if err != nil {
// if err == io.EOF {
// break
// } else {
// return errors.New(fmt.Sprintf("failed to read file:%v,Error:%v", v, err.Error()))
// }
// }
// // 判断是否包含需要替换的内容
// if strings.Contains(string(line), OldWord){
// newLine := strings.Replace(string(line), OldWord, NewWord,-1)
// _, err = newFile.WriteString(newLine+"\n")
// if err != nil {
// return errors.New(fmt.Sprintf("failed to replace word at file:%v,Error:%v", v, err.Error()))
// }
// }else {
// _, err := newFile.WriteString(string(line)+"\n")
// if err != nil {
// return errors.New(fmt.Sprintf("failed to replace word at file:%v,Error:%v", v, err.Error()))
// }
// }
//}
// 简单粗暴(耗时短)
file, err := ioutil.ReadFile(v)
if err != nil {
return errors.New(fmt.Sprintf("failed to read file:%v,Error:%v", v, err.Error()))
}
newFile := strings.Replace(string(file), OldWord, NewWord, -1)
err = ioutil.WriteFile(v, []byte(newFile), 0)
if err != nil {
return errors.New(fmt.Sprintf("failed to replace word at file:%v,Error:%v", v, err.Error()))
}
}
return nil
}
func main() {
fmt.Println("Start to replace word...")
fileList, err := getFileList()
if err != nil {
fmt.Println(err)
return
}
//fmt.Println(fileList)
now := time.Now()
err = replaceWord(fileList)
if err != nil {
replaceLogs(err.Error())
os.Exit(1)
}
costTime := time.Now().Sub(now)
fmt.Printf("Finished! CostTime:%v",costTime)
}
五. nginx配置
worker_processes 1;
events {
worker_connections 1024;
}
http {
include mime.types;
default_type application/octet-stream;
sendfile on;
keepalive_timeout 65;
server {
listen 80;
server_name 域名1;
server_name 域名2;
charset utf-8;
rewrite ^(.*)$ https://$host$1 permanent;
}
server {
listen 443 ssl;
server_name 域名1;
server_name 域名2;
root root地址;
ssl_certificate com.pem;
ssl_certificate_key com.key;
ssl_session_cache shared:SSL:1m;
ssl_session_timeout 5m;
ssl_ciphers HIGH:!aNULL:!MD5;
ssl_prefer_server_ciphers on;
location / {
# 保存原始IP
proxy_set_header X-real-ip $remote_addr;
proxy_pass http://127.0.0.1:8080/;
}
}
}
待解决
- 编写脚本修改markdown文章的图片url(已解决)
- gin获取静态文件是否可以将单次同一文章下的多个资源请求视为一个请求(已解决过滤图片cssjs等资源请求)
- nginx配置修改,原来是基于hugo静态文件的配置(已解决)
- 视情况修改增加限制,比如增加频率策略,设置令牌,浏览器校验等
- 暂时想到这么多