Ruby on Rails :: Authentication

Posted by PunNeng, Mon Aug 07 01:40:00 UTC 2006

หลังจากที่ทำไปได้เยอะแล้ว คราวนี้จะเป็นส่วนของการ authentication(แบบง่ายๆ) โดยผมจะทำการสร้าง controller มาตัวนึง ชื่อว่า Login ซึ่ง controller ตัวนี้จะทำการ login/logout และจะรวมถึงระบบ users ด้วย โดยหน้า admin ทุกหน้า จำเป็นจะต้อง login ก่อน ถึงจะเข้ามาที่หน้านั้นๆ ได้ มาเริ่มเลย

ก่อนอื่นต้องไปสร้าง user model ก่อน ใน terminal พิมพ์ว่า

./script/generate model user

เราก็จะได้ model มาตัวนึง แล้วก็ตามด้วยการสร้างตารางบนฐานข้อมูล ตามนี้เลย

  1
  2
  3
  4
  5
  6
CREATE TABLE `users` (
`id` INT NOT NULL AUTO_INCREMENT PRIMARY KEY ,
`name` VARCHAR( 64 ) NOT NULL ,
`password` VARCHAR( 64 ) NOT NULL ,
`email` VARCHAR( 64 ) NOT NULL
)

แล้วก็มาที่ฝั่ง controller กันบ้าง สร้าง login_controller.rb ก่อน

./script/generate controller login

ตอนนี้ ในตาราง user ยังไม่มีอะไรเลย คงต้องเพิ่ม user ไปก่อนสักคน ตรงนี้ เราจะทำเหมือนๆ เดิม คือกรอก username กับ password เพื่อลงทะเบียน แต่เป็นฝ่าย admin ที่ทำการลงทะเบียนให้ พอฝั่ง login controller รับตัวแปรมา ก็เอามาจับใส่ฐานข้อมูล มาเริ่มกันที่ add_user กันก่อน ไปแก้ที่ app/controllers/login_controller.rb

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
def add_user
  if request.get?
    @user = User.new
  else
    @user = User.new(params[:user])
    if @user.save
      redirect_to_index("User #{@user.name} created")
    end
  end
end

ตรงนี้ก็เริ่มเหมือนๆ เดิมแล้ว คือตรวจ method ก่อนว่าเป็น get หรือเปล่า ถ้าเป็นแสดงว่าต้องแสดงหน้าที่ให้กรอกข้อมูล แต่ถ้าไม่ใช่ ก็แสดงว่ามันเป็น post เราจะทำการยัดมันลงในฐานข้อมูล

แล้วไปสร้าง form สำหรับกรอกข้อมูลพวกนี้ โดยสร้าง add_user.rhtml ที่ app/views/login/ ตามนี้

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
< %= error_messages_for 'user' %>
<%= form_tag %>
<table>
  <tr>
    <td>User name:</td>
    <td><%= text_field("user", "name") %></td>
  </tr>
  <tr>
    <td>Password:</td>
    <td><%= password_field("user", "password") %></td>
  </tr>
  <tr>
    <td></td>
    <td><input type="submit" value=" ADD USER " /></td>
  </tr>
</table>
<%= end_form_tag %>

ในนี้จะมีการใช้ text_field และ password_field ซึ่งเป็น form helpers ของทาง Rails บนหน้าเว็บ มันจะถูกสร้างออกมาเป็น <input /> ก็ลอง view source ดูละกันครับ

จากนั้นมา validate ข้อมูลกันต่อที่ app/models/user.rb

  1
  2
  3
  4
  5
  6
  7
  8
  9
class User < ActiveRecord::Base
  validates_uniqueness_of :name
  validates_presence_of   :name, :password

  protected
  def validate
    errors.add(:email, "is wrong.") unless self.email =~ /\A[\w\._%-]+@[\w\.-]+\.[a-zA-Z]{2,4}\z/
  end
end

คราวนี้ จะมีการสร้าง validate ขึ้นใช้เอง โดยจะตรวจจาก validate method ซึ่งจะตรวจจาก regular expression ซึ่งตัวนี้ ซึ่ง validate นี้ จะถูกเรียกจาก rails หลังจากที่มีการ save อะไรก็ตามลงในฐานข้อมูล ผมไป copy ของชาวบ้านมาใช้ ถ้ามันไม่ตรง มันจะขึ้นว่า email is wrong บนหน้าเว็บ ตามที่เราเคยทำ validation กันไปคราวนู้นนน แต่คราวนี้ method นี้มี modifier เป็น protected หมายความว่า ไม่ให้ถูกเรียกจากข้างนอกได้ แต่ถ้าเป็น object ที่อยู่ข้างใน class นั้นๆ ซึ่งเป็นชนิดเดียวกัน ก็สามารถเรียกได้ อย่าเพิ่งงง ไว้ถึงเรื่อง Ruby เต็มๆ ก่อน ผมจะมาอธิบายให้ละเอียดกว่านี้

