Bảo vệ file upload của người dùng với ví dụ website bán tranh
Bài đăng này đã không được cập nhật trong 4 năm
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