+3

Bảo vệ file upload của người dùng với ví dụ website bán tranh

Ngày nay việc mọi người mua những sản phẩm điện tử trên mạng diễn ra rất thường xuyên như hình ảnh, âm thanh, phần mềm, ... . Và tất nhiên không phải ai muốn mình upload lên cái gì thì người khác cũng có thể tự do download file đó mà không ràng buộc có gì. Ví dụ như các trang web mua bán hình ảnh, âm thanh, ...

Ví dụ chúng ta có 1 website bán tranh chẳng hạn, với các yêu cầu cơ bản như sau:

  • Người dùng có thể upload ảnh lên để bán
  • Người dùng có thể mua ảnh từ người khác
  • Người dùng có thể xem lại và download ảnh mà họ đã đã mua

Chúng ta sẽ xây dựng database với mối quan hệ như sau:

Trong ví dụ này chúng ta sẽ dùng paperclip cho việc upload file:

# Gemfile
gem 'paperclip', '~> 5.0.0'

sau đó chạy bundle install.

Chúng ta sẽ đi nhanh các phần khởi tạo migration, và model:

Migration

# Migration for create_users.rb 
class CreateUsers < ActiveRecord::Migration[5.1]
  def change
    create_table :users do |t|
      t.string :email, :name
      t.timestamps
    end
  end
end
# Migration for create_images.rb
class CreateImages < ActiveRecord::Migration[5.1]
  def change
    create_table :images do |t|
      t.integer :user_id
      t.timestamps
    end
  end
end
# Migration for Paperclip attachments
class AddAttachmentToImages < ActiveRecord::Migration[5.1]
  def up
    add_attachment :images, :asset
  end

  def down
    remove_attachment :images, :asset
  end
end
# Migration for create_purchased_images.rb
class CreatePurchasedImages < ActiveRecord::Migration[5.1]
  def change
    create_table :purchased_images do |t|
      t.integer :user_id, :image_id
      t.timestamps
    end
  end
end

Đừng quên chạy lệnh rails db:migrate

Model

# app/models/user.rb
class User < ApplicationRecord

  has_many :images
  has_many :purchased_images

end
# app/models/image.rb
class Image < ApplicationRecord

  belongs_to :user

  has_attached_file :asset, styles: { thumb: "200x200>" }
  validates_attachment_content_type :asset, content_type: /\Aimage\/.*\z/

end
# app/models/purchased_image.rb
class PurchasedImage < ApplicationRecord

  belongs_to :user
  belongs_to :image

end

Upload Ảnh

Trước khi người dùng có thể bán thì tất nhiên họ phải tải ảnh lên

# config/routes.rb
resources :users do
  resources :images
end
# app/controllers/images_controller.rb
class ImagesController < ApplicationController

  def new
    @image = Image.new
  end

end
# app/views/images/new.html.erb
<h1>New Image for <%= current_user.name %></h1>

<%= form_for [current_user, @image], html: { multipart: true } do |f| %>
  <p><%= f.file_field :asset %></p>
  <p><%= f.submit %></p>
<% end %>

Danh sách ảnh đã upload của người dùng

# app/controllers/users_controller.rb
class UsersController < ApplicationController

  def index
    @users = User.all
  end

  def show
    @user = User.find(params[:id])
  end

end
# app/views/users/index.html.erb
<h1>Users</h1>

<ul>
  <% @users.each do |user| %>
    <li>
      <%= link_to "#{user.name}, #{user.images.size} images", user_path(user) %>
      <%= link_to 'Upload Image', new_user_image_path(user) if current_user == user %>
    </li>
  <% end %>
</ul>
# app/views/users/show.html.erb
<h1>Images offered by <%= @user.name %></h1>

<% @user.images.each do |image| %>
  <%= image_tag image.asset.url(:thumb) %>
<% end %>

Mua hàng

# config/routes.rb
resources :users do
  resources :images do
    post :purchase
  end
end

Chúng tra cần tạo bản ghi PurchasedImage mới

# app/views/images/show.html.erb
<h1><%= @image.asset_file_name %> offered by <%= @image.user.name %></h1>

<% unless @image.user == current_user %>
  <%= form_for [current_user, @image], url: user_image_purchase_path, method: :post do |f| %>
    <%= f.submit "Purchase" %>
  <% end %>
<% end %>

<%= image_tag @image.asset.url(:thumb) %>
# app/controllers/images_controller.rb
class ImagesController < ApplicationController
  # code omitted

  def purchase
    image = Image.find(params[:image_id])
    PurchasedImage.create(user: current_user, image: image)
    redirect_to users_path
  end

