Authenticate Your Rails API with JWT from Scratch
Xác thực là một trong những phần quan trọng của bất kỳ ứng dụng web. Có vô số các thư viện và các frameworks mà cung cấp tùy chọn khác nhau để thực hiện xác thực theo cách này hay cách khác. Các thư viện này lấy đi rất nhiều groundwork cần thiết để thiết lập một hệ thống xác thực, cung cấp "magic" ...
Xác thực là một trong những phần quan trọng của bất kỳ ứng dụng web. Có vô số các thư viện và các frameworks mà cung cấp tùy chọn khác nhau để thực hiện xác thực theo cách này hay cách khác. Các thư viện này lấy đi rất nhiều groundwork cần thiết để thiết lập một hệ thống xác thực, cung cấp "magic" với những gì đang xảy ra đằng sau hậu trường. Đối với Rails, chúng tôi có một số hệ thống xác thực, nổi bật là Devise.
Devise là một công cụ xác thực mà chạy như là một phần của ứng dụng của chúng tôi và làm tất cả các việc khi nói đến xác thực. Tuy nhiên, thường thì chúng ta không cần nhiều những phần nó cung cấp. Ví dụ, Devise không làm việc rất tốt với các hệ thống dựa trên API, đó là lý do tại sao chúng tôi có gem devisetokenauth. devisetokenauth là một thư viện mà những gì Devise không có, nhưng với tokens thay vì của sessions.
Hôm nay chúng ta sẽ đi tìm hiểu xây dựng hệ thống xác thực dựa trên JWT từ đầu.
Chú ý: Hướng dẫn này nhằm mục đích cho việc xác thực dựa trên API.
1. Tại sao là JWT
JWT (JSON Web Token, pronounced “jot”) là một tiêu chuẩn xác thực khép kín được thiết kế để trao đổi dữ liệu một cách an toàn giữa các hệ thống. Kể từ khi nó khép kín, nó không cần backing storage để làm việc. Ngoài ra, cách tiếp cận JWT là rất đáng tin cậy và linh hoạt, cho phép nó được sử dụng với bất kỳ khách hàng. Nó không có bất kỳ chi phí để bắt đầu và gần như tất cả các ngôn ngữ đều có thư viện mà làm cho làm việc với JWTs một dễ dàng.
Nếu bạn muốn tìm hiểu thêm về JWT hãy theo dõi bài viết tuyệt vời về JWT và làm thế nào để sử dụng nó với Rails. Nếu bạn là người mới biết đến JWT tôi muốn đề nghị bạn đọc nó đầu tiên để có được một ý tưởng về những gì chúng ta sẽ tạo ra.
2. model
Chúng tôi sẽ bắt đầu bằng việc xây dựng một model cho các ứng dụng của mình. Các ứng dụng sẽ xác thực các người dùng, vì vậy hãy tạo một model User.
rails g model user
Đây là lệnh tạo một file migration db/migrate được đặt tên XXXcreateusers, ở đây XXX là ngày hiện tại. Bạn cần tạo model User có các column như file sau:
create_table :users do |t| t.string :email, null: false t.string :password_digest, null: false t.string :confirmation_token t.datetime :confirmed_at t.datetime :confirmation_sent_at t.timestamps end
Sau đó bạn cần chạy rake db:migrate để chạy file migration vừa tạo.
Chúng ta cần thêm colunm email và password_digest ở đây, chúng là các cột cơ bản cần thiết để xác thực một user. Tất nhiên bạn có thể thêm các cột khác tùy vào ý muốn của mình. Các cột confirmation_token, confirmed_at và confirmation_sent_at cần yêu cầu user xác nhận. Bạn có thể bỏ qua điều này nếu bạn không muốn để xác nhận email.
3. Validations
Hãy thêm validations cho models/user.rb:
validates :email, presence: true, uniqueness: {case_sensitive: false} validates :email, format: { with: /@/ }
Chúng ta validate uniqueness của email với trường hợp sensitivity và làm một kiểm tra email đơn giản đảm bảo rằng string email có chứa ký tự @. Điều này có thể không phải là một solid validation, nhưng không có một tiêu chuẩn và đó là lý do tại sao chúng tôi xác minh các email sau khi đăng ký.
4. Callbacks
Chúng tôi sẽ sử dụng các hassecurepassword của Rails để xử lý password hashing.
Để bắt đầu, cần thêm gem bcrypt vào Gemfile của bạn và sau đó chạy bundle install. Sau khi gem được cài đặt thì thêm dòng dưới đây vào class User trong file models/user.rb
class User < ApplicationRecord has_secure_password ...
Hãy thêm callbacks mà chúng ta muốn thực hiện trước khi tạo user. Đầu tiên chúng ta muốn lowercase các email và loại bỏ hết các space. Thứ hai chúng ta muốn sinh một token xác nhận mà sẽ được gửi trong email đến user.
Thêm các callbacks before vào file models/user.rb:
before_save :downcase_email before_create :generate_confirmation_instructions
Chúng tôi downcase các email trước khi lưu nó vào DB. Các hướng dẫn xác nhận sẽ được tạo ra chỉ trong việc tạo ra các bản ghi user. Hãy thêm các method:
def downcase_email self.email = self.email.delete(' ').downcase end def generate_confirmation_instructions self.confirmation_token = SecureRandom.hex(10) self.confirmation_sent_at = Time.now.utc end
Nếu bạn định bỏ qua xác nhận, bạn có thể bỏ qua các method trên và gọi lại tương ứng.
5. Registration
5.1. Create
Bây giờ chúng ta cài đặt model, nên hãy thêm một endpoint cho user được tạo. Tạo controller cho users:
rails g controller users
Thêm dòng dưới vào file config/routes.rb:
resources :users, only: :create
Bây giờ chúng ta đã tạo ra UsersController củamình. Hãy chuyển qua controller (app/controllers/users_controller.rb) và thêm những dòng sau đây:
def create user = User.new user_params if user.save render json: {status: "User created successfully"}, status: :created else render json: { errors: user.errors.full_messages }, status: :bad_request end end private def user_params params.require(:user).permit :email, :password, :password_confirmation end
Chúng ta có một API endpoint để tạo một user. Bạn có thể thử nó bằng cách start server và gửi yêu cầu POST với tiêu đề user như JSON trong nội dung. Ví dụ bạn có thể post đến http://localhost:3000/users
{ user: { email: "test@example.com", password: "anewpassword", password_confirmation: "anewpassword" } }
Bạn sẽ nhận được message User được tạo thành công như tương ứng. Các request tiếp theo với giống dữ liệu nên tương ứng với message error. Bạn có thể bỏ qua những điều sau này và đi vào phần Login.
5.2. Confirmation
Một trong những điều đang chờ xử lý là confirmation user. Chúng tôi sẽ gửi email xác nhận cho user trướckhi được tạo và tạo ra một endpoint mà validates token để xác nhận user.
Để bắt đầu, Chúng ta phải gửi một email tới user khi bản ghi được tạo thành công. Chúng ta sẽ không nói đến làm thế nào để gửi email ở đây. Chúng ta nên thêm dòng gửi email sau khi user.save trong users_controller.
... if user.save #Invoke send email method here ...
Chỉ cần chắc chắn rằng bạn bao gồm user.confirmation_token trong email của bạn. Lý tưởng nhất , URL sẽ dẫn đến một endpoint mà tìm nạp token và gửi nó đến API của chúng tôi. Hãy tạo ra mà post API endpoint.
Thêm một route vào file config/routes.rb để xác nhận endpoint:
resources :users, only: :create do collection do post 'confirm' end end
Bây giờ, tạo action xác nhận trong UsersController:
def confirm token = params[:token].to_s user = User.find_by(confirmation_token: token) if user.present? && user.confirmation_token_valid? user.mark_as_confirmed! render json: {status: 'User confirmed successfully'}, status: :ok else render json: {status: 'Invalid token'}, status: :not_found end end
Hãy xem những gì chúng tôi đang làm ở đây. Đầu tiên, chúng ta lấy các token từ params, gọi to_s để xử lý các trường hợp token không được gửi trong request. Tiếp theo, lấy user tương ứng dựa trên các xác nhận token.
Nếu user tồn tại và confirmation chưa hết hạn, gọi phương thức model mark_as_confirmed! và tương ứng với message thành công. Chúng ta phải thêm các phương thức confirmation_token_valid? và mark_as_confirmed! vào model User:
def confirmation_token_valid? (self.confirmation_sent_at + 30.days) > Time.now.utc end def mark_as_confirmed! self.confirmation_token = nil self.confirmed_at = Time.now.utc save end
Phương thức confirmation_token_valid? kiểm tra nếu xác nhận đã gửi trong 30 ngày và do đó chưa hết hạn. Bạn có thể thay đổi nó với giá trị bất kỳ theo ý mình.
mark_as_confirmed! lưu thời gian xác nhận và làm vô hiệu các token xác nhận để email xác nhận cùng có thể không được sử dụng để xác nhận lại người dùng.
Bây giờ chúng ta có endpoint để xác nhận một user. Bạn có thể kiểm tra nó bằng cách gửi một request post đến endpoint users/confirm?token=<CONFIRMATION_TOKEN> và kiểm tra giá trị confirmed_at và confirmation_token của user. Bạn nên lấy User đã xác nhận thành công. Các request theo sau với cùng một token phải trả lại token không hợp lệ.
Bây giờ chúng ta có thể thực hiện đăng ký như một phần của ứngdụng.
6. Login
6.1. Controller
Hãy thêm route login vào file config/routes.rb dưới resource user:
resources :users, only: :create do collection do post 'confirm' post 'login' end end
Chúng ta đã tạo một route users/login. Trong controllers yêu cầu một vài thay đổi để lấy code trong json_web_token và tạo action tương ứng trong UsersController.
Trong controllers/application_controller.rb, thêm dòng dưới sau định nghĩa class:
require 'json_web_token'
Trong controllers/users_controller.rb thêm các dòng như sau:
def login user = User.find_by(email: params[:email].to_s.downcase) if user && user.authenticate(params[:password]) auth_token = JsonWebToken.encode({user_id: user.id}) render json: {auth_token: auth_token}, status: :ok else render json: {error: 'Invalid username / password'}, status: :unauthorized end end
Tìm hiểu phương thức này từng bước. Đầu tiên Chúng ta sẽ lấy user từ email và nếu tồn tại, gọi phương thức xác thực qua password đã được cung cấp. Phương thức xác thực đượccung cấp bằng helper has_secure_password.
Một khi chúng tôi xác minh email và mật khẩu, mã hóa id của user vào một token JWT qua phương thức mã hóa của chúng tôi từ JsonWebToken lib mà chúng ta vẫn chưa tạo ra. Sau đó, trả lại token.
Đối với những người được xác nhận email, chúng tôi có để không cho phép những người dùng không được xác nhận. Sửa đổi các hành động điều khiển bao gồm các điều kiện một hơn:
... if user && user.authenticate(params[:password]) if user.confirmed_at? auth_token = JsonWebToken.encode({user_id: user.id}) render json: {auth_token: auth_token}, status: :ok else render json: {error: 'Email not verified' }, status: :unauthorized end else ...
kiểm tra nếu trường confirmed_at không rỗng là làm việc, có nghĩa là user đã được xác nhận trước khi cho phép họ đăng nhập.
6.2. JWT Library
Bây giờ hãy thêm thư viện JWT. Bắt đầu bằng cách thêm gem dưới đây vào Gemfile của bạn và chạy bundle instal:
gem 'jwt'
Sau khi thực hiện, tạo ra một file gọi là jsonwebtoken.rb dưới lib/ và thêm những dòng này:
require 'jwt' class JsonWebToken # Encodes and signs JWT Payload with expiration def self.encode(payload) payload.reverse_merge!(meta) JWT.encode(payload, Rails.application.secrets.secret_key_base) end # Decodes the JWT with the signed secret def self.decode(token) JWT.decode(token, Rails.application.secrets.secret_key_base) end # Validates the payload hash for expiration and meta claims def self.valid_payload(payload) if expired(payload) || payload['iss'] != meta[:iss] || payload['aud'] != meta[:aud] return false else return true end end # Default options to be encoded in the token def self.meta { exp: 7.days.from_now.to_i, iss: 'issuer_name', aud: 'client', } end # Validates if the token is expired by exp parameter def self.expired(payload) Time.at(payload['exp']) < Time.now end end
Bây giờ chúng ta có chức năng login endpoint mà chúng ta có thể sử dụng để đăng nhập user. Thử gọi endpoint users/login với các dữ liệu dưới định dạng trong body yêu cầu:
{ "email": "test@example.com", "password": "anewpassword" }
Bạn sẽ thấy một response tương tự như sau:
{ "auth_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoxLCJleHAiOjE0NzUzMTM5OTQsImlzcyI6Imlzc3Vlcl9uYW1lIiwiYXVkIjoiY2xpZW50In0.5P3qJKelCdbTixnLyIrsLKSVnRLCv2lvHFpXqVKdPOs" }
Nó là token xác nhận của chúng tôi cho user. Bây giờ chúng ta có thể sử dụng token này để xác nhận từng yêu cầu cho user.
6.3. Authentication Helper
Chúng ta sẽ tạo một phương thức helper để lấy token từ header, validates các payload, và lấy các user tương ứng từ DB. Mở file /app/controllers/application_controller.rb và thêm vào như sau:
protected # Validates the token and user and sets the @current_user scope def authenticate_request! if !payload || !JsonWebToken.valid_payload(payload.first) return invalid_authentication end load_current_user! invalid_authentication unless @current_user end # Returns 401 response. To handle malformed / invalid requests. def invalid_authentication render json: {error: 'Invalid Request'}, status: :unauthorized end private # Deconstructs the Authorization header and decodes the JWT token. def payload auth_header = request.headers['Authorization'] token = auth_header.split(' ').last JsonWebToken.decode(token) rescue nil end # Sets the @current_user with the user_id from payload def load_current_user! @current_user = User.find_by(id: payload[0]['user_id']) end
Ở đây, authenticate_request! là phương thức helper mà chúng ta sẽ sử dụng để xác thực các hành động controller. Nó lấy payload từ header Authorization của request, sau đó validates payload sử dụng các phương thức valid_payload mà chúng ta đã xem ở trên. Sau khi xác nhận hợp lệ, rồi nó lấy user sử dụng user_id trong payload, tải bản g vào scope.
Chúng ta có thể thêm phương thức authenticate_request như một before_filter đến bất kỳ hành động controller mà chúng ta muốn xác thực cho user.
Kết luận:
Cảm ơn các bạn đã theo dõi bài viết, mong rằng nó sẽ giúp ích cho các bạn!
Tham khảo: https://www.sitepoint.com/authenticate-your-rails-api-with-jwt-from-scratch/