70 phút
Authentication và Authorization trong Rails
Authentication
Setup với bcrypt
# Gemfile
gem 'bcrypt', '~> 3.1.7'
bundle install
rails g model User name:string email:string password_digest:string
rails db:migrate
User Model
class User < ApplicationRecord
has_secure_password
validates :name, presence: true
validates :email, presence: true, uniqueness: { case_sensitive: false }
validates :password, length: { minimum: 8 }, if: -> { password.present? }
before_save { self.email = email.downcase }
def self.from_omniauth(auth)
# OAuth integration
end
end
Sessions Controller
class SessionsController < ApplicationController
def new
end
def create
user = User.find_by(email: params[:email].downcase)
if user&.authenticate(params[:password])
session[:user_id] = user.id
redirect_to root_path, notice: "Logged in successfully"
else
flash.now[:alert] = "Invalid email or password"
render :new, status: :unprocessable_entity
end
end
def destroy
session[:user_id] = nil
redirect_to root_path, notice: "Logged out"
end
end
Current User Helper
class ApplicationController < ActionController::Base
helper_method :current_user, :user_signed_in?
private
def current_user
@current_user ||= User.find_by(id: session[:user_id]) if session[:user_id]
end
def user_signed_in?
current_user.present?
end
def require_login
unless user_signed_in?
redirect_to login_path, alert: "You must be logged in"
end
end
def require_admin
unless current_user&.admin?
redirect_to root_path, alert: "Access denied"
end
end
end
Routes
Rails.application.routes.draw do
get 'login', to: 'sessions#new'
post 'login', to: 'sessions#create'
delete 'logout', to: 'sessions#destroy'
resources :users, only: [:new, :create]
resources :posts
end
Devise (Alternative)
# Gemfile
gem 'devise'
bundle install
rails g devise:install
rails g devise User
rails db:migrate
Devise Views
rails g devise:views
Devise Configuration
# config/initializers/devise.rb
config.password_length = 8..128
config.timeout_in = 30.minutes
config.confirm_within = 3.days
Protected routes
class PostsController < ApplicationController
before_action :authenticate_user!
before_action :set_post, only: [:show, :edit, :update, :destroy]
def index
@posts = current_user.posts
end
end
Authorization với Pundit
# Gemfile
gem 'pundit'
bundle install
rails g pundit:install
Application Policy
class ApplicationPolicy
attr_reader :user, :record
def initialize(user, record)
@user = user
@record = record
end
def index?; false; end
def show?; false; end
def create?; false; end
def new?; create?; end
def update?; false; end
def edit?; update?; end
def destroy?; false; end
private
def admin?
user&.admin?
end
def owner?
user && record.respond_to?(:user_id) && record.user_id == user.id
end
class Scope
def initialize(user, scope)
@user = user
@scope = scope
end
def resolve
raise NotImplementedError
end
private
attr_reader :user, :scope
end
end
Post Policy
class PostPolicy < ApplicationPolicy
def index?; true; end
def show?; record.published? || owner? || admin?; end
def create?; user.present?; end
def update?; owner? || admin?; end
def destroy?; owner? || admin?; end
class Scope < ApplicationPolicy::Scope
def resolve
if user&.admin?
scope.all
elsif user
scope.where(published: true).or(scope.where(user_id: user.id))
else
scope.where(published: true)
end
end
end
end
Sử dụng trong Controller
class PostsController < ApplicationController
before_action :authenticate_user!, except: [:index, :show]
def index
@posts = policy_scope(Post)
end
def show
@post = Post.find(params[:id])
authorize @post
end
def edit
@post = Post.find(params[:id])
authorize @post
end
def create
@post = current_user.posts.build(post_params)
authorize @post
if @post.save
redirect_to @post
else
render :new
end
end
private
def post_params
params.require(:post).permit(:title, :body, :published)
end
end
Roles
Simple enum-based roles
# Migration
add_column :users, :role, :string, default: 'user'
add_index :users, :role
# Model
class User < ApplicationRecord
ROLES = %w[user moderator admin].freeze
validates :role, inclusion: { in: ROLES }
ROLES.each do |r|
define_method "#{r}?" do
role == r
end
end
end
Sử dụng trong views
<% if current_user&.admin? %>
<%= link_to "Admin", admin_path %>
<% end %>
<% if policy(@post).edit? %>
<%= link_to "Edit", edit_post_path(@post) %>
<% end %>
JWT cho API
# Gemfile
gem 'jwt'
# app/services/json_web_token.rb
class JsonWebToken
SECRET_KEY = Rails.application.secret_key_base
def self.encode(payload, exp = 24.hours.from_now)
payload[:exp] = exp.to_i
JWT.encode(payload, SECRET_KEY)
end
def self.decode(token)
decoded = JWT.decode(token, SECRET_KEY).first
HashWithIndifferentAccess.new(decoded)
rescue JWT::ExpiredSignature, JWT::DecodeError
nil
end
end
# app/controllers/api/v1/auth_controller.rb
module Api
module V1
class AuthController < ApplicationController
skip_before_action :verify_authenticity_token
def login
user = User.find_by(email: params[:email]&.downcase)
if user&.authenticate(params[:password])
token = JsonWebToken.encode(user_id: user.id)
render json: { token: token, user: user.as_json(except: :password_digest) }
else
render json: { error: "Invalid credentials" }, status: :unauthorized
end
end
def register
user = User.new(user_params)
if user.save
token = JsonWebToken.encode(user_id: user.id)
render json: { token: token, user: user.as_json(except: :password_digest) },
status: :created
else
render json: { errors: user.errors.full_messages }, status: :unprocessable_entity
end
end
private
def user_params
params.require(:user).permit(:name, :email, :password)
end
end
end
end
# app/controllers/api/v1/base_controller.rb
module Api
module V1
class BaseController < ApplicationController
before_action :authenticate_request
attr_reader :current_user
private
def authenticate_request
header = request.headers['Authorization']
token = header.split(' ').last if header
decoded = JsonWebToken.decode(token)
@current_user = User.find(decoded[:user_id]) if decoded
unless @current_user
render json: { error: 'Unauthorized' }, status: :unauthorized
end
end
end
end
end
Bài tập thực hành
Hãy implement authentication + authorization!