Fading Coder

One Final Commit for the Last Sprint

Home > Tech > Content

Building a Blog with Image Upload Using Spring Boot and Vue.js

Tech May 13 2

This article provides a complete solution for implementing image upload, storage, and display in a blog built with Spring Boot and Vue.js. It covers configuring an image server on a cloud instance, the backend API, the frontend component, and database storage.

1. Image Server Setup (Ubuntu)

1.1 Option Selection

  • Self-hosted image server: Use Nginx to serve static resources.
  • Cloud storage service: Use object storage like Alibaba Cloud OSS (recommended for CDN acceleration).

1.2 Steps for Self-hosted Image Server

# Install Nginx
sudo apt update && sudo apt install nginx

# Create directory for images
sudo mkdir -p /var/www/images/blog
sudo chmod -R 777 /var/www/images/blog

# Configure Nginx (/etc/nginx/sites-available/default)
server {
    listen 80;
    server_name your-domain.com;
    
    location /images/ {
        root /var/www;
        autoindex on; # allow directory listing (testing only, disable in production)
        expires 30d; # caching optimization
    }
}

# Restart Nginx
sudo systemctl restart nginx

Images will be accessible via http://server-ip/images/blog/filename.

1.3 Security Optimizations

  • Use HTTPS (free certificate from Let's Encrypt).
  • Restrict file types: Add if ($request_filename ~* \.(php|jsp)) { deny all; } in the Nginx config.
  • Hotlinking protection: Add valid_referers blocked server_names; if ($invalid_referer) { return 403; }.

2. Spring Boot Backend

2.1 File Upload Endpoint

@RestController
@RequestMapping("/api/upload")
public class UploadController {
    
    @Value("${file.upload-dir}")
    private String uploadPath;

    @PostMapping("/image")
    public ResponseEntity<Map<String, String>> uploadImage(
            @RequestParam("file") MultipartFile file) {
        
        // Generate unique filename
        String originalName = file.getOriginalFilename();
        String fileExt = FilenameUtils.getExtension(originalName);
        String newFileName = UUID.randomUUID() + "." + fileExt;
        
        // Save file
        Path targetPath = Paths.get(uploadPath).resolve(newFileName);
        try {
            Files.copy(file.getInputStream(), targetPath, StandardCopyOption.REPLACE_EXISTING);
        } catch (IOException e) {
            throw new RuntimeException("File upload failed");
        }
        
        // Return accessible URL
        String imageUrl = "https://your-domain.com/images/blog/" + newFileName;
        return ResponseEntity.ok(Collections.singletonMap("url", imageUrl));
    }
}

2.2 Configuration for Upload Path

# application.yml
file:
  upload-dir: /var/www/images/blog

2.3 Cross-Origin Configuration

@Configuration
public class WebConfig implements WebMvcConfigurer {
    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/**")
                .allowedOrigins("http://localhost:8080") // Vue frontend URL
                .allowedMethods("*");
    }
}

3. Vue.js Frontend

3.1 Image Upload Component (Using Element Plus)

<template>
  <el-upload
    action="#"
    :http-request="customUpload"
    :show-file-list="false"
    :before-upload="beforeUpload">
    <el-button type="primary">Upload Image</el-button>
  </el-upload>
</template>

<script setup>
import axios from 'axios';
import { ElMessage } from 'element-plus';

const customUpload = async ({ file }) => {
  const formData = new FormData();
  formData.append('file', file);
  
  try {
    const res = await axios.post('/api/upload/image', formData, {
      headers: { 'Content-Type': 'multipart/form-data' }
    });
    // Insert returned URL into rich text editor
    editor.insertEmbed(editor.getSelection().index, 'image', res.data.url);
  } catch (err) {
    console.error('Upload failed', err);
    ElMessage.error('Upload failed');
  }
};

const beforeUpload = (file) => {
  const isImage = ['image/jpeg', 'image/png'].includes(file.type);
  const isLt5M = file.size / 1024 / 1024 < 5;
  
  if (!isImage) {
    ElMessage.error('Only JPG/PNG formats are supported');
    return false;
  }
  if (!isLt5M) {
    ElMessage.error('Image size must be less than 5MB');
    return false;
  }
  return true;
};
</script>

3.2 Rich Text Editor Integration (Using Quill)

// Initialize the editor
const editor = new Quill('#editor', {
  modules: {
    toolbar: [
      ['bold', 'image'] // Add image button
    ]
  }
});

// Listen for image button click
editor.getModule('toolbar').addHandler('image', () => {
  document.querySelector('.el-upload input').click();
});

4. Database Storage

4.1 Blog Table Structure

CREATE TABLE blog (
  id INT PRIMARY KEY AUTO_INCREMENT,
  title VARCHAR(255) NOT NULL,
  content LONGTEXT NOT NULL, -- Stores HTML containing <img src="URL">
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

4.2 JPA Entity

@Entity
public class Blog {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    private String title;
    
    @Lob // for large text
    private String content;
    
    @CreationTimestamp
    private LocalDateTime createdAt;
    
    // getters and setters
}

5. Deployment Considerations

  1. Server permissions: Ensure the Spring Boot process has write permissions: sudo chown -R springboot_user:springboot_user /var/www/images.
  2. Periodic cleanup: Set up a cron job to delete unused images older than 30 days.
  3. Backup strategy: Use rsync to synchronize the image directory to a backup server.
  4. Monitoring and alerting: Monitor storage usage with Prometheus.

Extension: Using Alibaba Cloud OSS

  1. Create an OSS bucket, obtain endpoint and access keys.
  2. Add the SDK dependency:
<dependency>
    <groupId>com.aliyun.oss</groupId>
    <artifactId>aliyun-sdk-oss</artifactId>
    <version>3.10.2</version>
</dependency>
  1. Modify the upload logic:
// Create OSS client
OSS ossClient = new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret);

// Upload file stream
ossClient.putObject(bucketName, objectName, new ByteArrayInputStream(file.getBytes()));

// Generate signed URL (valid for 1 hour)
String url = ossClient.generatePresignedUrl(bucketName, objectName, 
    new Date(System.currentTimeMillis() + 3600 * 1000)).toString();

The cloud storage option provides automatic scaling, CDN aceleration, and access statistics, making it suitable for high-traffic scenarios.

Both the self-hosted and cloud storage approaches follow the same flow: frontend upload → backend processing → URL storage → content rendering, achieving a complete image management feature.

Related Articles

Understanding Strong and Weak References in Java

Strong References Strong reference are the most prevalent type of object referencing in Java. When an object has a strong reference pointing to it, the garbage collector will not reclaim its memory. F...

Comprehensive Guide to SSTI Explained with Payload Bypass Techniques

Introduction Server-Side Template Injection (SSTI) is a vulnerability in web applications where user input is improper handled within the template engine and executed on the server. This exploit can r...

Implement Image Upload Functionality for Django Integrated TinyMCE Editor

Django’s Admin panel is highly user-friendly, and pairing it with TinyMCE, an effective rich text editor, simplifies content management significantly. Combining the two is particular useful for bloggi...

Leave a Comment

Anonymous

◎Feel free to join the discussion and share your thoughts.