Computer >> บทช่วยสอนคอมพิวเตอร์ >  >> การเขียนโปรแกรม >> Ruby

เสริมสร้าง Ruby App ด้วย JSON Web Tokens (JWT) ที่ปลอดภัย

หาก web application เกี่ยวข้องกับ users โดยปกติ ข้อมูลของพวกเขาควรได้รับการปกป้องและรักษาความปลอดภัย

การ secure web application อาจหมายถึงหลายอย่าง ในโพสต์นี้ เราจะอภิปราย subset ของ web security ที่เกี่ยวข้องกับ authentication โดยใช้ JSON Web Tokens (JWTs) และ Ruby on Rails web application framework

มาเริ่มกัน!

JSON Web Token คืออะไร?

JSON Web Token เป็น internet standard ที่ defined โดย Internet Engineering Task Force (IETF) ว่า: "compact, URL-safe means of representing claims to be transferred between two parties"

ที่นี่ "claims" หมายถึง assorted pieces of information เกี่ยวกับ subject Claim ถูกแทนเป็น name/value pair ที่ name เสมอเป็น string และ value สามารถเป็น JSON value ใดๆ

โครงสร้างพื้นฐานของ JSON Web Tokens

การเจาะลึก intricacies ของ JWTs อยู่นอก scope ของโพสต์นี้ ที่ว่ามัน worth knowing โครงสร้างของ JWTs

JWT ประกอบด้วยสามส่วน แยกด้วย period: header, payload, และ signature

ตัวอย่าง JWT อาจมีลักษณะดังนี้:


เพื่อ readability แต่ละส่วนของ token เริ่มบรรทัดใหม่ ในปฏิบัติ ส่วนต่างๆ ถูก joined กัน

Token นี้ส่งจาก server ไป client Client ส่ง token ไป server เพื่อ identify ตัวเองและให้ process request

ส่วนแรก header มีข้อมูลเกี่ยวกับ algorithm ที่ใช้ generate token และ type of token หาก decode เราจะได้ประมาณนี้:


ส่วนที่สอง payload มี set of claims เกี่ยวกับ user ในกรณีส่วนใหญ่ นี่จะเป็น client ใน client-server setup และอาจมีลักษณะแบบนี้:


Signature (ส่วนสุดท้ายของ signed JWT) validate token มันถูก generate โดยการ encode header และ payload ใช้ Base64url Encoding — RFC 4648 และ concatenate พวกมันด้วย period separator

Essentially สิ่งที่เกิดขึ้นกับ signature bit คือนี้:


HMAC_SHA256 เป็นประเภทของ keyed hash algorithm ที่สร้างจาก SHA-256 hash function ที่ hash signature การเลือก cryptographic algorithm มาจาก "alg": "HS256" ใน header หาก token ไม่ได้ signed มันจะมีแค่ header และ payload โดยไม่มี signature

JWTs เทียบกับ Authentication Methods อื่นๆ สำหรับ Ruby App ของคุณ

JSON Web Tokens ตามที่ชื่อบ่งชี้ เป็น token-based อีกปลายของ spectrum เรามี session-based authentication: วิธีดั้งเดิมมากกว่าในการ authenticate users Flow ของ session-based authentication ค่อนข้างต่างจาก token-based authentication

Flow ของ session-based authentication อาจมีลักษณะดังนี้:

  1. User หรือ client ส่ง request ที่มี user credentials
  2. Server authenticate user, store session และคืนค่า session ID ที่เก็บเป็น cookie ใน browser
  3. Client ส่ง cookies พร้อม subsequent requests ไป server
  4. Server inspect session information ที่เสนอ และถ้า valid authenticate user และคืนค่าข้อมูลที่ขอไป client

ในขณะที่ session-based authentication ใช้ส่วนใหญ่สำหรับ client-server connections token-based authentication บ่อยครั้งใช้กับ server-server connections (เช่น ระหว่างสอง APIs)

