Kotlin向AWS预签名URL二进制传图遇403错误排查
问题描述
尝试通过流将图片上传至S3预签名URL时返回403错误,但该文件通过POSTMAN可正常上传,使用Retrofit却失败,请问遗漏了什么?
用户代码实现
val stream = ByteArrayOutputStream() bitmap.compress(Bitmap.CompressFormat.JPEG, 100, stream)//bitmap is the bitmap of the image val byteArray = stream.toByteArray() val encodedImage = Base64.encodeToString(byteArray, Base64.DEFAULT) val requestBody : RequestBody = encodedImage.toRequestBody()// for base64 format val url = "https://s3.presigned_url.com" val baseUrl = url.split(".com").toTypedArray()[0] + ".com" var queryUrl: String? = url.split(".com").toTypedArray()[1] val client = OkHttpClient.Builder().build() val retrofit_image_upload: Retrofit = Retrofit.Builder() .baseUrl(baseUrl) .addConverterFactory(GsonConverterFactory.create()) .client(client) .build() var request1 = retrofit_image_upload.create(Api::class.java) try { request1.upload( url, fileSize,//size of the image requestBody, ).enqueue( object : retrofit2.Callback<Void> { override fun onResponse(call: Call<Void>, response: Response<Void>) { println(response.code().toString()) } override fun onFailure(call: Call<Void>, t: Throwable) { println(t.toString()) } } ) } catch (e: IOException) { e.printStackTrace() }
Retrofit上传接口
@PUT fun upload( @Url url: String, @Header("content-length") contentlength: Int, @Body image: RequestBody, ): Call<Void>
用户尝试将请求体转为流后问题仍存在,参考相关文章未解决。
问题分析与解决方案
出现403的核心原因是请求体格式不匹配、预签名URL使用错误,以下是具体修正点:
移除Base64编码:S3预签名PUT请求要求上传原始二进制文件,你将Bitmap转成Base64字符串后,请求体内容与Postman上传的原始文件不一致,导致签名校验失败。直接用原始字节数组创建RequestBody并指定正确MIME类型:
val requestBody = byteArray.toRequestBody("image/jpeg".toMediaTypeOrNull())修正预签名URL使用方式:Retrofit的
@Url参数可直接传入完整预签名URL,无需拆分baseUrl和路径。拆分操作会破坏预签名URL中的签名参数,导致请求无效。创建Retrofit时使用任意合法的S3 baseUrl即可:val retrofit_image_upload: Retrofit = Retrofit.Builder() .baseUrl("https://s3.amazonaws.com/") .client(client) .build()删除手动设置的Content-Length:OkHttp会自动计算并添加正确的Content-Length头部,手动设置可能与实际请求体长度不匹配,触发签名校验失败。直接移除接口中的
@Header("content-length")参数。移除GsonConverterFactory:上传二进制文件不需要Gson转换器,添加后会干扰请求体处理,导致数据格式错误。
修正后的完整代码
实现代码
val stream = ByteArrayOutputStream() bitmap.compress(Bitmap.CompressFormat.JPEG, 100, stream) val byteArray = stream.toByteArray() // 直接用原始字节数组创建RequestBody,指定MIME类型 val requestBody = byteArray.toRequestBody("image/jpeg".toMediaTypeOrNull()) val presignedUrl = "https://s3.presigned_url.com" val client = OkHttpClient.Builder().build() val retrofit_image_upload: Retrofit = Retrofit.Builder() .baseUrl("https://s3.amazonaws.com/") // 通用S3 baseUrl .client(client) .build() val request1 = retrofit_image_upload.create(Api::class.java) request1.upload(presignedUrl, requestBody).enqueue(object : retrofit2.Callback<Void> { override fun onResponse(call: Call<Void>, response: Response<Void>) { println("响应码:${response.code()}") } override fun onFailure(call: Call<Void>, t: Throwable) { println("请求失败:${t.message}") } })
Retrofit接口
@PUT fun upload( @Url url: String, @Body image: RequestBody ): Call<Void>
内容的提问来源于stack exchange,提问作者Lokesh Bisht
相关产品推荐
相关产品推荐

