JWT Authentication in Rails
JSON Web Token (JWT) Authentication là một cách thức nhỏ gọn và an toàn để thể hiện claims được chuyển giao giữa client và server. Các claims trong JWT được mã hóa dưới dạng JSON và được sử dụng làm trọng tải cho cấu trúc JSON Web Signature (JWS) hoặc là bản rõ của cấu trúc SON Web Encryption ...
JSON Web Token (JWT) Authentication là một cách thức nhỏ gọn và an toàn để thể hiện claims được chuyển giao giữa client và server. Các claims trong JWT được mã hóa dưới dạng JSON và được sử dụng làm trọng tải cho cấu trúc JSON Web Signature (JWS) hoặc là bản rõ của cấu trúc SON Web Encryption (JWE), cho phép các claims được kỹ thuật số hóa hay được bảo vệ toàn vẹn với một Message Authentication Code (MAC) và/hoặc encrypted.
JWT tokens sẽ được encrypted thành ba phần:
- The header: meta-data mô tả thuật toán mã hóa và loại mã thông báo
- The payload: dữ liệu thực tế liên quan đến người dùng (id, email, v.v.)
- The signature: sự kết hợp đặc biệt giữa header và payload để đảm bảo xác nhận người gửi là chính xác
Sau đây là một ví dụ đơn giản:
Ta có thông tin như sau:
- {user_id: 1}
- hmac secret: $39asdulawk3j489us39vm9370dmsZ
- encryption algorithm: HS256
Chúng ta có thể mã hóa như sau:
require 'jwt' JWT.encode( {user_id: 1}, hmac, "H256")
Và nó sẽ trả về ba phần JWT như sau:
QyJ0asdfjos.ald925lIn0.eyJ0ZXN0Ijas39uZGF0YSJ9.
Tương tự, để giải mã thông tin trên, chúng ta có thể làm như sau:
JWT.decode(token, hmac, "H256") => [ {"user_id"=>"1"}, {"typ"=>"JWT", "alg"=>"HS256"} ]
Như vậy, có thể thấy nó dễ dàng được triển khai ở trong Ruby, vậy còn Rails thì sao??
Ở trong Rails 5, giả sử rằng chúng ta có một Model User, chúng ta sẽ sử dụng gem bcrypt để băm mật khẩu, do vậy, chúng ta cần thêm has_secure_password method vào Model User
Vì chúng ta sử dụng encode và decode JWTs khá nhiều, nên sẽ thuận tiện hơn nếu viết một class bao gồm các chức năng đó. Và để class này (goi là Auth) pr trong thư mục lib/
- Đầu tiên chúng ta thêm gem jwt vào Gemfile
# Gemfile gem 'jwt'
- Tạo một file với đường dẫn lib/auth.rb
- Sau đó thêm đường dẫn tự động tải trong config/application.rb
config.autoload_paths << Rails.root.join('lib')
Tại class Auth chúng ta xác định 2 method .issue chịu trách nhiệm tạo JWT từ thông tin người dùng nhất định và .decode sẽ giải mã JWT
require 'jwt' class Auth def self.issue(payload) end def self.decode(token) end end
Chúng ta sẽ bắt đầu với method .issue
Tạo JWT: Auth.issue
Phương pháp này chỉ đơn giản là bọc method JWT.encode mà gem jwt tạo sẵn cho chúng ta. Phương thức này có ba đối số:
- Dữ liệu ở dạng băm, chúng ta sẽ mã hóa trong JWT
- Chìa khóa cho thuật toán băm của bạn
- Các loại thuật toán băm
Ví dụ:
payload = {name: "sophie"} secret_key = "masd82348$asldfja" algorithm = "HS256" JWT.encode(payload, secret_key, algorithm) => "esdiva23euihrusdfcnkjz2snciusdhuihr7480y2qikjh8"
Vấn đề đặt ra là làm thế nào chúng ta sẽ tạo ra một khóa bí mật?
Tạo khóa băm
Chúng ta sẽ sử dụng module Digest có sẵn từ Ruby để tạo khóa bí mật. Chúng ta sẽ tạo khóa trên command, trong bảng điều khiển Pry và thêm nó vào môi trường của chúng ta dưới dạng một biến môi trường. Chúng ta sẽ sử dụng Figaro để đảm bảo rằng biến môi trường không bị đẩy lên GitHub.
$ pry pry > Digest::SHA256.digest('beyonce') => "x85x11xFAxEFxF2Ax11xC7x90x9C! {xDCx11WxFBx93xE5xA3xCDxE3xC2x9E#7xC4xCDaxCFxC9/xEA"
Chúng ta sẽ thêm nó vào application.yml như thế này:
# config/application.yml AUTH_SECRET: x85x11xFAxEFxF2Ax11xC7x90x9C! {xDCx11WxFBx93xE5xA3xCDxE3xC2x9E#7xC4xCDaxCFxC9/xEA
Bây giờ chúng ta có thể hoàn thiện method Auth.issue như sau:
# lib/auth.rb require 'jwt' class Auth ALGORITHM = 'HS256' def self.issue(payload) JWT.encode( payload, auth_secret, ALGORITHM) end def self.decode(token) end def self.auth_secret ENV["AUTH_SECRET"] end end
Chúng ta đã hoàn thành phương thức mã hóa, bây giờ sẽ đến phương thức giải mã
Giải mã JWT: Auth.decode
Method Auth.decode là bọc method JWT.decode mà gem jwt tạo sẵn. Phương thức này có ba đối số:
- JWT muốn giải mã,
- Khóa bí mật của thuật toán băm
- Các loại thuật toán băm
# lib/auth.rb require 'jwt' class Auth ALGORITHM = 'HS256' def self.issue(payload) JWT.encode( payload, auth_secret, ALGORITHM) end def self.decode(token) JWT.decode(token, auth_secret, true, { algorithm: ALGORITHM }).first end def self.auth_secret ENV["AUTH_SECRET"] end end
Giờ chúng ta đã sẵn sàng sử dụng class Auth trong controller
Đầu tiên chúng ta cần thêmmột đường dẫn tới Sessions#create
# config/routes.rb ... post '/login', to: "sessions#create"
Giờ chúng ta sẽ xây dưng action create trong Session_controller:
# app/controllers/sessions_controller.rb class SessionsController < ApplicationController skip_before_action :authenticate def create user = User.find_by(email: auth_params[:email]) if user.authenticate(auth_params[:password]) jwt = Auth.issue({user: user.id}) render json: {jwt: jwt} else end end private def auth_params params.require(:auth).permit(:email, :password) end end
Lưu ý rằng hiện tại chúng ta đang mã hóa ID người dùng, không phải email và mật khẩu. Điều này là do ID người dùng là một định danh duy nhất và không phải là thông tin nhạy cảm (như mật khẩu).
Sau đó, chúng ta sẽ gửi JWT trở lại phía client, dưới dạng JSON, nơi nó sẽ được lưu trữ localStorage.
Nếu ứng dụng phía client của chúng ta được thiết lập đúng, tất cả các yêu cầu tiếp theo đối với API của chúng ta sẽ bao gồm tiêu đề sau:
"HTTP_AUTHORIZATION" => "Bearer <super encoded JWT>"
Vì vậy, method current_user của chúng ta, được xác định trong Application Controller, sẽ cần giải mã JWT. Let's do it.
# app/controllers/application_controller.rb class ApplicationController < ActionController::API before_action :authenticate def logged_in? !!current_user end def current_user if auth_present? user = User.find(auth["user"]) if user @current_user ||= user end end end def authenticate render json: {error: "unauthorized"}, status: 401 unless logged_in? end private def token request.env["HTTP_AUTHORIZATION"].scan(/Bearer (.*)$/).flatten.last end def auth Auth.decode(token) end def auth_present? !!request.env.fetch("HTTP_AUTHORIZATION", "").scan(/Bearer/).flatten.first end end
Ở đây chúng ta sẽ thấy một số method trông khá quen thuộc: method current_user chịu trách nhiệm truy xuất người dùng hiện tại, method logged_in? trả về đúng hoặc sai, tùy thuộc vào sự hiện diện của người dùng hiện tại và methodauthenticate đóng vai trò là người gác cổng, trả về lỗi nếu người dùng chưa đăng nhập. Hãy đi sâu vào method current_user của chúng ta.
- Đầu tiên, chúng ta kiểm tra xem liệu request của chúng ta có tiêu đề hay khóa, ["HTTP_AUTHORIZATION"] và liệu giá trị của tiêu đề đó có bao gồm tên gọi Bearer hay không.
- Lấy mã thông báo ra khỏi giá trị của tiêu đề đó, sẽ giống như "Bearer <encoded JWT>"
Sau đó, trong method #auth chúng ta sử dụng thư viện Auth của mình để giải mã mã thông báo:
Auth.decode(token)
Cuối cùng, chúng ta sử dụng mã thông báo được giải mã, hiện có định dạng bên dưới và chứa ID duy nhất của người dùng
{user: 1}
Chúc các bạn thành công
https://www.thegreatcodeadventure.com/jwt-auth-in-rails-from-scratch/
https://github.com/plataformatec/devise