第八課:使用者認證
網頁需要辨識使用者身份,查看該使用者是否有權限能做某些事 (像是貼文、留言)
或使用某些特定的功能 (像是只有管理者能夠刪除貼文)
1. HTTP 溝通協定是無狀態,代表它不會記住使用者狀態 (登入狀態,購物車內容 etc.)
2. Server 與 Client 之間不是一直保持連線
3. 為了讓 browser 能跨 request 記住資訊,使用 session 來儲存資訊
session 資訊是由 server 端產出
4. 為了使用者體驗,session 資訊通常會放在 cookie 裡面
5. cookie 是 browser 的暫存空間,只有 4k 的大小
每一次使用者登入都會建立一個 session id
這個 session id 會被加密,然後儲存到 cookie 裡面
Client 每次發 request 時,server 就會讀取 session id
使用者登出後,該 session id 才會被銷毀
因為我是對 User 這個 Model 做驗證,因此我們需要一個 users table 來記錄使用者的資訊
class CreateUsers < ActiveRecord::Migration[5.0]
def change
create_table :users do |t|
t.string :name
t.string :email
t.timestamps
end
end
end
$rails g model user --no-test-framework
我們可以用以下指令直接產生 migration 檔與 model 檔:
接下來再編輯 migration 檔:
Rails 內建的使用者認證功能:has_secure_password
使用此功能需要安裝 bcrypt gem
# 把以下加入 Gemfile
gem 'bcrypt', '~> 3.1.7'
# 然後在 User Model 裡加上
class User < ActiveRecord::Base
has_secure_password validation: false
end
# validation: false 是避免使用者需要輸入密碼兩次
記得使用 has_secure_password 時,users table 需要加入
password_digest 欄位:
class AddPasswordDigestToUsersTable < ActiveRecord::Migration
def change
add_column :users, :password_digest, :string
end
end
在 Rails 實作 session 十分簡單:
class SessionsController < ApplicationController
def new
#登入頁面使用
end
def create
#尋找使用者
user = User.find_by(name: params[:name])
#驗證使用者,若成功,就建立一個 session,把 user_id 放入 session hash
if user && user.authenticate(params[:password])
session[:user_id] = user.id
redirect_to root_path
else
redirect_to login_path
end
end
def destroy
#登出畫面使用,刪除 session hash 裡面的 user_id
session[:user_id] = nil
redirect_to root_path
end
end
在這裡可以清楚看到,session 是一個 hash, user_id 在被加密過後,會傳至 client 端的 cookie store
加上登入與登出的 url:
# config/routes.rb
get '/login', to: 'sessions#new'
post '/login', to: 'sessions#create'
get '/logout', to: 'sessions#destroy'
登入畫面的 template:
<%= render '/shared/title', title: '使用者登入'%>
<div class="well">
<%= form_tag '/login' do |f| %>
<div class="form-group">
<%= label_tag :name %>
<br>
<%= text_field_tag :name, params[:name] %>
</div>
<div class="form-group">
<%= label_tag :password %>
<br>
<%= password_field_tag :password, params[:password] %>
</div>
<br>
<%= submit_tag "登入", class: "btn btn-success" %>
<% end %>
</div>
1. 兩者都是 Rails 用來建立表單的 helper
2. form_for 是會和 model 綁定的表單,像是對 user 或是 post 等 model 物件的新增和修改
3. form_tag 是沒有對應 model 的表單,適用像是登入、搜尋等功能
今天我若需要一個跨 controller 和 view 的方法:
# app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
# Prevent CSRF attacks by raising an exception.
# For APIs, you may want to use :null_session instead.
protect_from_forgery with: :exception
helper_method :current_user, :logged_in?
def current_user
@current_user ||= User.find(session[:user_id]) if session[:user_id]
end
def logged_in?
!!current_user
end
end
有了 logged_in? 這個 helper method 後,就可以在前端做判斷了:
# app/views/layouts/_navigation.html.erb
<nav class="navbar navbar-default">
<div class="container-fluid">
<div class="navbar-header">
<%= link_to "歡樂碼農訂便當系統", orders_path, class: 'navbar-brand' %>
</div>
<div class="collapse navbar-collapse">
<ul class="nav navbar-nav">
<% if logged_in? %>
<li><%= link_to '新增訂單', new_order_path %></li>
<li><%= link_to '登出', logout_path %></li>
<% else %>
<li><%= link_to '登入', login_path %></li>
<% end %>
</ul>
</div>
</div>
</nav>
# 若今天一個方法我需要傳進許多參數...
class Person
# 宣告方法的介面時,要寫的 code 就會變得又臭又長...
def initialize(name, age, hair_color, favorite_book, occupation)
@name = name
@age = age
@hair_color = hair_color
@favorite_book = favorite_book
@occupation = occupation
end
end
# 更麻煩的是,在使用上還必須依照順序傳入參數...
bob = Person.new("bob", 17, "black", "1984", "Salesman")
class Person
attr_accessor :name, :age, :occupation
# 可以把方法改成用 hash 傳遞參數
def initialize(options ={})
@name = options[:name] # 到hash內取值
@age = options[:age]
@hair_color = options[:hair_color]
@favorite_book = options[:favorite_book]
@occupation = options[:occupation]
end
end
hash = {
hair_color: "black",
name: "bob",
age: "1984",
occupation: "Salesman",
favorite_book: "1984"
}
# 使用時就丟 hash...
bob = Person.new(hash)
# 或這樣:
bob = Person.new(name: "bob", age: "1984", occupation: "Salesman", ...)
# app/controllers/users_controllers.rb
class UsersController < ApplicationController
def new
@user = User.new
end
def create
@user = User.new(user_params)
if @user.save
redirect_to root_path
else
render :new
end
end
private
def user_params
params.require(:user).permit(:name, :password)
end
end
# app/views/users/new.html.erb
<h4>Register</h4>
<div class="well col-md-6">
<%= form_for @user do |f| %>
<div class="form-group">
<%= f.label :name %>
<br>
<%= f.text_field :name %>
</div>
<div class="form-group">
<%= f.label :password %>
<br>
<%= f.password_field :password%>
</div>
<br>
<%= f.submit(@user.new_record? ? 'Register' : 'Update Profile', class: 'btn btn-success') %>
<% end %>
</div>
# config/routes.rb
resources :users, only: [:new, :create]
get '/register', to: 'users#new'
# config/routes.rb
resources :users, only: [:new, :create]
get '/register', to: 'users#new'
<nav class="navbar navbar-default">
<div class="container-fluid">
<div class="navbar-header">
<%= link_to '歡樂碼農訂便當系統', root_path, class: 'navbar-brand' %>
</div>
<div class="collapse navbar-collapse" id="bs-example-navbar-collapse-1">
<ul class="nav navbar-nav">
<% if logged_in? %>
<li><%= link_to '新增訂單', new_order_path %></li>
<% else %>
<li><%= link_to '註冊', register_path %></li>
<% end %>
</ul>
<ul class="nav navbar-nav navbar-right">
<% if logged_in? %>
<li class="dropdown">
<%= link_to '#', class: 'dropdown-toggle', 'data-toggle' => 'dropdown' do %>
<%= current_user.name %> <span class='caret'></span>
<% end %>
<ul class="dropdown-menu">
<li role="separator" class="divider"></li>
<li><%= link_to '登出', logout_path %></li>
</ul>
</li>
<% else %>
<li><%= link_to '登入', login_path %></li>
<% end %>
</ul>
</div>
</div>
</nav>
開始構思 + 實作期末 project
第十節課報告你要做的東西
兩個星期後展示你的作品