Rails – Xác thực user – Phần 1

4.3/5 - (36 votes)

Trong phần này chúng ta sẽ tạo cấu trúc lưu trữ thông tin user (người dùng).

Ở đây chúng ta chỉ lưu trữ đơn giản, bao gồm username và password, nhưng password sẽ không được lưu bình thường mà chúng ta sẽ mã hóa bằng thuật toán SHA2, do đó password trong CSDL sẽ gồm 2 thành phần là hashed (mật khẩu đã được mã hóa) và salt (giá trị salt).

Nếu bạn chưa biết về kiểu mã hóa này thì có thể hiểu đơn giản là khi người dùng gửi mật khẩu lên để đăng ký, chúng ta sẽ tạo ra một ký tự chuỗi ngẫu nhiên có độ dài cố định gọi là salt, sau đó dùng một thuật toán mã hóa (ở đây là SHA2) để mã hóa chuỗi mật khẩu với chuỗi salt để cho ra chuỗi hashed hay còn gọi là mật khẩu đã được mã hóa. Về thuật toán SHA2 thì ở đây chúng ta dùng thư viện, nếu bạn muốn biết chi tiết cách hoạt động thì có thể tìm hiểu trên mạng.

Định nghĩa model user

Đầu tiên chúng ta tạo model rồi cập nhật lên CSDL:

C:\Projects\Rails\depot>rails generate scaffold user name:string hashed_password:string salt:string
...
C:\Projects\Rails\depot>rake db:migrate

Bảng user của chúng ta sẽ gồm 3 trường là name, hashed_passwordsalt.

File user.rb được tạo ra, chúng ta sửa lại file này như sau:

require 'digest/sha2'

class User < ActiveRecord::Base
    validates :name, :presence => true, :uniqueness => true
    validates :password, :confirmation => true
    attr_accessor :password_confirmation
    attr_reader :password 
 
    validate :password_must_be_present
 
    def User.encrypt_password(password, salt) 
        Digest::SHA2.hexdigest(password + salt)
    end
 
    def password=(password) 
        @password = password
 
        if password.present?
            generate_salt
            self.hashed_password = self.class.encrypt_password(password, salt)
        end
    end
 
    def User.authenticate(name, password) 
        if user = find_by_name(name)
            if user.hashed_password == encrypt_password(password, user.salt)
               user
            end
        end
    end

private
    def password_must_be_present 
        if hashed_password.present? == false
            errors.add(:password, "Missing password")
        end
    end
 
    def generate_salt
        self.salt = self.object_id.to_s + rand.to_s
    end
end

Lớp này sẽ hơi phức tạp một chút.

require 'digest/sha2'

Ngay dòng đầu tiên chúng ta gọi thư viện SHA2, đây là thư viện có sẵn trong Ruby.

validates :name, :presence => true, :uniqueness => true
validates :password, :confirmation => true

Chúng ta kiểm tra xem biến name có tồn tại hay không, và name không được trùng nhau. Ngoài ra biến password phải được nhập 2 lần và cả 2 phải giống nhau, tức là khi chúng ta đăng ký thì người dùng gõ mật khẩu, sau đó phải gõ lần nữa để xác nhận.

attr_accessor :password_confirmation
attr_reader :password 

attr_accessor là phương thức tạo nhanh GetterSetter của Ruby, ở đây là biến password_confimation, tương tự attr_reader tạo Getter cho biến password.

validate :password_must_be_present

Ngoài ra chúng ta còn kiểm tra xem biến hashed_password có tồn tại hay không, tức là kiểm tra xem mật khẩu đã được mã hóa hay chưa. Chúng ta kiểm tra bằng cách định nghĩa phương thức password_must_be_present:

private
    def password_must_be_present 
        if hashed_password.present? == false
            errors.add(:password, "Missing password")
        end
    end

Nếu chưa có thì chúng ta cho báo lỗi.

Tiếp theo chúng ta định nghĩa hàm thực hiện mã hóa password:

    def User.encrypt_password(password, salt) 
        Digest::SHA2.hexdigest(password + salt)
    end
.
.
.
private
.
.
.
    def generate_salt
        self.salt = self.object_id.to_s + rand.to_s
    end

Hàm encrypt_password sẽ gọi hàm Digest::SHA2.hexdigest và nhận vào tham số là passwordsalt. Kết quả sẽ cho ra một chuỗi 64 kí tự (256 bit). Salt sẽ được tạo từ hàm generate_salt, chúng ta cho tạo chuỗi salt từ id của chính đối tượng User đó rồi nối với một chuỗi số ngẫu nhiên tạo từ hàm rand.

def password=(password) 
    @password = password
 
    if password.present?
        generate_salt
        self.hashed_password = self.class.encrypt_password(password, salt)
    end
end

Chúng ta định nghĩa lại toán tử = trên biến password, để khi người dùng gửi password lên, thì password sẽ không được gán thẳng vô biến mà sẽ được mã hóa lại rồi mới lưu vào CSDL.

def User.authenticate(name, password) 
    if user = find_by_name(name)
        if user.hashed_password == encrypt_password(password, user.salt)
           user
        end
    end
end

Chúng ta định nghĩa phương thức authenticate để xác thực người dùng, khi người dùng đăng nhập với namepassword, chúng ta mã hóa password đó với chuỗi salt của người dùng lưu trong CSDL và kiểm tra xem chuỗi vừa được mã hóa có giống như chuỗi đã được mã hóa trong CSDL không, nếu giống thì trả về đối tượng User, tức là đăng nhập thành công.

Tùy chỉnh controller

