apisix~自界说文件上传代理插件~支持form-data文件和kv参数 ...

打印 上一主题 下一主题

主题 932|帖子 932|积分 2800

参考文献

问题产生的缘故原由

后端有个文件上传服务,前端可以直接像文件上传到服务器,但这个上传服务除了有form-data文件流之外,还需要有其它key/value的表单参数,这些参数是固定的,或者有一定的规则,这时我们通过apisix代理一下,就显得更加机动和理了。
http中的multipart/form-data消息体如下

修改后的请求,是一个标准的http请求,你通过postman的codesnippet视图也可以看到,代码如下
  1. POST /mobile-server/manager/6.0.0.0.0/cdnManage/customUpload HTTP/1.1
  2. Host: api-gw-test.pkulaw.com
  3. Cookie: CookieId=b97385476b3c721c81a9163f1c8a85dd; SUB=347c9e9e-076c-45e3-be74-c482fffcc6e5; preferred_username=test; session_state=458053bd-5970-4200-9b6f-cf538ec9808b
  4. Content-Length: 508
  5. Content-Type: multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW
  6. ----WebKitFormBoundary7MA4YWxkTrZu0gW
  7. Content-Disposition: form-data; name="folder"
  8. app/icon
  9. ----WebKitFormBoundary7MA4YWxkTrZu0gW
  10. Content-Disposition: form-data; name="domain"
  11. https://static.pkulaw.com
  12. ----WebKitFormBoundary7MA4YWxkTrZu0gW
  13. Content-Disposition: form-data; name="fileName"
  14. xzcf.png
  15. ----WebKitFormBoundary7MA4YWxkTrZu0gW
  16. Content-Disposition: form-data; name="multipartFile"; filename="/C:/Users/User/Pictures/21111.png"
  17. Content-Type: image/png
  18. (data)
  19. ----WebKitFormBoundary7MA4YWxkTrZu0gW--
复制代码
开发过程中的一些坑


  • 参数拼接错误,form-data的文件流应该是第一个参数
  1. 服务端收到的请求体和参数为空
复制代码

  • 后端服务直接报错,缘故原由有以下几个


  • 有空的boundary,
  • boundary与字段之间没有\r\n换行
  • 将所有\n替换为\r\n,可能会办理上传文件和参数在接收端为空的问题
  • http请求头中的boundary是没有开头的两个减号的,这块非常容易出错,例如ngx.req.set_header("Content-Type", "multipart/form-data; boundary=" .. boundary)
  • boundary在各字段之前并不相同,需要着重看一下,一般是------开头,看看是否-的数量不同,可能接收端会有下面的错误,体现请求体拼接不精确
  1. Failed to parse multipart servlet request; nested exception is java.io.IOException: org.apache.tomcat.util.http.fileupload.FileUploadException: Stream ended unexpectedly
复制代码

  • 请求报400,大于8K的文件无法上传

    • 缘故原由:apisix中的nginx设置中,client_max_body_size默认设置为0,体现没有限制,不是它的缘故原由
    • 缘故原由:client_body_buffer_size在apisix中没有设置,它会使用默认值,默以为8K,大于8K的文件就会报错,是这个缘故原由
    • 办理:修改apisix-helm/values.yaml文件,nginx.configurationSnippet.httpStart节点添加设置client_body_buffer_size:15m
    1. nginx:
    2.     # -- Custom configuration snippet.
    3.     configurationSnippet:
    4.       main: |
    5.       httpStart: |
    6.         client_body_buffer_size 20m;
    复制代码
file-upload-proxy文件上传转发插件源码
  1. -- author: zhangzhanling
  2. -- 文件上传服务代理
  3. -- 代理前端,与文件上传服务进行通讯
  4. -- 在请求体中,添加统一的参数
  5. local core = require("apisix.core")
  6. local uuid = require("resty.jit-uuid")
  7. local ngx = require("ngx")
  8. -- 定义原数据格式
  9. local schema = {
  10.     type = "object",
  11.     properties = {
  12.         folder = {
  13.             type = "string",
  14.             description = "相对目录"
  15.         },
  16.         domain = {
  17.             type = "string",
  18.             description = "图片服务的域名"
  19.         }
  20.     }
  21. }
  22. local _M = {
  23.     version = 0.1,
  24.     priority = 1009, --数值超大,优先级越高,因为authz-keycloak是2000,它需要在authz-keycloak之后执行,所以把它定为1000,因为咱们也依赖proxy_rewrite插件
  25.     name = "file-upload-proxy",
  26.     schema = schema
  27. }
  28. local function get_specific_header(ctx, header_name)
  29.     local headers = core.request.headers(ctx)
  30.     local value = headers[header_name]
  31.     if type(value) == "table" then
  32.         return table.concat(value, ", ")
  33.     else
  34.         return value
  35.     end
  36. end
  37. -- 辅助函数:查找边界字符串
  38. local function find_boundary(content_type)
  39.     return content_type:match("boundary=([^;]+)")
  40. end
  41. function _M.rewrite(conf, ctx)
  42.     ngx.req.read_body()
  43.     local body_data = ngx.req.get_body_data()
  44.     if not body_data then
  45.         core.log.warn("Failed to read request body.")
  46.         return 400
  47.     end
  48.     local content_type = ngx.req.get_headers()["content-type"]
  49.     local boundary = find_boundary(content_type)
  50.     if not boundary then
  51.         core.log.warn("No boundary found in content type.")
  52.         return 400
  53.     end
  54.     local startBoundary = "--" .. boundary
  55.     local sub_value = get_specific_header(ctx, "sub")
  56.     local folder = conf.folder
  57.     if sub_value then
  58.         folder = folder .. "/" .. sub_value
  59.     end
  60.     ---- 构建新的请求体
  61.     local new_body = ""
  62.     local fileExt = ".jpg"
  63.     local filename = string.match(body_data, 'filename="([^"]+)"')
  64.     if filename then
  65.         -- 从filename中提取扩展名
  66.         local _, _, ext = string.find(filename, "%.([^.]+)$")
  67.         if ext then
  68.             core.log.info("文件扩展名为: " .. ext)
  69.             fileExt = "." .. ext;
  70.         end
  71.     end
  72.     -- 添加新字段
  73.     local new_fields = {
  74.         { name = "domain", value = conf.domain },
  75.         { name = "fileName", value = uuid() .. fileExt },
  76.         { name = "folder", value = folder }
  77.     }
  78.     ---- 添加新字段
  79.     for _, field in ipairs(new_fields) do
  80.         new_body = new_body .. string.format("\r\n%s\r\nContent-Disposition: form-data; name="%s"\r\n\r\n%s", startBoundary, field.name, field.value)
  81.     end
  82.     new_body = new_body .. "\r\n" .. body_data
  83.     -- 设置新的请求体
  84.     ngx.req.set_body_data(new_body)
  85.     -- 更新 Content-Type 头
  86.     ngx.req.set_header("Content-Type", "multipart/form-data; boundary=" .. boundary)
  87.     -- 计算并设置 Content-Length
  88.     local content_length = string.len(new_body)
  89.     ngx.req.set_header("Content-Length", content_length)
  90.     -- 日志输出新请求体和内容长度
  91.     core.log.warn("boundary:", boundary)
  92.     core.log.warn("New request body: ", new_body)
  93.     core.log.warn("Content-Length: ", content_length)
  94. end
  95. -- 注册插件
  96. return _M
复制代码
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。
回复

使用道具 举报

0 个回复

倒序浏览

东湖之滨

金牌会员
这个人很懒什么都没写!

标签云

快速回复 返回顶部 返回列表