ความแตกต่างสำคัญหนึ่งที่ต้องสังเกต คือด้วย session-based authentication authentication state ถูก handle บน server — ในขณะที่ tokens ถูก managed บน client

ทำไมต้องใช้ JWTs สำหรับ Authentication?

นอกจากจะ implement ได้ relative simple มี advantages อื่นๆ ของการใช้ JSON Web Tokens สำหรับ authentication เช่น:

  • พวกมัน stateless หมายความว่า session store ไม่จำเป็น Token เองมีข้อมูลผู้ใช้ทั้งหมด ดังนั้นไม่ต้อง query database หรือ authentication server สำหรับข้อมูลในแต่ละ request
  • JWTs generally more performant กว่าวิธี authentication แบบดั้งเดิมส่วนใหญ่ (ตราบใดที่ server ไม่ทำ lookups ต่อ database หรือ store เพื่อ authenticate user) ทำให้พวกมัน efficient มาก
  • พวกมันยังเสนอ solid security guarantees ที่ signed JWTs ให้ safeguards ให้ attacker หรือ client ไม่สามารถ modify tokens เพื่อ gain access ไปยัง protected data

JWT Best Practices สำหรับ Ruby Apps

มันไม่ต้องพูดว่า secret keys ที่ใช้ sign JWTs ควรยาว, random และมี complex character combinations นี่ให้แน่ใจว่า keys safe เพียงพอ และยากสำหรับ attackers ที่จะ brute-force พวกมัน

Secret keys ที่ Rails generate safe สำหรับส่วนใหญ่ แต safety guarantees ถูก nullified หากคุณ commit keys โดยบังเอิญและ reveal พวกมัน

มันสำคัญที่จะใช้ Transport Layer Security (TLS) เมื่อ transport tokens ระหว่าง parties บน network TLS สามารถ mitigate man-in-the-middle attack (ทั้ง token และ session-based authentication methods ร่อนต่อ attacks เหล่านี้)

การ Implement JWT authentication ใน Rails App

มาดู token-based authentication flow:

เสริมสร้าง Ruby App ด้วย JSON Web Tokens (JWT) ที่ปลอดภัย

ไม่เหมือน session-based authentication stateful authentication technique ที่เราใช้ sessions track authenticated user token-based authentication ด้วย JWTs เป็น stateless ไม่จำเป็นต้อง store ข้อมูลใดๆ เกี่ยวกับ authentication state ของ user บน server นี่ทำให้ application design เรียบง่ายขึ้น

ในโพสต์นี้ เราจะสมมติว่าแอปพลิเคชันของเรา split เป็น frontend และ backend Authentication เกิดขึ้นบน backend ดังนั้นเราจะสร้าง Rails API backend พร้อม authentication

Sample code ในโพสต์นี้ based on Rails 7.0.5 และ Ruby 3.2.2

การใช้ jwt และ bcrypt Ruby Gems

เราจะต้องการสอง gems สำหรับแอปพลิเคชัน: jwt และ bcrypt

jwt เป็น Ruby implementation ของ RFC 7519 OAuth JSON Web Token standard bcrypt เป็น Ruby binding สำหรับ OpenBSD bcrypt() password hashing algorithm

คุณสามารถ follow along กับ sample code ใน code repo นี้

Note: jwt ไม่ใช่ only solution สำหรับทำงานกับ JWTs อีก well-known gem คือ devise-jwt ซึ่งให้ JWT authentication สำหรับ Devise และ Rails แตเราจะ focus บน jwt ในโพสต์นี้

มาเริ่มกัน

การสร้าง Rails API ของเรา

สิ่งแรกที่เราต้องการคือ API application เราจะสร้างด้วย:


--api option ที่นี่ preconfigures smaller stack ของ Rails สำหรับ API applications เท่านั้น

ภายใน Gemfile เราสามารถเพิ่ม dependency แรก jwt gem ที่สอง bcrypt อยู่ใน Gemfile ของ newly-generated Rails application แล้ว — เราแค่ต้อง uncomment มัน

