一、操作步骤
1. 登录 边缘安全加速平台 EO 控制台,在左侧菜单栏中,进入服务总览,单击网站安全加速内需配置的站点。
2. 在站点详情页面,单击日志服务 > 实时日志。
3. 在实时日志页面,单击新建推送任务。
4. 在选择日志源页面,填写任务名称、选择日志类型、服务区域,单击下一步。
5. 在选择目的地页面,选择 HTTP 服务(POST),单击下一步。
6. 在定义推送内容页面:
·(必选)选择需推送日志的域名/四层代理实例/边缘函数实例;
·(必选)在预设字段列表中勾选需要推送的日志字段;
·(可选)添加 自定义日志字段,支持自定义日志字段名称,以及使用表达式自定义日志字段值;
·(可选)配置 推送日志筛选条件,默认推送全量日志;
·(可选)在高级配置中,配置采样比例,默认不开启采样,推送 100% 日志至目的地;
·(可选)在高级配置中,配置日志输出格式,默认格式为 JSON Lines;
·单击下一步。
7. 在目的地信息页面,填写相关目的地及参数信息。
| 参数名称 | 说明 |
| 接口地址 | 填入你的日志接收接口地址,例如:https://www.example.com/edgeone-logs |
| 内容压缩 | 为减少日志内容的大小,节约流量开销,你可以通过勾选使用 gzip 压缩日志文件开启内容压缩,EdgeOne 将会使用 gzip 格式压缩日志后再传输日志,并且会增加 HTTP 头部 Content-Encoding: gzip 来指明压缩格式。 |
| 源站鉴权 | 选择为加密鉴权时,推送日志时将携带鉴权信息供源站进行验证,保证数据来源身份的安全性。鉴权算法见:鉴权算法参考。 |
| 自定义 HTTP 请求头 | 添加需要 EdgeOne 发起请求时携带的 HTTP 头部。例如:通过添加头部 log-source: EdgeOne 来识别日志来源为 EdgeOne。通过添加头部 BatchSize: ${batchSize}来获取每次 POST 请求内推送的日志条数。说明:若你填写的头部名称为 Content-Type 等 EdgeOne 日志推送默认携带的头部,那么你填写的头部值将覆盖默认值。 |
8. 单击推送。
9. 实时日志推送任务在配置阶段为了校验接口连通性,将向接口地址发送测试数据进行验证,数据格式如下所示:
{ "ClientState": "CH-AH", "EdgeResponseTime": 366, "RequestID": "13515444256055847385", "ClientRegion": "CN", "RemotePort": 443, "RequestHost": "www.tencent.com", "RequestMethod": "GET", "RequestUrlQueryString": "-", "RequestUrl": "/en-us/about.html", "RequestProtocol": "HTTP/2.0", "EdgeServerID": "336d5ebc5436534e61d16e63ddfca327-d41d8cd98f00b204e9800998ecf8427e", "RequestTime": "2022-07-01T02:37:13Z", "EdgeCacheStatus": "-", "EdgeResponseBytes": 39430, "EdgeResponseStatusCode": 200, "ClientIP": "0.0.0.0", "RequestReferer": "https://www.tencent.com/", "RequestUA": "Mozilla/5.0 (iPhone; CPU iPhone OS 15_5 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.5 Mobile/15E148 Safari/604.1", "EdgeServerIP": "0.0.0.0", "RequestRange": "0-100/200", "EdgeInternalTime": 334, "RequestBytes": 237}
二、服务端解析日志代码示例
当你未开启源站鉴权时,可参考以下 Python 代码在服务端解析请求正文中的日志内容。
# 导入Python标准库中的模块import time # 用于获取当前时间import gzip # 用于处理gzip压缩的数据# 从http.server模块导入HTTPServer和BaseHTTPRequestHandler类from http.server import HTTPServer, BaseHTTPRequestHandlerimport json # 用于处理JSON数据格式# 定义一个继承自BaseHTTPRequestHandler的类,用于处理HTTP请求class Resquest(BaseHTTPRequestHandler): # 重写do_POST方法,该方法会在服务器接收到POST请求时被调用 def do_POST(self): # 打印请求头信息 print(str(self.headers)) # 打印HTTP请求的命令(如POST) print(self.command) # 读取请求体内容,根据请求头中的Content-Length字段确定读取的长度 req_datas = self.rfile.read(int(self.headers['content-length'])) try: # 尝试解码请求体内容并打印 print(req_datas.decode()) except Exception as e: # 如果解码过程中发生异常,打印异常信息 print(e) # 检查请求头中是否有Content-Encoding: gzip,如果有,则解压请求体 if self.headers['Content-Encoding'] == 'gzip': data = gzip.decompress(req_datas) # 打印解压后的gzip内容 print('—————decompress gzip content————————-') print(data.decode()) # 检查请求的路径是否为 '/edgeone-logs',如果不是,则返回404错误 if self.path != '/edgeone-logs': self.send_error(404, "Page not Found!") return # 如果请求路径正确,准备响应数据 data = { 'result_code': '1', 'result_desc': 'Success', 'timestamp': int(time.time()) # 响应当前时间戳 } # 发送HTTP响应状态码200,表示请求成功 self.send_response(200) # 设置响应头Content-type为application/json self.send_header('Content-type', 'application/json') # 结束响应头的发送 self.end_headers() # 将响应数据以JSON格式写入到响应体中 self.wfile.write(json.dumps(data).encode('utf-8'))# 检查当前脚本是否作为主程序运行if __name__ == '__main__': # 定义服务器监听的地址和端口,你可将9002替换为自定义端口 host = ('', 9002) # 创建HTTPServer对象,传入监听地址和端口以及处理请求的请求处理器类 server = HTTPServer(host, Resquest) # 打印服务器启动信息 print("Starting server, listen at: %s:%s" % host) # 启动服务器,使其持续运行直到外部中断 server.serve_forever()
三、请求鉴权算法
如果你在推送目的地信息中,源站鉴权内选择了加密签名,可输入你自定义配置的 SecretId 和 SecretKey,EdgeOne 将在请求 URL 中增加签名 auth_key 和 access_key,签名算法详情如下:
1. 请求 URL 构成
如下所示,请求 URL 将在?后携带 auth_key 和 access_key。
http://DomainName[:port]/[uri]?auth_key=timestamp-rand-md5hash&access_key=SecretId
参数说明:
·timestamp:请求当前时间,使用 Unix 秒级10位时间戳。
·rand:随机数。
·access_key:用于标识接口请求方的身份,即你所自定义配置的 SecretId。
·SecretKey:固定长度 32 位,即你所自定义配置的 SecretKey。
·uri:资源标识符,例如:/access_log/post。
·md5hash:md5hash = md5sum(string_to_sign),其中 string_to_sign ="uri-timestamp-rand-SecretKey"。通过 md5 算法计算出的验证串,数字0-9和小写英文字母 a-z 混合,固定长度为32个字符。
2. 计算示例
假定填入参数为:
接口地址:https://www.example.com/access_log/post
SecretId = YourID
SecretKey = YourKey
uri = /access_log/post
timestamp = 1571587200
rand = 0
string_to_sign = "/access_log/post-1571587200-0-YourKey"
基于该字符串计算出:
md5hash=md5sum("/access_log/post-1571587200-0-YourKey")=1f7ffa7bff8f06bbfbe2ace0f14b7e16
最终推送时的请求 URL 为:
https://www.example.com/access_log/post?auth_key=1571587200-0-1f7ffa7bff8f06bbfbe2ace0f14b7e16&access_key=YourID
服务端在接收到推送请求后,提取 auth_key 的值。对 auth_key 的值进行拆分,获取 timestamp 、rand 和 md5hash。可先检查 timestamp 是否过期,过期时间建议为300s,并基于上述规则拼装加密字符串,利用 SecretKey 拼装出需加密的字符串,加密后与 auth_key 中的 md5hash 值进行比较,相同则说明鉴权通过。
3. 服务端解析鉴权请求代码示例
PythonGolangimport hashlibfrom flask import Flask, requestapp = Flask(__name__)def get_rsp(msg, result={}, code=0): return { "respCode": code, "respMsg": msg, "result": result }def get_secret_key(access_key): return "secret_key"@app.route("/access_log/post", methods=['POST'])def access_log(): if request.method == 'POST': if request.content_type.startswith('application/json'): current_time_ts, rand_num, md5hash = request.args.get("auth_key").split("-") # 判断请求时间是否是在有效期内 if time.time() – int(current_time_ts) > 300: return get_rsp(msg="The request is out of time", code=-1) access_key = request.args.get("access_key") # 通过access_key(SecretId)获取secret_key secret_key = get_secret_key(access_key) raw_str = "%s-%s-%s-%s" % (request.path, current_time_ts, rand_num, secret_key) auth_md5hash = hashlib.md5(raw_str.encode("utf-8")).hexdigest() if auth_md5hash == md5hash: # 认证通过 if request.headers['content-encoding'] == 'gzip': # 解压数据 pass # 数据处理 return get_rsp("ok") return get_rsp(msg="Please use content_type by application/json", code=-1) return get_rsp(msg="The request method not find, method == %s" % request.method, code=-1)if __name__ == '__main__': app.run(host='0.0.0.0', port=8888, debug=True)package mainimport ( "context" "crypto/md5" "fmt" "log" "net/http" "os" "os/signal" "strings" "syscall")func main() { mux := http.NewServeMux() mux.Handle("/access_log/post", &logHandler{}) server := &http.Server{ Addr: ":5000", Handler: mux, } // 创建系统信号接收器 done := make(chan os.Signal) signal.Notify(done, os.Interrupt, syscall.SIGINT, syscall.SIGTERM) go func() { <-done if err := server.Shutdown(context.Background()); err != nil { log.Fatal("Shutdown server:", err) } }() err := server.ListenAndServe() if err != nil { if err == http.ErrServerClosed { log.Print("Server closed under request") } else { log.Fatal("Server closed unexpected") } }}type logHandler struct{}func (*logHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { if r.Method == "POST" { query := r.URL.Query() authKey := query.Get("auth_key") accessKey := query.Get("access_key")//access_key 即你提供的SecretId authKeys := strings.Split(authKey, "-") if len(authKeys) == 3 { currentTimeTs := authKeys[0] //进行时间戳有效期判断 RandNum := authKeys[1] md5Hash := authKeys[2] secretKey := getSecretKey(accessKey) authStr := fmt.Sprintf("%s-%s-%s-%s", "/access_log/post", currentTimeTs, RandNum, secretKey) data := []byte(authStr) has := md5.Sum(data) authMd5 := fmt.Sprintf("%x", has) //转换成字符串进行比较 if authMd5 == md5Hash { // todo 认证成功 if r.Header.Get("Content-Encoding") == "gzip" { //解压数据 } //数据处理 } } else { //异常处理 } }}// 获取SecretKeyfunc getSecretKey(accessKey string) string { if accessKey != "" { // 通过Access_key(SecretI)获取Secret_Key return "secret_key" } return ""}