You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Cloudinary签名/无签名上传Kotlin实现及安全与应用问题

Cloudinary 上传相关问题解答

我已参考Cloudinary官方文档及相关文章,在Kotlin中实现了Cloudinary无签名上传,代码如下:

package com.example.calculatorapp

import android.app.Activity
import android.os.Bundle
import android.util.Log
import android.view.View
import android.widget.ImageView
import android.widget.Toast
import androidx.activity.result.ActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AppCompatActivity
import com.cloudinary.android.MediaManager
import com.cloudinary.android.callback.ErrorInfo
import com.cloudinary.android.callback.UploadCallback
import com.github.dhaval2404.imagepicker.ImagePicker
import com.google.firebase.firestore.ktx.firestore
import com.google.firebase.ktx.Firebase


class MainActivity : AppCompatActivity() {
    // Access a Cloud Firestore instance from your Activity
    val db = Firebase.firestore
    var config: HashMap<String, String> = HashMap()

    private lateinit var imageV: ImageView;

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        imageV = findViewById<ImageView>(R.id.imageV)
        config.put("cloud_name", BuildConfig.CLOUD_NAME);
        MediaManager.init(this, config)

        ImagePicker.with(this)
            .compress(1024)         //Final image size will be less than 1 MB(Optional)
            .maxResultSize(1080, 1080)  //Final image resolution will be less than 1080 x 1080(Optional)
            .createIntent { intent ->
                startForProfileImageResult.launch(intent)
            }
    }

    fun onDigit(view: View) {
        Toast.makeText(this, "clicked", Toast.LENGTH_LONG).show()
    }

    private val startForProfileImageResult =
        registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result: ActivityResult ->
            Log.d("test", "HEYYYY")

            val resultCode = result.resultCode
            val data = result.data

            if (resultCode == Activity.RESULT_OK) {
                //Image Uri will not be null for RESULT_OK
                val fileUri = data?.data!!
                val filePath: String = fileUri.path!!

                imageV.setImageURI(fileUri)
                Log.d("test", filePath)
                uploadToCloudinary(filePath)
            } else if (resultCode == ImagePicker.RESULT_ERROR) {
                Toast.makeText(this, ImagePicker.getError(data), Toast.LENGTH_SHORT).show()
            } else {
                Toast.makeText(this, "Task Cancelled", Toast.LENGTH_SHORT).show()
            }
        }

    private fun uploadToCloudinary(filepath: String) {
        MediaManager.get().upload(filepath).unsigned("your_upload_preset").callback(object : UploadCallback {
            override fun onSuccess(requestId: String?, resultData: MutableMap<Any?, Any?>?) {
                Log.d("cloudinary", "Task successful")

                Toast.makeText(applicationContext, "Task successful", Toast.LENGTH_LONG).show()
            }

            override fun onProgress(requestId: String?, bytes: Long, totalBytes: Long) {
                val progress = bytes.toDouble() / totalBytes
            }

            override fun onReschedule(requestId: String?, error: ErrorInfo?) {
                Log.d("cloudinary", "Task rescheduled")
            }

            override fun onError(requestId: String?, error: ErrorInfo?) {
                Log.d("cloudinary", "Task Not successful$error")
                Toast.makeText(applicationContext, "Task Not successful$error", Toast.LENGTH_LONG)
                    .show()
                if (error != null) {
                    Log.e(
                        "t",
                        "Upload failed. Error description: ${error.description}, Code: ${error.code}"
                    )
                } else {
                    Log.e("t", "Upload failed with unknown error")
                }
            }

            override fun onStart(requestId: String?) {
                Log.d("cloudinary", "Start")
                Toast.makeText(applicationContext, "Start", Toast.LENGTH_LONG).show()
            }
        }).dispatch()
    }
}

问题解答

1. 仅隐藏上传预设就能保障无签名上传的Cloudinary安全吗?