ผมลองมองดูคร่าวๆ แล้ว คิดว่ามีส่วนที่ต้องย้อนกลับไปที่หน้า index เยอะเลย ถ้ามี method ที่พิมพ์ทีเดียว แล้วไปหน้า index เลยก็คงดี โดยที่ไม่ต้องคอยสั่ง redirect_to แล้วก็แถม flash[:notice] ไปทุกๆ ครั้ง งั้นเรามาสร้างดีกว่า ไปสร้างที่ app/controllers/application.rb

  1
  2
  3
  4
  5
  6
  7
class ApplicationController < ActionController::Base
  private
  def redirect_to_index(msg = nil)
    flash[:notice] = msg if msg
    redirect_to(:action => 'index')
  end
end

method นี้ จะถูกเรียกใช้ได้ใน class นี้ และใน class ที่ต่อเติม(extend) จาก class นี้เท่านั้น เพราะว่าเป็น private ซึ่ง method ไหนก็ตาม ที่อยู่ต่ำกว่า private ก็จะมี modifier เป็น private หมด ยกเว้นว่าจะไปเจอ modifier ตัวอื่นซะก่อน ลองย้อนๆ กลับไปดูได้ในทุกๆ controller มันจะสืบทอด(inherite) มาจาก ApplicationController ทั้งนั้น จากนั้น มาลอง add_user กันก่อน ซึงน่าจะได้ตามนี้

ถ้า email ไม่ผ่าน น่าจะขึ้นแบบนี้

login_add_user_with_email_validate

แต่ว่าเรายังไม่มี index สำหรับ login เลย กลับไปเพิ่มใน login_controller.rb ใหม่

  1
  2
  3
def index
  redirect_to :controller => "admin", :action => "index"
end

ซึ่งมันก็จะสั่ง redirect ไปที่หน้า index ของ admin นั่นเอง

หลังจากที่ทำการ add เรียบร้อยแล้ว คราวนี้ก็มาสร้างส่วนของการ login กันต่อ กลับเข้าไปที่ login_controller.rb แล้วใส่อันนี้เพิ่มเข้าไป

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
def login
  if request.get?
    session[:user_id] = nil
    @user = User.new
  else
    @user = User.new(params[:user])
    logged_in_user = @user.try_to_login
    if logged_in_user
      session[:user_id] = logged_in_user.id
      redirect_to(:action => "index")
    else
      flash[:notice] = "Invalid user/password combination"
    end
  end
end

เมื่อมันตรวจสอบได้ว่า method เป็น get ก็จะแสดงหน้า login แต่ถ้าเป็น post ก็จะทำการตรวจสอบการ login ที่ต้องมีการตรวจสอบ method เพราะว่าเราจะใช้ชื่อ action ตัวเดียวกัน ถ้าทำการ login สำเร็จ ก็จะ redirect ไปที่หน้า admin ถ้าไม่สำเร็จ ก็ย้อนกลับไปที่หน้าเดิมพร้อมทั้งขึ้นข้อความเตือน

แล้วย้อนไปสร้าง try_to_login ใน user.rb ก่อน เพราะเราเรียกใช้มันนี่ แต่เรายังไม่ได้สร้างเลย

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
class User < ActiveRecord::Base
  validates_uniqueness_of :name
  validates_presence_of   :name, :password

  def self.login(name, password)
    find(:first,
            :conditions => ["name = ? and password = ?",
            name, password])
  end

  def try_to_login
    User.login(self.name, self.password)
  end

  protected
  def validate
    errors.add(:email, "is wrong.") unless self.email =~ /\A[\w\._%-]+@[\w\.-]+\.[a-zA-Z]{2,4}\z/
  end
end

มาดูกันหน่อย self ใน Ruby ก็เหมือนๆ กับ this แต่การใช้ self.login จะหมายความว่าทำให้มันเป็น static ซึ่งสามารถเรียกได้โดยตรงโดยผ่าน class ได้เลยเช่น User.login ไม่ต้องสร้าง instance มาใหม่เพื่อเรียกใช้งาน ถ้ามันสามารถ login ได้สำเร็จ มันก็จะส่ง user ออกมา ถ้าไม่สำเร็จก็จะส่ง nil ออกมาแทน ถ้าสำเร็จแล้วมันจะทำการเก็บใส่ใน session(ย้อนกลับไปดูใน controller ก็ได้ครับ)

ต่อไป สร้างหน้า login.rhtml ซะหน่อย ยังไม่ได้สร้างอีกเช่นเคย

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
<%= form_tag %>
<table>
  <tr>
    <td>User name:</td>
    <td><%= text_field("user", "name") %></td>
  </tr>
  <tr>
    <td>Password:</td>
    <td><%= password_field("user", "password") %></td>
  </tr>
  <tr>
    <td></td>
    <td><input type="submit" value="Login" /></td>
  </tr>
</table>
<%= end_form_tag %>

แล้วลองไปที่ http://localhost:3000/login/login น่าจะได้แบบนี้

