Skip to content

JWT

أصبح JWT أحد الطرق الرئيسية للمصادقة في الاتصالات الحديثة من جانب الخادم، ويتميز بخفة الوزن وعدم وجود حالة.

مقدمة

الاسم الكامل لـ JWT هو JSON Web Tokens، ويمكن الاطلاع على مقدمة تفصيلية عن JWT في jwt.io، وهو طريقة مفتوحة وآمنة ومدمجة لنقل المعلومات بين طرفي خدمة باستخدام كائن JSON كحامل، وخصائصه هي الأمان العالي، ومنع التلاعب بالمحتوى، وانخفاض الاستهلاك.

الهيكل

في معيار RFC، يتكون JWT من الأجزاء الثلاثة التالية:

  • Header (الرأس)
  • Payload (الحمولة)
  • Signature (التوقيع)

ثم يتم فصل كل جزء بنقطة .، وأخيرًا يتشكل سلسلة، والتنسيق هو header.payload.signature، وهذا هو الهيكل القياسي لرمز JWT، وفيما يلي شرح لكل هيكل.

TIP

تجدر الإشارة إلى أن base64 و base64URL ليسا نفس طريقة الترميز، فالأخير متوافق مع عنوان URL للويب، وقد تم تهربيه.

الرأس

الرأس يصرح فقط ببعض المعلومات الأساسية، ويتكون عادةً من جزأين، نوع الرمز، وخوارزمية التشفير المستخدمة في التوقيع، على سبيل المثال:

json
{
  "alg": "HS256",
  "typ": "JWT"
}

المعلومات أعلاه تعني تقريبًا أن نوع الرمز هو JWT، وخوارزمية التشفير المستخدمة في جزء التوقيع هي HS256، ثم يتم ترميز كائن JSON إلى سلسلة عبر Base64Url، وهذه السلسلة هي رأس JWT.

الحمولة

الجزء الثاني من JWT هو جزء الحمولة، ويحتوي بشكل رئيسي على جزء الإعلانات (claims)، وعادةً ما تكون الإعلانات بيانات عن كيان ما، مثل مستخدم. هناك ثلاثة أنواع من الإعلانات:

  • reigstered: تمثل Registered claims بعض الإعلانات المُعرَّفة مسبقًا، بعضها ليس إلزاميًا ولكن لا يزال يُوصى باستخدامه، مثل: iss (الجهة المُصدرة)، و exp (وقت الانتهاء)، و aud (الجمهور).
  • public: يمكن لـ Public claims أن يُعرِّفها مستخدمو JWT بحرية، ومن الأفضل تجنب التعارض مع أجزاء الإعلانات الأخرى.
  • private claims:这部分声明同样是自定义的,通常用于在服务双方共享一些信息。

مثال على حمولة:

json
{
  "sub": "1234567890",
  "name": "John Doe",
  "admin": true
}

سيتم ترميز كائن JSON هذا إلى سلسلة عبر Base64Url، لتشكيل الجزء الثاني من JWT.

DANGER

رغم أن جزء الحمولة محمي أيضًا ومنع التلاعب، إلا أن هذا الجزء قابل للقراءة بشكل عام، لذا لا تضع معلومات حساسة داخل JWT.

التوقيع

بعد الحصول على الرأس المُرمَّز وجزء الحمولة المُرمَّز، يمكن إجراء توقيع التشفير وفقًا لخوارزمية التوقيع المُشار إليها في الرأس بناءً على محتوى الجزأين السابقين بالإضافة إلى المفتاح، لذا بمجرد تغيير أي محتوى في JWT، سيكون التوقيع الذي تم الحصول عليه عند فك التشفير مختلفًا، وفي نفس الوقت إذا تم استخدام مفتاح خاص، يمكن أيضًا التحقق من الجهة المُصدرة لـ JWT.

sign = HMACSHA256(
  base64UrlEncode(header) + "." +
  base64UrlEncode(payload),
  secret)