Chúng ta sửa lại một số phương thức trong lớp UsersController như sau:

class UsersController < ApplicationController
    .
    .
    .
    # POST /users
    # POST /users.json
    def create 
        @user = User.new(user_params)
 
        respond_to do |format|
            if @user.save 
                format.html { redirect_to users_url, notice: 'User was successfully created.' }
                format.json { render :show, status: :created, location: @user }
            else 
                format.html { render :new }
                format.json { render json: @user.errors, status: :unprocessable_entity }
            end
        end
    end
    .
    .
    .
    # PATCH/PUT /users/1
    # PATCH/PUT /users/1.json
    def update
        respond_to do |format|
            if @user.update(user_params)
                format.html { redirect_to users_url, notice: 'User was successfully updated.' }
                format.json { render :show, status: :ok, location: @user }
            else
                format.html { render :edit }
                format.json { render json: @user.errors, status: :unprocessable_entity }
            end
        end
    end
    .
    .
    .
    # Never trust parameters from the scary internet, only allow the white list through.
    def user_params
        params.require(:user).permit(:name, :hashed_password, :salt, :password, :password_confirmation)
    end
end

Chúng ta sửa lại 3 phương thức là update, createuser_params.

Hai phương thức updatecreate thì chúng ta chỉ sửa đơn giản là cho redirect về trang /users thôi, thay vì trang /users/<id>.

Còn phương thức user_params thì chúng ta sửa lại phương thức params.requrie().permit cho nhận thêm tham số passwordpassword_confirmation. Khi người dùng nhập form trên trình duyệt, sau đó bấm nút gửi thì trình duyệt sẽ gửi lệnh HTTP kèm theo các tham số lên server, các tham số đó sẽ nằm trong biến params, tùy nhiên mặc định Rails sẽ không chấp nhận các tham số được gửi lên vì lý do bảo mật, do đó nếu muốn một tham số nào được nhận thì chúng ta phải khai báo trong phương thức permit(). Trong số các tham số gửi lên khi đăng ký tài khoản có một tham số tên là user, tham số này lại gồm 3 tham số khác là name, passwordpassword_confirmation.

Mặc định Rails chỉ nhận các tham số có tên trùng với tên trong model thôi, tức là chỉ có name, hashed_passwordsalt, nhưng khi đăng ký tài khoản thì người dùng đâu có nhập hashed và salt mà chỉ nhập password bình thường. Do đó chúng ta phải đưa thêm tham số passwordpassword_confirmation thì Rails mới cho phép “đi qua” controller.

Tùy chỉnh view

Đầu tiên chúng ta sửa file index.html.erg trong thư mục app/views/users như sau:

<p id="notice"><%= notice %></p>

<h1>Listing Users</h1>

<table>
    <thead>
        <tr>
            <th>Name</th>
            <th></th>
            <th></th>
            <th></th>
        </tr>
    </thead>

    <tbody>
        <% @users.each do |user| %>
        <tr>
            <td><%= user.name %></td> 
            <td><%= link_to 'Show', user %></td>
            <td><%= link_to 'Edit', edit_user_path(user) %></td>
            <td><%= link_to 'Destroy', user, method: :delete, data: { confirm: 'Are you sure?' } %></td>
        </tr>
        <% end %>
    </tbody>
</table>

<br>

<%= link_to 'New User', new_user_path %>

Mặc định file này hiển thị cả giá trị hashed và salt nữa, ở đây chúng ta bỏ các dòng đó đi.

Tiếp theo chúng ta sửa file _form.html.erb trong thư mục app/views/users như sau:

<div class="depot_form">

    <%= form_for @user do |f| %>
        <% if @user.errors.any? %>
            <div id="error_explanation">
                <h2><%= pluralize(@user.errors.count, "error") %> prohibited this user from being saved: </h2>
                <ul>
                    <% @user.errors.full_messages.each do |msg| %>
                        <li><%= msg %></li>
                    <% end %>
                </ul>
            </div>
        <% end %>
 
        <fieldset>
        <legend>Enter User Details</legend>
 
        <div>
            <%= f.label :name %>:
            <%= f.text_field :name, :size => 40 %>
        </div>
 
        <div>
            <%= f.label :password, 'Password' %>:
            <%= f.password_field :password, :size => 40 %>
        </div>
  
        <div>
            <%= f.label :password_confirmation, 'Confirm' %>:
            <%= f.password_field :password_confirmation, :size => 40 %>
        </div>
 
        <div>
            <%= f.submit %>
        </div> 
        </fieldset>
    <% end %>
</div>

Chúng ta sửa lại để hiển thị theo các lớp CSS chúng ta đã định nghĩa.

Bây giờ chúng ta có thể trỏ đăng ký tài khoản được rồi:

capture

0 0 votes
Article Rating
Subscribe
Thông báo cho tôi qua email khi
guest

This site uses Akismet to reduce spam. Learn how your comment data is processed.

1 Comment
Inline Feedbacks
View all comments
Hùng
Hùng
6 năm trước

Em vẫn chưa hiểu lắm ở một vấn đề mong được anh giải đáp sớm.Đó là cái strong Parameter này. Câu hỏi 1: Ví dụ, khi ở form_for @user do |f| nhận biến @user từ controler @user = User.new truyền sang. Thì các giá trị symbol sử dụng trong view ở : text_field , :password_filed phải trùng với tên của trường Model đã khai báo? Tại sao chúng ta không sử dụng form_for :user do |f| ( Sử dụng Symbol :user thay vì @user vì bản chất @user = User.new từ Controler truyền sang nó cũng rỗng mà). Và khi… Đọc thêm »