เราต้องการ bcrypt เพื่อ securely hash user passwords ใน database มันสำคัญที่จะสังเกตว่าเราจะไม่ใช้ bcrypt โดยตรง เราจะ leverage Active Model's has_secure_password class method ซึ่ง depend บน bcrypt

Ignoring default gems ที่มาพร้อม new Rails application Gemfile ของเราควรจะมีลักษณะแบบนี้:


ตอนนี้เป็นเวลาที่ดีที่จะ install gems ด้วย bundle install

Generate User และ Product Models

ต่อไป เราจะ generate สอง models: User และ Product User จะเป็น model แทน users และเราจะ authenticate มันเพื่ออนุญาต access ไป products ที่แทนโดย Product model


หลังจากรัน migration ด้วย rails db:migrate setup ของเรากับ models เสร็จ และ schema ที่อยู่ใน db/schema.rb ควรจะมีลักษณะคล้ายกับนี้:


มันสำคัญที่จะชี้ให้เห็นในขั้นตอนนี้ว่าเรา deliberately ignoring potential issues เช่น database constraints, validations, ensuring uniqueness ฯลฯ เพื่อโพสต์นี้ เราจะ disregard handling errors ฯลฯ วัตถุประสงค์คือ demonstrate JWTs ใน action เป็นรูปแบบของ stateless authentication เพื่อ secure Ruby application ใน production application คุณจะอยากให้แน่ใจว่าทั้งหมดนี้ covered

สร้าง jwt Gem Wrapper

ขั้นตอนต่อไปคือสร้าง wrapper รอบ jwt gem ที่เราติดตั้งก่อนหน้า เราจะใช้ wrapper นี้ encode และ decode claims จาก server ไป client สำหรับนี้ เราจะสร้าง folder app/lib

เหตุผลที่เราไม่ใช้ lib folder ที่มาพร้อม Rails คือมันไม่ autoloaded ทุกอย่างภายใต้ app autoloaded และ eager-loaded โดย default ทำให้ setup เรียบง่ายในกรณีของเรา

Wrapper class ของเราอยู่ใน app/lib/json_web_token.rb และมีลักษณะแบบนี้:


Main methods ที่นี่คือ encode — เพื่อ encode user information — และ decode — เพื่อ decode user information ใน server ต่อมา สังเกตว่าเรา delegate encoding และ decoding tasks ไป jwt gem ผ่าน JWT.encode และ JWT.decode

ณ จุดนี้ คุณสามารถ test class นี้ใน Rails console แล้ว:


สังเกตว่า result ของ JsonWebToken.encode(data) แยกเป็นสามส่วนโดย period สร้าง header, payload และ signature เรามี signature bit เพราะเรา signed payload ด้วย secret key ที่ Rails provide ผ่าน Rails.application.secrets.secret_key_base

กลับไปที่ User Model

ตอนนี้จะเป็นเวลาที่ดีที่จะไปที่ User model ที่ app/models/user.rb ทั้งหมดที่เราต้องทำคือเพิ่ม has_secure_password class method:


has_secure_password securely hashes passwords ของ users ใน database

สร้าง Sample User และ Product ใน Rails

ตอนนี้เราสามารถ hop เข้า Rails console เพื่อ generate sample user และ product ใน database และ test security ของแอปพลิเคชัน:


คุณสามารถวาง code ชิ้นเดียวกันใน seeds.rb เพื่อประหยัด typing หากคุณ reset database

การใช้ JWTs ใน Rails Controllers

ในขั้นตอนต่อไป เราจะ implement security โดยใช้ JWTs ภายใน controllers ของเรา Good place to start คือ ApplicationController ที่ app/controllers/application_controller.rb:


ที่นี่ เราสร้าง authenticate method เพื่อ decode JSON Web tokens ที่ users ส่งมา หากเรา verify token สำเร็จ เราคืนค่า User object ที่แทน user ที่ทำ request เราสนใจ happy path ส่วนใหญ่และจะ bypass หลาย checks