على سبيل المثال المثال التالي:

Header
{
  "alg": "HS256",
  "typ": "JWT"
}

Payload
{
  "alg": "HS256",
  "typ": "JWT"
}

Verify Signature
HMACSHA256(
  base64UrlEncode(header) + "." +
  base64UrlEncode(payload),
  your secret
)

الناتج النهائي هو سلسلة مكونة من ثلاث سلاسل base64Url ومفصولة بـ .، وتبدو تقريبًا كالتالي

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.
cThIIoDvwdueQB468K5xDc5633seEFoqwxjF_xSJyQQ

مبدأ العمل

في المصادقة، عندما يسجل المستخدم الدخول بنجاح باستخدام بيانات الاعتماد، يتم إرجاع رمز JSON Web. نظرًا لأن الرمز هو بيانات اعتماد، فيجب توخي الحذر الشديد لمنع مشاكل الأمان. بشكل عام، يجب ألا يكون وقت حفظ الرمز أطول من اللازم. ثم كلما أراد المستخدم الوصول إلى المسارات والموارد المحمية، يجب عليه حمل الرمز عند تقديم الطلب، عادةً في Bearer schema داخل ترويسة Authorization، على سبيل المثال:

Authorization: Bearer <token>

بعد أن يستلم الخادم JWT، سيتحقق من صحته، مثل التلاعب بالمحتوى، أو انتهاء صلاحية الرمز، وما إلى ذلك، وإذا نجح التحقق يمكن الوصول إلى المورد بسلاسة. رغم أن JWT يمكنه حمل بعض المعلومات الأساسية، إلا أنه لا يزال يُوصى ألا تكون المعلومات كبيرة جدًا.

مكتبة JWT

المستودع الرسمي: golang-jwt/jwt: Community maintained clone of https://github.com/dgrijalva/jwt-go

الوثائق الرسمية: jwt package - github.com/golang-jwt/jwt/v4 - Go Packages

هذه المكتبة تدعم التحليل والتحقق وكذلك إنشاء وتوقيع JWT. خوارزميات التوقيع المدعومة حاليًا هي HMAC SHA، و RSA، و RSA-PSS، و ECDSA، ويمكن أيضًا إضافة خطافات خاصة.

التثبيت

go get -u github.com/golang-jwt/jwt/v4

الاستيراد

go
import "github.com/golang-jwt/jwt/v4"

اختيار خوارزمية التوقيع

هناك عدة خوارزميات توقيع متاحة، وقبل الاستخدام يجب فهم الفروق بينها لاختيار خوارزمية التوقيع بشكل أفضل، وأكبر اختلاف بينها هو التشفير المتماثل و التشفير غير المتماثل.

أبسط خوارزمية تشفير متماثل HSA، تتيح لأي []byte أن يكون مفتاحًا صالحًا، لذا سرعة الحساب أسرع قليلاً. عندما يكون كل من المنتج والمستهلك موثوقين، تكون كفاءة خوارزمية التشفير المتماثل هي الأعلى. ومع ذلك، نظرًا لأن نفس المفتاح يُستخدم للتوقيع والتحقق، فلا يمكن توزيع المفتاح للتحقق بسهولة، ففي النهاية مفتاح التوقيع هو نفسه، وإذا تسرب التوقيع فأمان JWT يصبح بلا معنى.

طرق التوقيع بالتشفير غير المتماثل، مثل RSA، تستخدم مفاتيح مختلفة للتوقيع والتحقق من الرمز، مما يجعل من الممكن إنشاء رموز بمفتاح خاص، ويسمح في نفس الوقت لأي شخص يستخدم المفتاح العام للتحقق بالوصول الطبيعي.

