Direct Browser Uploads to Alibaba Cloud OSS in ASP.NET Core
Distributed deployments present unique challenges for file storage. When applications run behind load balancers, saving uploads to local filesystems creates synchronization issues across instances. Centralized object storage eliminates this complexity while enabling horizontal scaling.
Alibaba Cloud OSS supports direct browser uploads using POST policies and temporary signatures. This approach bypasses application servers for data transfer, reducing bandwidth costs and eliminating the need for shared network storage.
Backend Credential Generation
Create a service to generate temporary upload credentials with restricted permissions:
public class OssCredentialProvider
{
private readonly string _accessKey;
private readonly string _secretKey;
private readonly string _bucket;
private readonly string _regionEndpoint;
public OssCredentialProvider(IConfiguration configuration)
{
_accessKey = configuration["Aliyun:Oss:AccessKeyId"];
_secretKey = configuration["Aliyun:Oss:AccessKeySecret"];
_bucket = configuration["Aliyun:Oss:BucketName"];
_regionEndpoint = configuration["Aliyun:Oss:Endpoint"];
}
public SecureUploadToken CreateUploadToken(int resourceType)
{
var storagePrefix = DetermineResourcePath(resourceType);
var ossClient = new OssClient(_regionEndpoint, _accessKey, _secretKey);
var expiration = DateTime.UtcNow.AddMinutes(10);
var policyConstraints = new PolicyConditions();
policyConstraints.AddConditionItem("bucket", _bucket);
policyConstraints.AddConditionItem(MatchMode.StartWith, PolicyConditions.CondKey, storagePrefix);
policyConstraints.AddConditionItem(PolicyConditions.CondContentLengthRange, 1024, 10485760);
var policyDocument = ossClient.GeneratePostPolicy(expiration, policyConstraints);
var base64Policy = Convert.ToBase64String(Encoding.UTF8.GetBytes(policyDocument));
var securitySignature = GenerateSignature(_secretKey, base64Policy);
return new SecureUploadToken
{
StoragePrefix = storagePrefix,
TargetBucket = _bucket,
CredentialId = _accessKey,
PolicyString = base64Policy,
SecurityHash = securitySignature,
UploadUrl = $"https://{_bucket}.{_regionEndpoint}"
};
}
private string DetermineResourcePath(int category)
{
var mappings = new Dictionary<int, string>
{
[1] = "content/articles",
[2] = "media/carousels",
[3] = "branding/partners",
[4] = "user-generated/attachments"
};
return mappings.TryGetValue(category, out var path)
? $"production/{path}"
: "production/misc";
}
private string GenerateSignature(string secret, string payload)
{
using var algorithm = new HMACSHA1(Encoding.UTF8.GetBytes(secret));
var signatureBytes = algorithm.ComputeHash(Encoding.UTF8.GetBytes(payload));
return Convert.ToBase64String(signatureBytes);
}
}
public class SecureUploadToken
{
public string StoragePrefix { get; set; }
public string TargetBucket { get; set; }
public string CredentialId { get; set; }
public string PolicyString { get; set; }
public string SecurityHash { get; set; }
public string UploadUrl { get; set; }
}
Front end Implementation
Construct a hidden form to submit files directly to OSS endpoints:
<form id="cloud-storage-form" method="POST" enctype="multipart/form-data" style="display:none;">
<input type="hidden" name="key" id="storage-path" />
<input type="hidden" name="bucket" id="target-bucket" />
<input type="hidden" name="OSSAccessKeyId" id="access-credential" />
<input type="hidden" name="policy" id="encoded-policy" />
<input type="hidden" name="Signature" id="request-signature" />
<input type="hidden" name="success_action_status" value="200" />
<input type="file" name="file" id="asset-selector" accept="image/jpeg,image/png,image/webp" />
</form>
Handle token acquisition and submission:
const UploadManager = {
async retrieveSecureToken(assetCategory) {
const response = await fetch(`/api/storage/token?category=${assetCategory}`);
if (!response.ok) throw new Error('Failed to obtain upload credentials');
return await response.json();
},
configureForm(tokenData) {
document.getElementById('storage-path').value = tokenData.storagePrefix;
document.getElementById('target-bucket').value = tokenData.targetBucket;
document.getElementById('access-credential').value = tokenData.credentialId;
document.getElementById('encoded-policy').value = tokenData.policyString;
document.getElementById('request-signature').value = tokenData.securityHash;
document.getElementById('cloud-storage-form').action = tokenData.uploadUrl;
},
generateUniqueKey(prefix, originalFilename) {
const timestamp = new Date().getTime();
const entropy = Math.random().toString(36).substring(2, 10);
const extension = originalFilename.substring(originalFilename.lastIndexOf('.'));
return `${prefix}/${timestamp}-${entropy}${extension}`;
},
async uploadAsset(fileInput, category) {
const token = await this.retrieveSecureToken(category);
this.configureForm(token);
const file = fileInput.files[0];
const uniquePath = this.generateUniqueKey(token.storagePrefix, file.name);
document.getElementById('storage-path').value = uniquePath;
const formElement = document.getElementById('cloud-storage-form');
const formData = new FormData(formElement);
const response = await fetch(formElement.action, {
method: 'POST',
body: formData,
mode: 'cors'
});
if (response.ok) {
return `${token.uploadUrl}/${uniquePath}`;
}
throw new Error('Upload to cloud storage failed');
}
};
// Initialize on page load
document.addEventListener('DOMContentLoaded', () => {
UploadManager.retrieveSecureToken(1).then(token => {
UploadManager.configureForm(token);
});
});
Rich Text Editor Integration
For TinyMCE integration, override the default upload handler to route through OSS:
tinymce.init({
selector: '#content-editor',
images_upload_handler: (blobInfo, progress) => {
return new Promise((resolve, reject) => {
const blob = blobInfo.blob();
const mockInput = { files: [blob] };
UploadManager.uploadAsset(mockInput, 1)
.then(url => resolve(url))
.catch(error => reject(error.message));
});
},
automatic_uploads: true,
file_picker_types: 'image'
});
Image Processing Optimization
Leverage OSS Image Service (IMG) for on-the-fly transformatoins instead of storing multiple variants:
- Responsive sizing: Append
?x-oss-process=image/resize,w_800to generate 800px width versions - Format optimization: Use
?x-oss-process=image/format,webpfor modern browser support - Watermarking: Apply
?x-oss-process=image/watermark,text_SG9zdGVkIEJ5,size_30for branding - Quality adjustment: Add
,q_80to balance quality and file size
Configure CORS rules in the OSS console to permit POST requests from you're application domain. Specify allowed origins, enable the POST method, and include Content-Type in exposed headers.