สำหรับ flash[:notice] ที่เราใส่กันไว้ จะไม่แสดง จนกว่าจะเอา layout ที่เราใส่ flash[:notice] แปะเอาไว้มาแสดง ถ้าใครใจร้อนอยากลอง ก็ลองเอา flash[:notice] มาใส่ไว้ละกัน

คราวนี้ มาสร้างตัวตรวจสอบการ login กันต่อ ใน controller ที่เราต้องการใส่ premission ให้มัน เรายังไม่ได้ใส่เลย ขั้นตอนนี้ง่ายมากๆ สำหรับ Rails มันจะมี method อยู่ตัวนึงที่ชื่อว่า before_filter มันจะถูกเรียกทุกๆ ครั้งเมื่อมีการเข้าถึง controller เราก็สร้างตัวตรวจสอบมา แล้วจัดการใส่ไว้ใน before_filter นี้ ให้มันตรวจสอบทุกๆ ครั้ง ว่ามันได้ทำการ login หรือยัง ถ้ายัง ให้กลับไปหน้า login

ใน controller ของเรา เราจะใช้ admin และ login ที่ต้องการการ authentication มาสร้างตัวตรวจสอบตรงนี้กันก่อน ไปที่ app/controllers/application.rb แก้ไข code ดังนี้

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
class ApplicationController < ActionController::Base
  def authorize
    unless session[:user_id]
      flash[:notice] = "Please log in"
      redirect_to(:controller => "login", :action => "login")
    end
  end
  private
  def redirect_to_index(msg = nil)
    flash[:notice] = msg if msg
    redirect_to(:action => 'index')
  end
end

จะมีส่วนที่เพิ่มขึ้นมาเอาไว้บน private ละกัน แล้วย้อนกลับไปที่ admin_controller.rb เพิ่ม before_filter เข้าไปส่วนหัวดังนี้

  1
  2
  3
  4
  5
  6
  7
  8
class AdminController < ApplicationController
  before_filter :authorize

  def index
      .
      .
      .
end

ทีนี้ ทุกๆ ครั้งที่มีการเข้ามาเรียกใช้ action ใน controller ตัวนี้ มันจะวิ่งเข้าไปตรวจว่า session[:user_id] ได้ถูกตั้งค่าหรือยัง ถ้ายัง ก็กลับไปหน้า login ซึ่งมันจะถูกตั้งค่าตั้งแต่ตอน login แล้ว ลองย้อนกลับไปดู code ได้

แต่ว่า ระบบ user ผมจะเอามันไว้ใน login นี่แหละ เพราะฉะนั้น ใน login_controller.rb ก็ต้องเพิ่มเข้าไปด้วย และผมต้องการใช้ layout อันเดียวกับ admin ด้วย

  1
  2
  3
  4
  5
  6
  7
  8
  9
class LoginController < ApplicationController
  layout 'admin'
  before_filter :authorize, :except => :login  

  def index
      .
      .
      .
end

แต่คราวนี้ต่างออกไป เพราะต้องทำการยกเว้น login ไว้ ไม่งั้นก็เรียกใช้ login ตอนที่ต้องการ login ไม่ได้

ยังขาด logout เพิ่มไปหน่อยที่ login_controller.rb นี่แหละ

  1
  2
  3
  4
  5
def logout
  session[:user_id] = nil
  flash[:notice] = "Logged out"
  redirect_to(:action => "login")
end

ก็ทำการสั่งให้ session[:user_id] มีค่าเป็น nil

login_login_with_layout

เราอาจจะไปเขียนเพิ่มว่า ถ้า controller เป็น login ยังไม่ต้องแสดง navigator ก็ได้ จะได้ดูดีอีกหน่อย

พอเป็นรูปเป็นร่างมาแล้ว คราวหน้า จะมาทำการตกแต่ง sidebar ซะหน่อย ให้มันเหมือนกับ blog จริงๆ

ปล. ส่วนที่เหลือ พวกระบบ user ลองไปปรับๆ แก้ไข หรือทำให้มันดีขึ้นละกันครับ ติดอะไรตรงไหนโพสทิ้งไว้ละกันครับ

แก้ไขล่าสุด วันที่ 12 กรกฏาคม 2550 เวลา 2.32 น.

Filed Under: | Tags: authentication howto ruby on rails

Comments

  1. s 09.08.07 / 16PM

    อยากทราบเกี่ยวกับการเปลี่ยน password ต้องทำยังไงค่ะ

  2. PunNeng 09.09.07 / 02AM

    คำถามตอบยากมาก ก็รับค่า password มา แล้วเอาไป update ใน DB ก็ได้ครับ ถ้าต้องการเปลี่ยน password

  3. GiggsWalk 12.24.08 / 10AM

    อธิบายได้ดีมากครับ ขอบคุณสำหรับบทความดีๆ ติดตามอยู่ตลอดนะครับ อิอิ

  4. GiggsWalk 12.24.08 / 10AM

    อธิบายได้ดีมากครับ ขอบคุณสำหรับบทความดีๆ ผมติดตามอยู่ตลอดเลยครับ อิอิ

Have your say

A name is required. You may use HTML in your comments.




codegent: we're hiring