أنواع المفاتيح المطلوبة تختلف باختلاف خوارزميات التوقيع، وفيما يلي أنواع بعض خوارزميات التوقيع الشائعة:

  • HMAC: تشفير متماثل، يتطلب قيمة من نوع []byte للتوقيع والتحقق. (HS256، HS384، HS512)
  • RSA: تشفير غير متماثل، يتطلب قيمة من نوع *rsa.PrivateKey للتوقيع، وقيمة من نوع *rsa.PublicKey للتحقق. (RS256، RS384، RS512)
  • ECDSA: تشفير غير متماثل، يتطلب قيمة من نوع *ecdsa.PrivateKey للتوقيع، وقيمة من نوع *ecdsa.PublicKey للتحقق. (ES256، ES384، ES512)
  • EdDSA: تشفير غير متماثل، يتطلب قيمة من نوع ed25519.PrivateKey للتوقيع وقيمة من نوع ed25519.PublicKey للتحقق. (Ed25519)

أمثلة

فيما يلي عرض لبعض الأمثلة حول إنشاء وتوقيع JWT، وتحليله والتحقق منه.

go
type Token struct {
  Raw       string                 // سلسلة الرمز الأصلية، يتم ملء هذا الحقل عند بدء التحليل
  Method    SigningMethod          // الطريقة المستخدمة في التوقيع
  Header    map[string]interface{} // جزء الرأس من JWT
  Claims    Claims                 // جزء الحمولة من JWT
  Signature string                 // جزء التوقيع من JWT، يتم ملء هذا الحقل عند بدء التحليل
  Valid     bool                   // هل JWT صالح قانونيًا
}

هيكل Token يمثل رمز JWT، واستخدام الحقول يعتمد بشكل رئيسي على كيفية إنشاء/توقيع أو تحليل/تحقق JWT.

go
type RegisteredClaims struct {
   // the `iss` (Issuer) claim. See https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.1
   Issuer string `json:"iss,omitempty"`

   // the `sub` (Subject) claim. See https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.2
   Subject string `json:"sub,omitempty"`

   // the `aud` (Audience) claim. See https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.3
   Audience ClaimStrings `json:"aud,omitempty"`

   // the `exp` (Expiration Time) claim. See https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.4
   ExpiresAt *NumericDate `json:"exp,omitempty"`

   // the `nbf` (Not Before) claim. See https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.5
   NotBefore *NumericDate `json:"nbf,omitempty"`

   // the `iat` (Issued At) claim. See https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.6
   IssuedAt *NumericDate `json:"iat,omitempty"`

   // the `jti` (JWT ID) claim. See https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.7
   ID string `json:"jti,omitempty"`
}

هذه Claims المُعرَّفة مسبقًا في المكتبة، ويمكن استخدامها بشكل مناسب لتلبية المتطلبات.

مثال 1: إنشاء وتوقيع HMAC

go
func TestHmac(t *testing.T) {
   // نوع مفتاح hmac هو مصفوفة بايت
   secret := []byte("my secret")
   // استخدام خوارزمية HS256، jwt.MapClaims هي الحمولة
   token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
      "id":   123456,
      "name": "jack",
   })
   fmt.Printf("%+v\n", *token)
   // التوقيع
   signedString, err := token.SignedString(secret)
   fmt.Println(signedString, err)
}

الخرج:

{Raw: Method:0xc000008150 Header:map[alg:HS256 typ:JWT] Claims:map[id:123456 name:jack] Signature: Valid:false}
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
eyJpZCI6MTIzNDU2LCJuYW1lIjoiamFjayJ9.
QxLw9NkFgZW3BluyXIofe4efp1IAy61s8b2fe3Eo86M
<nil>

مثال 2: استخدام Claims المُعرَّفة مسبقًا

go
mySigningKey := []byte("AllYourBase")

// إنشاء Claims
claims := &jwt.RegisteredClaims{
  ExpiresAt: jwt.NewNumericDate(time.Unix(1516239022, 0)),
  Issuer:    "test",
}