不行,仅隐藏上传预设远远不够。无签名上传的预设本质上是设计为可公开使用的(虽然不建议泄露),即使你把预设从代码中移除(比如用BuildConfig管理),仍存在以下风险:

  • 恶意用户可通过逆向工程APP获取到预设值;
  • 即使预设未泄露,若没在Cloudinary控制台配置严格的预设限制,任何人拿到预设后都能无限制上传内容。

要提升无签名上传的安全性,需配合以下操作:

  • 在Cloudinary控制台给上传预设配置严格限制:比如指定允许的文件类型、最大文件大小、限制上传数量、启用文件名唯一化(防止覆盖已有资源)、设置自动删除规则;
  • 避免在客户端代码中暴露任何敏感的Cloudinary凭证(如API密钥);
  • 对上传的内容做前端校验(如文件类型、大小),减少无效请求。

2. 签名上传的适用场景、限制能力及Kotlin实现步骤

适用场景

签名上传适用于需要严格控制上传权限和内容的场景:

  • 仅允许已登录用户上传;
  • 需要动态控制上传路径、文件名或转换规则;
  • 要做上传配额限制(比如每个用户每天最多上传5张图);
  • 防止恶意批量上传,需后端验证用户身份后再生成上传权限。

限制能力

签名上传能施加比无签名上传更精细、更强制的限制:

  • 可在签名时指定allowed_formats(仅允许特定格式文件)、bytes_limit(最大文件大小)、public_id_prefix(指定资源存储路径前缀)等参数;
  • 这些限制由签名生成时的后端逻辑控制,无法被客户端绕过,而无签名上传的限制是固定在预设中的,灵活性差。

Kotlin实现签名上传步骤

签名上传需要后端配合生成签名,前端使用签名进行上传:

步骤1:后端生成上传签名

后端需使用Cloudinary的API密钥和API Secret,生成包含timestamp及其他可选参数的签名。以Node.js为例:

const cloudinary = require('cloudinary').v2;

cloudinary.config({
  cloud_name: 'YOUR_CLOUD_NAME',
  api_key: 'YOUR_API_KEY',
  api_secret: 'YOUR_API_SECRET'
});

const generateUploadSignature = () => {
  const timestamp = Math.round(new Date().getTime()/1000);
  const signature = cloudinary.utils.api_sign_request(
    { timestamp: timestamp, allowed_formats: 'jpg,png' },
    cloudinary.config().api_secret
  );
  return { signature, timestamp };
};

后端将生成的signature、timestamp返回给Android客户端。

步骤2:Android客户端修改上传代码

在Kotlin中,替换无签名上传的逻辑,使用后端返回的签名进行上传:

// 假设已从后端获取到signature、timestamp、可选的publicId
private fun uploadToCloudinaryWithSignature(filepath: String, signature: String, timestamp: Long) {
    MediaManager.get().upload(filepath)
        .signature(signature, timestamp)
        .option("allowed_formats", "jpg,png") // 可选,需和后端签名参数对应
        .callback(object : UploadCallback {
            override fun onSuccess(requestId: String?, resultData: MutableMap<Any?, Any?>?) {
                Log.d("cloudinary", "Signed upload successful")
                Toast.makeText(applicationContext, "上传成功", Toast.LENGTH_LONG).show()
            }

            override fun onProgress(requestId: String?, bytes: Long, totalBytes: Long) {
                val progress = (bytes.toDouble() / totalBytes) * 100
                Log.d("cloudinary", "Upload progress: ${progress.toInt()}%")
            }

            override fun onReschedule(requestId: String?, error: ErrorInfo?) {
                Log.d("cloudinary", "Upload rescheduled")
            }

            override fun onError(requestId: String?, error: ErrorInfo?) {
                Log.e("cloudinary", "Signed upload failed: ${error?.description}")
                Toast.makeText(applicationContext, "上传失败: ${error?.description}", Toast.LENGTH_LONG).show()
            }

            override fun onStart(requestId: String?) {
                Log.d("cloudinary", "Signed upload started")
            }
        }).dispatch()
}

注意:签名生成时的参数(如allowed_formats)必须和客户端上传时的参数一致,否则Cloudinary会拒绝上传。

内容的提问来源于stack exchange,提问作者Starlight

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.01 02:35:56