Tất nhiên, nếu một ứng dụng web liên quan đến người dùng thì dữ liệu của họ phải được bảo vệ và bảo mật.
Bảo mật một ứng dụng web có thể có nhiều ý nghĩa. Trong bài đăng này, chúng ta sẽ thảo luận về một tập hợp con của bảo mật web liên quan đến xác thực bằng cách sử dụng JSON Web Tokens (JWT) và khung ứng dụng web Ruby on Rails.
Hãy bắt đầu!
Mã thông báo Web JSON là gì?
Mã thông báo Web JSON là một tiêu chuẩn internet được Lực lượng đặc nhiệm kỹ thuật Internet (IETF) xác định là:"phương tiện nhỏ gọn, an toàn với URL để thể hiện các khiếu nại được chuyển giao giữa hai bên".
Ở đây, "khiếu nại" đề cập đến các loại thông tin về một chủ đề. Xác nhận quyền sở hữu được biểu thị dưới dạng cặp tên/giá trị trong đó tên luôn là một chuỗi và giá trị có thể là bất kỳ giá trị JSON nào.
Cấu trúc cơ bản của mã thông báo web JSON
Đi sâu vào sự phức tạp của JWT nằm ngoài phạm vi của bài viết này. Điều đó nói lên rằng, cần phải biết cấu trúc của JWT.
JWT bao gồm ba phần, cách nhau bằng dấu chấm:tiêu đề, tải trọng và chữ ký.
Một ví dụ JWT có thể trông giống như sau:
Để dễ đọc, mỗi phần của mã thông báo bắt đầu trên một dòng mới. Trong thực tế, các phần được nối với nhau.
Mã thông báo này được gửi từ máy chủ đến máy khách. Máy khách gửi mã thông báo đến máy chủ để nhận dạng chính nó và xử lý yêu cầu.
Phần đầu tiên, tiêu đề, chứa thông tin về thuật toán được sử dụng để tạo mã thông báo và loại mã thông báo. Nếu được giải mã, chúng ta sẽ nhận được kết quả như sau:
Phần thứ hai, tải trọng, chứa một tập hợp các xác nhận quyền sở hữu về người dùng. Trong hầu hết các trường hợp, đây sẽ là máy khách trong thiết lập máy khách-máy chủ và nó có thể trông giống như:
Chữ ký (phần cuối cùng của JWT đã ký) xác thực mã thông báo. Nó được tạo bằng cách mã hóa tiêu đề và tải trọng bằng Mã hóa Base64url — RFC 4648, sau đó nối chúng bằng dấu phân cách.
Về cơ bản, điều xảy ra với bit chữ ký là thế này:
HMAC_SHA256 là một loại thuật toán băm có khóa được xây dựng từ hàm băm SHA-256 để băm chữ ký. Việc lựa chọn thuật toán mã hóa xuất phát từ "alg": "HS256" trong tiêu đề. Nếu mã thông báo chưa được ký, nó sẽ chỉ có tiêu đề và tải trọng mà không có chữ ký.
JWT Vs. Các phương thức xác thực khác cho ứng dụng Ruby của bạn
Mã thông báo Web JSON, như tên của nó, dựa trên mã thông báo. Ở đầu bên kia của quang phổ, chúng tôi có xác thực dựa trên phiên:một cách xác thực người dùng truyền thống hơn. Luồng xác thực dựa trên phiên khá khác so với luồng xác thực dựa trên mã thông báo.
Luồng xác thực dựa trên phiên có thể trông giống như sau:
- Người dùng hoặc khách hàng gửi yêu cầu chứa thông tin xác thực của người dùng.
- Máy chủ xác thực người dùng, lưu trữ phiên và trả về ID phiên được lưu dưới dạng cookie trong trình duyệt.
- Máy khách gửi cookie cùng với các yêu cầu tiếp theo của nó tới máy chủ.
- Máy chủ kiểm tra thông tin phiên được trình bày và nếu hợp lệ sẽ xác thực người dùng và trả về thông tin được yêu cầu cho khách hàng.
Mặc dù xác thực dựa trên phiên chủ yếu được sử dụng cho các kết nối máy khách-máy chủ, nhưng xác thực dựa trên mã thông báo thường được sử dụng với các kết nối máy chủ-máy chủ (ví dụ:giữa hai API).
Tuy nhiên, một điểm khác biệt quan trọng cần lưu ý là với xác thực dựa trên phiên, trạng thái xác thực được xử lý trên máy chủ — trong khi mã thông báo được quản lý trên máy khách.
Tại sao nên sử dụng JWT để xác thực?
Ngoài việc triển khai tương đối đơn giản, việc sử dụng Mã thông báo Web JSON để xác thực còn có một số lợi ích khác, chẳng hạn như:
- Họ không quốc tịch , có nghĩa là kho lưu trữ phiên là không cần thiết. Bản thân mã thông báo chứa tất cả thông tin người dùng, do đó không cần phải truy vấn cơ sở dữ liệu hoặc máy chủ xác thực để biết thông tin về từng yêu cầu.
- JWT nhìn chung có hiệu suất cao hơn hơn hầu hết các phương pháp xác thực truyền thống (miễn là máy chủ không thực hiện bất kỳ tra cứu nào đối với cơ sở dữ liệu hoặc cửa hàng để xác thực người dùng), khiến chúng khá hiệu quả.
- Họ cũng cung cấp đảm bảo an ninh vững chắc , trong đó JWT đã ký cung cấp các biện pháp bảo vệ để kẻ tấn công hoặc khách hàng không thể sửa đổi mã thông báo để có quyền truy cập vào dữ liệu được bảo vệ.
Các phương pháp hay nhất của JWT dành cho ứng dụng Ruby
Không cần phải nói rằng các khóa bí mật được sử dụng để ký JWT phải dài, ngẫu nhiên và có các tổ hợp ký tự phức tạp. Điều này đảm bảo rằng các khóa được an toàn đầy đủ và những kẻ tấn công khó có thể cưỡng bức chúng.
Các khóa bí mật mà Rails tạo ra hầu hết đều an toàn, nhưng đảm bảo an toàn sẽ bị vô hiệu nếu bạn vô tình làm mất khóa và tiết lộ chúng.
Điều quan trọng nữa là sử dụng Bảo mật lớp vận chuyển (TLS) khi vận chuyển mã thông báo giữa các bên trên mạng. TLS có thể giảm thiểu cuộc tấn công trung gian (cả phương thức xác thực dựa trên phiên và mã thông báo đều dễ bị tấn công như vậy).
Triển khai xác thực JWT trong ứng dụng Rails
Hãy cùng xem quy trình xác thực dựa trên mã thông báo:

Không giống như xác thực dựa trên phiên, một kỹ thuật xác thực trạng thái trong đó chúng tôi sử dụng phiên để theo dõi người dùng đã được xác thực, xác thực dựa trên mã thông báo với JWT là không trạng thái; không cần lưu trữ bất kỳ thông tin nào về trạng thái xác thực của người dùng trên máy chủ. Điều này giúp đơn giản hóa việc thiết kế ứng dụng.
Trong bài đăng này, chúng tôi sẽ giả định rằng ứng dụng của chúng tôi được chia thành giao diện người dùng và phần phụ trợ. Quá trình xác thực diễn ra ở phần phụ trợ, vì vậy chúng tôi sẽ xây dựng phần phụ trợ API Rails có xác thực.
Mã mẫu trong bài đăng này dựa trên Rails 7.0.5 và Ruby 3.2.2.
Sử dụng jwt và bcrypt Đá quý Ruby
Chúng ta sẽ cần hai viên ngọc cho ứng dụng của mình:jwt và bcrypt .
jwt là một triển khai Ruby của tiêu chuẩn Mã thông báo Web JSON RFC 7519 OAuth. bcrypt là một liên kết Ruby cho bcrypt() OpenBSD thuật toán băm mật khẩu.
Bạn có thể làm theo mã mẫu trong kho mã này.
Lưu ý: jwt không phải là giải pháp duy nhất để làm việc với JWT; một loại đá quý nổi tiếng khác là devise-jwt , cung cấp xác thực JWT cho Devise và Rails. Nhưng chúng ta sẽ tập trung vào jwt trong bài đăng này.
Hãy bắt đầu.
Xây dựng API Rails của chúng tôi
Điều đầu tiên chúng ta cần là một ứng dụng API. Chúng tôi sẽ tạo một cái với:
--api tùy chọn ở đây định cấu hình trước một nhóm Rails nhỏ hơn chỉ dành cho các ứng dụng API.
Bên trong Gemfile, chúng ta có thể thêm phần phụ thuộc đầu tiên, jwt . Viên ngọc thứ hai của chúng tôi, bcrypt , đã có trong Gemfile của ứng dụng Rails mới được tạo — chúng ta chỉ cần bỏ ghi chú nó.
Chúng tôi cần bcrypt để băm mật khẩu người dùng một cách an toàn trong cơ sở dữ liệu. Điều quan trọng cần lưu ý là chúng tôi sẽ không sử dụng bcrypt trực tiếp. Chúng tôi sẽ tận dụng has_secure_password của Active Model phương thức lớp, phụ thuộc vào bcrypt .
Bỏ qua các gem mặc định đi kèm với ứng dụng Rails mới, Gemfile của chúng ta sẽ trông giống như:
Bây giờ là thời điểm thích hợp để cài đặt gem của chúng tôi với bundle install .
Tạo User và Product Người mẫu
Tiếp theo, chúng ta sẽ tạo hai mô hình:User và Product . User sẽ là mô hình đại diện cho người dùng và chúng tôi sẽ xác thực nó để cho phép truy cập vào các sản phẩm, được biểu thị bằng Product người mẫu.
Sau khi chạy quá trình di chuyển của chúng tôi với rails db:migrate , quá trình thiết lập của chúng tôi với các mô hình đã hoàn tất và lược đồ của chúng tôi, được tìm thấy trong db/schema.rb , bây giờ trông giống như thế này:
Điều quan trọng là phải chỉ ra ở giai đoạn này rằng chúng tôi đang cố tình bỏ qua các vấn đề tiềm ẩn như hạn chế về cơ sở dữ liệu, xác thực, đảm bảo tính duy nhất, v.v. Vì lợi ích của bài đăng này, chúng tôi sẽ bỏ qua các lỗi xử lý, cùng với các lỗi khác. Mục đích của chúng tôi ở đây là chứng minh JWT hoạt động như một hình thức xác thực không trạng thái để bảo mật ứng dụng Ruby của chúng tôi. Trong một ứng dụng sản xuất, bạn muốn đảm bảo tất cả những điều này đều được đề cập.
Xây dựng jwt Gói đá quý
Bước tiếp theo là xây dựng một trình bao bọc xung quanh jwt gem chúng tôi đã cài đặt trước đó. Chúng tôi sẽ sử dụng trình bao bọc này để mã hóa và giải mã các xác nhận quyền sở hữu từ máy chủ đến máy khách. Đối với điều này, chúng tôi sẽ tạo một app/lib thư mục.
Lý do chúng tôi không sử dụng lib thư mục đi kèm với Rails là nó không được tải tự động. Mọi thứ trong app được tải tự động và háo hức theo mặc định, giúp thiết lập đơn giản hơn trong trường hợp của chúng tôi.
Lớp trình bao bọc của chúng tôi được tìm thấy trong app/lib/json_web_token.rb và trông như thế này:
Các phương thức chính ở đây là encode — để mã hóa thông tin người dùng — và decode — để giải mã thông tin người dùng sau này trong máy chủ. Lưu ý cách chúng tôi ủy quyền các tác vụ mã hóa và giải mã cho jwt đá quý thông qua JWT.encode và JWT.decode .
Tại thời điểm này, bạn đã có thể kiểm tra lớp này trong bảng điều khiển Rails của mình:
Hãy chú ý kết quả của JsonWebToken.encode(data) được chia thành ba phần theo một dấu chấm, tạo ra tiêu đề, tải trọng và chữ ký. Chúng tôi có bit chữ ký vì chúng tôi đã ký tải trọng của mình bằng khóa bí mật mà Rails cung cấp thông qua Rails.application.secrets.secret_key_base .
Quay lại User Người mẫu
Bây giờ sẽ là thời điểm thích hợp để ghé thăm User của chúng tôi mô hình tại app/models/user.rb . Tất cả những gì chúng ta cần làm ở đây là thêm has_secure_password phương thức lớp:
has_secure_password băm mật khẩu của người dùng trong cơ sở dữ liệu một cách an toàn.
Tạo người dùng và sản phẩm mẫu trong Rails
Bây giờ chúng ta có thể truy cập bảng điều khiển Rails để tạo người dùng và sản phẩm mẫu trong cơ sở dữ liệu của mình cũng như kiểm tra tính bảo mật của ứng dụng:
Bạn có thể đặt đoạn mã tương tự vào seeds.rb của mình để lưu một số thao tác gõ trong trường hợp bạn đặt lại cơ sở dữ liệu của mình.
Sử dụng JWT trong Rails Controller
Trong các bước tiếp theo, chúng tôi sẽ triển khai bảo mật bằng cách sử dụng JWT bên trong bộ điều khiển của chúng tôi. Một nơi tốt để bắt đầu là ApplicationController tại app/controllers/application_controller.rb :
Ở đây, chúng tôi tạo một authenticate phương pháp giải mã mã thông báo Web JSON mà người dùng gửi cho chúng tôi. Nếu chúng tôi có thể xác minh thành công mã thông báo, chúng tôi sẽ trả về User đối tượng đại diện cho người dùng đưa ra yêu cầu. Chúng tôi chủ yếu quan tâm đến con đường hạnh phúc ở đây và sẽ bỏ qua rất nhiều bước kiểm tra.
Xác định authenticate phương thức trong ApplicationController và thiết lập nó dưới dạng before_action bảo vệ mọi bộ điều khiển kế thừa từ nó. Yêu cầu tới bất kỳ bộ điều khiển nào khác sẽ cần JWT hợp lệ để truy cập các bộ điều khiển đó (vì mọi bộ điều khiển khác sẽ kế thừa bộ điều khiển chính này).
Tiếp theo, chúng ta cần một AuthenticationController mà người dùng có thể gửi yêu cầu và nhận Mã thông báo Web JSON đã ký từ máy chủ của chúng tôi. Bộ điều khiển này nên được đặt ở app/controllers/authentication_controller.rb và có thể trông như thế này:
Khi một yêu cầu chạm tới bộ điều khiển này, vì đó là người dùng yêu cầu mã thông báo nên ban đầu chúng tôi không muốn xác thực chúng. Mục đích của bộ điều khiển này là phản hồi bằng mã thông báo mà người dùng có thể sử dụng để truy cập phần tài nguyên còn lại trên máy chủ của chúng tôi. Do đó cần có skip_before_action :authenticate ở dòng thứ hai.
Trong login hành động (hành động mà người dùng nhấn để lấy mã thông báo), chúng tôi lấy username và password từ các tham số đi kèm với yêu cầu tới bộ điều khiển này. Nếu chúng tôi có thể xác thực người dùng — nghĩa là xác minh xem tên người dùng và mật khẩu của họ có khớp với những gì chúng tôi đã lưu trữ trong cơ sở dữ liệu của mình hay không — thì chúng tôi sẽ cấp cho họ mã thông báo đã ký và thông tin về thời điểm mã thông báo đó hết hạn.
Trong trường hợp của chúng tôi, chúng tôi sẽ không sử dụng thời hạn hết hạn của mã thông báo. Nhưng trong một ứng dụng sản xuất, điều đó có thể được sử dụng để thu hồi quyền truy cập vào tài nguyên.
Chúng ta sẽ thực hiện tất cả các bước này bằng cách sử dụng curl sau này.
Thử nghiệm ứng dụng Ruby của chúng tôi bằng tài nguyên được bảo vệ
Chúng tôi đã trình bày một phần quy trình trong sơ đồ quy trình của phần 'Triển khai xác thực JWT trong ứng dụng Rails' trước đó. Để hoàn thiện mọi thứ và kiểm tra tất cả các bước trong sơ đồ quy trình, chúng tôi cần có tài nguyên để bảo vệ.
Chúng tôi có Product người mẫu rồi. Bây giờ chúng tôi cần bộ điều khiển cho mô hình sản phẩm và các tuyến đường truy cập vào sản phẩm và mã thông báo.
Hãy tạo bộ điều khiển với rails g controller Product index vì vậy chúng tôi có một cái gì đó như:
Tất nhiên, chúng ta cần một lộ trình để truy cập các bộ điều khiển này. config/routes.rb của chúng tôi sẽ trông giống như:
Bây giờ chúng ta sẽ kiểm tra với curl để xem liệu mọi thứ có hoạt động như mong đợi hay không. Lưu ý rằng chúng tôi đã có người dùng để xác thực và tài nguyên sản phẩm để truy cập.
Hãy thử lấy JWT với người dùng không tồn tại:
Chúng tôi sẽ nhận được phản hồi sau:
Bây giờ hãy thử tương tự với người dùng mà chúng tôi đã tạo trước đó:
Điều này sẽ cung cấp cho chúng tôi mã thông báo web JSON đã ký có thể trông như thế này:
Hãy giữ mã thông báo này trong một giây và thử truy cập tài nguyên sản phẩm bằng mã thông báo xấu (Tôi đã thay đổi một ký tự ngẫu nhiên trong mã thông báo):
Và chúng ta sẽ nhận được:
Tuy nhiên, nếu chúng tôi đưa ra yêu cầu tương tự để truy cập tài nguyên sản phẩm bằng mã thông báo hợp lệ mà chúng tôi đã nhận được từ máy chủ trước đó:
Chúng tôi được cấp quyền truy cập vào tài nguyên sản phẩm:
Thế thôi! Chúng tôi đã bảo mật thành công ứng dụng Ruby của mình bằng mã thông báo web JSON!
Kết thúc
Trong bài đăng này, chúng tôi đã thảo luận về Mã thông báo Web JSON và cách chúng hoạt động. Trước tiên, chúng tôi đề cập đến những kiến thức cơ bản về JWT, bao gồm cấu trúc của chúng và một số phương pháp hay nhất. Sau đó, chúng tôi đã triển khai xác thực JWT đơn giản bằng cách sử dụng jwt đá quý.
Tôi hy vọng bạn thấy bài viết này hữu ích. Chúc bạn viết mã vui vẻ!
Tái bút. Nếu bạn muốn đọc các bài đăng của Ruby Magic ngay khi chúng được đăng tải, hãy đăng ký nhận bản tin Ruby Magic của chúng tôi và không bao giờ bỏ lỡ một bài đăng nào!