token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
ss, err := token.SignedString(mySigningKey)
fmt.Printf("%v %v", ss, err)

الخرج:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
eyJpc3MiOiJ0ZXN0IiwiZXhwIjoxNTE2MjM5MDIyfQ.
0XN_1Tpp9FszFOonIBpwha0c_SfnNI22DhTnjMshPg8
<nil>

مثال 3: Claims مخصصة

go
type MyClaims struct {
   User string `json:"user"`
   jwt.RegisteredClaims
}

func TestCustomClaims(t *testing.T) {
   // إنشاء المفتاح
   secret := []byte("my secret")
   // إنشاء Claims
   claims := MyClaims{
      User: "114514",
      RegisteredClaims: jwt.RegisteredClaims{
         ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Hour)),
         IssuedAt:  jwt.NewNumericDate(time.Now()),
         NotBefore: jwt.NewNumericDate(time.Now()),
         Issuer:    "Server",
      },
   }
   // إنشاء Token
   token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
   // التوقيع
   signedString, err := token.SignedString(secret)
   fmt.Println(signedString, err)
}

الخرج:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
eyJ1c2VyIjoiMTE0NTE0IiwiaXNzIjoiU2VydmVyIiwiZXhwIjoxNjczMDg1Nzk2LCJuYmYiOjE2NzMwODIxOTYsImlhdCI6MTY3MzA4MjE5Nn0.
PdPXdQBbDuYtE4ENXzoAcrW-dBSxqsufeYXCT5zTwVI
<nil>

TIP

عند تضمين Claims القياسية في Claims المخصصة، يجب التأكد من:

  1. Claims القياسية المضمنة من نوع غير مؤشر

  2. إذا كانت من نوع مؤشر، من الأفضل التأكد من تخصيص ذاكرة مناسبة لها قبل التمرير، وإلا سيحدث panic.

مثال 4: تحليل والتحقق من Token باستخدام HMAC

go
func TestParse(t *testing.T) {
   secret := []byte("my secret")
   // لنفترض أنه تم إنشاء رمز وتوقيعه باستخدام خوارزمية HS256
   tokenString := "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MTIzNDU2LCJuYW1lIjoiamFjayJ9.QxLw9NkFgZW3BluyXIofe4efp1IAy61s8b2fe3Eo86M"

   // تمرير سلسلة الرمز ودالة خطاف التحقق، القيمة المرجعة هي هيكل Token
   token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
      // التحقق من تطابق خوارزمية التوقيع
      if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
         return nil, fmt.Errorf("خوارزمية توقيع غير متطابقة: %s", token.Header["alg"])
      }

      // إرجاع مفتاح التحقق
      return secret, nil
   })
   if err != nil {
      fmt.Println(token, err)
   }

   if claims, ok := token.Claims.(jwt.MapClaims); ok && token.Valid {
      fmt.Println(claims)
   } else {
      fmt.Println(err)
   }

}

الخرج:

map[id:123456 name:jack]

مثال 5: معالجة الأخطاء

go
func TestProcess(t *testing.T) {
   secret := []byte("my secret")
   // لنفترض أنه تم إنشاء رمز وتوقيعه باستخدام خوارزمية HS256
   tokenString := "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MTIzNDU2LCJuYW1lIjoiamFjayJ9.QxLw9NkFgZW3BluyXIofe4efp1IAy61s8b2fe3Eo86M"

   // تمرير سلسلة الرمز ودالة خطاف التحقق، القيمة المرجعة هي هيكل Token
   token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
      // التحقق من تطابق خوارزمية التوقيع
      if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
         return nil, fmt.Errorf("خوارزمية توقيع غير متطابقة: %s", token.Header["alg"])
      }
      // إرجاع مفتاح التحقق
      return secret, nil
   })

   if token.Valid {
      fmt.Println("الرمز قانوني")
   } else if errors.Is(err, jwt.ErrTokenMalformed) {
      fmt.Println("السلسلة المُمررة ليست حتى رمزًا...")
   } else if errors.Is(err, jwt.ErrTokenExpired) || errors.Is(err, jwt.ErrTokenNotValidYet) {
      fmt.Println("الرمز منتهي الصلاحية أو لم يبدأ سريانه بعد")
   } else {
      fmt.Println("معالجة الرمز غير طبيعية...")
   }
}