end

Purchases Link

Chúng ta muốn xem những giao dịch mua hàng nào đã được thực hiện bởi người dùng

# config/routes.rb
Rails.application.routes.draw do
  resources :users do
    get :purchases
    resources :images do
      post :purchase
    end
  end
end
# app/views/users/index.html.erb
<h1>Users</h1>

<ul>
  <% @users.each do |user| %>
    <li>
      <%= link_to "#{user.name}, #{user.images.size} images", user_path(user) %>
      <%= link_to 'Upload Image', new_user_image_path(user) if current_user == user %>
      <%= link_to "#{user.purchased_images.size} Purchased Images", user_purchases_path(user) %>
    </li>
  <% end %>
</ul>
# config/routes.rb
Rails.application.routes.draw do
  resources :users do
    get :purchases
    resources :images do
      post :purchase
      get :download
    end
  end
end
# app/controllers/users_controllers.rb
class UsersController < ApplicationController
  # code omitted

  def purchases
    @user = current_user
  end
end
<% # app/view/users/purchases.html.erb %>
<h1>Images purchased by <%= current_user.name %></h1>

<% current_user.purchased_images.each do |purchase| %>
  <%= link_to image_tag(purchase.image.asset.url(:thumb)), user_image_download_path(current_user, purchase.image) %>
<% end %>

Vậy là chúng ta đã dựng xong phần khung của website. Và giờ người dùng có thể truy cập vào những hình ảnh họ đã mua. Đã đến lúc chúng ta thêm chức năng tải xuống được kích hoạt bởi liên kết đó. Theo mặc định, Paperclip sẽ lưu trữ tệp đính kèm của bạn trong thư mục public/system trong cấu trúc tệp của ứng dụng. Điều đó có nghĩa là chỉ cần click vào link là tải xuống. Tất nhiên, chúng ta muốn bảo mật các file, để chúng chỉ có thể được tải xuống bởi những người có quyền truy cập vào chúng sau khi mua.

Bản mật và download

Với paperclip, khi khai báo file đính kèm ta làm như sau:

# app/models/image.rb
class Image < ApplicationRecord

  belongs_to :user

  has_attached_file :asset, styles: { thumb: "200x200>" }
  validates_attachment_content_type :asset, content_type: /\Aimage\/.*\z/
end

Đường dẫn mặc định để lưu file như sau: :rails_root/public/system/:class/:attachment/:id_partition/:style/:filename. Folder public chúng ta thường để những gì dùng chung và mọi người đều có thể dùng, tất nhiên là chúng ta không muốn vậy, vì vậy chúng ta cần config 1 xíu trong model:

# app/models/image.rb
class Image < ApplicationRecord

  belongs_to :user

  has_attached_file :asset, styles: { thumb: "200x200>" },
                      path: ":rails_root/secure_files/:class/:attachment/:id_partition/:style/:filename.",
  validates_attachment_content_type :asset, content_type: /\Aimage\/.*\z/
end

Giờ thì chúng ta đã có địa chỉ mới để lưu file được upload

Serving the secure images

Nôn na là chúng ta sẽ hiển thị thumbnail thay vì ảnh full size

# config/routes.rb
Rails.application.routes.draw do

  resources :users do
    get :purchases
    resources :images do
      post :purchase
      get :download
    end
  end

  get '/images/:id/display', to: "images#display", as: "secure_image_display"

end
# app/models/image.rb
class Image < ApplicationRecord

  belongs_to :user

  has_attached_file :asset, styles: { thumb: "200x200>" },
                      path: ":rails_root/secure_files/:class/:attachment/:id_partition/:style/:filename.",
                      url: "/images/:id/display"
  validates_attachment_content_type :asset, content_type: /\Aimage\/.*\z/
end
# app/controllers/images_controller.rb
  def display
    @image = Image.find(params[:id])
    send_file @image.asset.path(:thumb)
  end

Downloading Purchases

Cuối cùng nhưng không kém quan trọng là download

# app/controllers/images_controller.rb
def download
  image = Image.find(params[:id])
  send_file image.asset.path
end

Khá giống với hàm display phía trên ngoại trừ việc ảnh được tải xuống sẽ là full size như ban đầu nó được upload lên

Tài liệu tham khảo

https://chrisherring.co/posts/how-can-i-protect-a-user-s-file-uploads-in-rails


All Rights Reserved

Viblo
Let's register a Viblo Account to get more interesting posts.