การ define authenticate method ใน ApplicationController และตั้งค่าเป็น before_action secure ทุก controller ที่สืบทอดจากมัน Request ไปยัง controller อื่นใดจะต้องมี valid JWT เพื่อ access controllers เหล่านั้น (เพราะทุก controller อื่นจะ inherit main controller นี้)

ต่อไป เราต้องการ AuthenticationController ที่ users สามารถส่ง requests และได้ signed JSON Web Token จาก server ของเรา Controller นี้ควรอยู่ที่ app/controllers/authentication_controller.rb และอาจมีลักษณะแบบนี้:


เมื่อ request hit controller นี้ เนื่องจากมันเป็น user ขอ token เราไม่อยาก authenticate พวกเขา initially วัตถุประสงค์ของ controller นี้คือ respond ด้วย token ที่ user สามารถใช้ access resources ที่เหลือบน server ดังนั้นจึงต้องการ skip_before_action :authenticate บรรทัดที่สอง

ใน login action (action ที่ users hit สำหรับ token) เรา grab username และ password จาก parameters ที่มาพร้อม request ไป controller นี้ หากเราสามารถ authenticate user — นั่นคือ verify ถ้า username และ password match สิ่งที่เราเก็บใน database — เรา present พวกเขาด้วย signed token และข้อมูลเกี่ยวกับเมื่อ token นั้นหมดอายุ

ในกรณีของเรา เราจะไม่ใช้ expiry period ของ token แตใน production application นั้นอาจใช้เพื่อ revoke access ไปยัง resource

เราจะผ่านทุกขั้นตอนเหล่านี้ด้วย curl ภายหลัง

การทดสอบ Ruby Application ด้วย Protected Resource

เรา cover flow ส่วนหนึ่งใน section 'Implementing JWT authentication in a Rails App' ก่อนหน้านี้ flow diagram เพื่อ cover ทุกอย่างและ test ทุกขั้นตอนใน flow diagram เราต้องการ resource ที่ protected

เรามี Product model แล้ว เราตอนนี้ต้องการ controller สำหรับ product model และ routes เพื่อ access product และ tokens

มา create controller ด้วย rails g controller Product index เพื่อให้เรามีอะไรแบบนี้:


แน่นอนว่าเราต้องการ route เพื่อ access controllers เหล่านี้ config/routes.rb ควรจะมีลักษณะแบบนี้:


ตอนนี้เราจะ test ด้วย curl เพื่อดูว่าทุกอย่างทำงาน as expected สังเกตว่าเรามี user สำหรับ authenticate และ product resource สำหรับ access

มาลอง get JWT ด้วย user ที่ไม่มีอยู่:


เราควรได้ response ดังนี้:


ตอนนี้ลองเดียวกันกับ user ที่เราสร้างก่อนหน้านี้:


สิ่งนี้ควรให้ signed JSON web token ที่อาจมีลักษณะแบบนี้:


มากัน token นี้ไว้สักครู่ และลอง access product resource ด้วย bad token (ผมเปลี่ยน random character ใน token):


และเราควรได้:


อย่างไรก็ตาม หากเราทำ request เดียวกันไป access product resource ด้วย valid token ที่เราได้จาก server ก่อนหน้านี้:


เราได้รับ access ไปยัง product resource:


แค่นั้น! เรา secure Ruby application ของเราสำเร็จด้วย JSON web token!

สรุป

ในโพสต์นี้ เราอภิปราย JSON Web Tokens และวิธีที่พวกมันทำงาน เราครอบคลุม basics ของ JWTs รวมถึง structure และ best practices บางอย่าง ต่อมาเรา implement simple JWT authentication โดยใช้ jwt gem

หวังว่าโพสต์นี้จะช่วยประโยชน์ Happy coding!

P.S. หากคุณต้องการอ่านโพสต์ Ruby Magic ทันทีที่เผยแพร่ ให้สมัครสมาชิก newsletter Ruby Magic ของเรา และจะไม่พลาดโพสต์เด็ดๆ ไป!