الخرج:

الرمز قانوني

مثال 6: تحليل Claims المخصصة

إذا تم استخدام Claims مخصصة عند إنشاء Token، فعند التحليل إذا كنت تريد أن يتم تحويل Claims مباشرة إلى Claims مخصصة بدلاً من map، فيجب تمرير Claims المخصصة.

go
func TestCustomClaimsParse(t *testing.T) {
  secret := []byte("my secret")
  tokenstring := "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyIjoiMTE0NTE0IiwiaXNzIjoiU2VydmVyIiwiZXhwIjoxNjczMDg4MDU2LCJuYmYiOjE2NzMwODQ0NTYsImlhdCI6MTY3MzA4NDQ1Nn0.T245aoDeL2x19X8_JZde0EmZ2TDyIgr1u3ddKFjQmgw"
  token, err := jwt.ParseWithClaims(tokenstring, &MyClaims{}, func(token *jwt.Token) (interface{}, error) {
    return secret, nil
  }, jwt.WithValidMethods([]string{"HS256"})) // استخدام option للتحقق

    // تأكيد النوع
  if claims, ok := token.Claims.(*MyClaims); ok && token.Valid {
    fmt.Println(claims)
  } else {
    fmt.Println(err)
  }
}

الخرج:

&{114514 {Server  [] 2023-01-07 18:40:56 +0800 CST 2023-01-07 17:40:56 +0800 CST 2023-01-07 17:40:56 +0800 CST }}

مثال 7: توقيع وتحليل RSA

يُستخدم RSA كثيرًا في البنى الموزعة، والعملية التقريبية كالتالي:

  1. مركز المصادقة ينشئ زوج مفاتيح، ويستخدم المفتاح الخاص لتوقيع JWT، ويعيد JWT للعميل، بينما المفتاح العام يحمله خدمة الأعمال
  2. العميل يحمل JWT ويقدم طلبًا لخدمة الأعمال، ووحدة الأعمال تستخدم المفتاح العام لتحليل JWT، دون الحاجة للوصول لمركز المصادقة
  3. إذا نجحت المصادقة تُعاد معلومات الأعمال
  4. إذا فشلت المصادقة تُعاد معلومات الفشل
go
func TestRsa(t *testing.T) {

   // إنشاء زوج مفاتيح
   privateKey, err := rsa.GenerateKey(rand.Reader, 2048)
   publicKey := &privateKey.PublicKey

   if err != nil {
      fmt.Println(err)
      return
   }

   // claims
   claims := MyClaims{
      User: "114514",
      RegisteredClaims: jwt.RegisteredClaims{
         ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Hour)),
         IssuedAt:  jwt.NewNumericDate(time.Now()),
         NotBefore: jwt.NewNumericDate(time.Now()),
         Issuer:    "Server",
      },
   }

   token := jwt.NewWithClaims(jwt.SigningMethodRS256, claims)
   // تشفير بالمفتاح الخاص
   signedString, err := token.SignedString(privateKey)

   fmt.Println(signedString, err)

   // فك التشفير بالمفتاح العام
   token, err = jwt.ParseWithClaims(signedString, &MyClaims{}, func(token *jwt.Token) (interface{}, error) {
      return publicKey, nil
   })

   if err != nil {
      fmt.Println(err)
   } else if claims, ok := token.Claims.(*MyClaims); ok && token.Valid {
      fmt.Println(claims)
   }
}

Golang تم تحريره بواسطة www.golangdev.cn