JWT
أصبح JWT أحد الطرق الرئيسية للمصادقة في الاتصالات الحديثة من جانب الخادم، ويتميز بخفة الوزن وعدم وجود حالة.
مقدمة
الاسم الكامل لـ JWT هو JSON Web Tokens، ويمكن الاطلاع على مقدمة تفصيلية عن JWT في jwt.io، وهو طريقة مفتوحة وآمنة ومدمجة لنقل المعلومات بين طرفي خدمة باستخدام كائن JSON كحامل، وخصائصه هي الأمان العالي، ومنع التلاعب بالمحتوى، وانخفاض الاستهلاك.
الهيكل
في معيار RFC، يتكون JWT من الأجزاء الثلاثة التالية:
- Header (الرأس)
- Payload (الحمولة)
- Signature (التوقيع)
ثم يتم فصل كل جزء بنقطة .، وأخيرًا يتشكل سلسلة، والتنسيق هو header.payload.signature، وهذا هو الهيكل القياسي لرمز JWT، وفيما يلي شرح لكل هيكل.
TIP
تجدر الإشارة إلى أن base64 و base64URL ليسا نفس طريقة الترميز، فالأخير متوافق مع عنوان URL للويب، وقد تم تهربيه.
الرأس
الرأس يصرح فقط ببعض المعلومات الأساسية، ويتكون عادةً من جزأين، نوع الرمز، وخوارزمية التشفير المستخدمة في التوقيع، على سبيل المثال:
{
"alg": "HS256",
"typ": "JWT"
}المعلومات أعلاه تعني تقريبًا أن نوع الرمز هو JWT، وخوارزمية التشفير المستخدمة في جزء التوقيع هي HS256، ثم يتم ترميز كائن JSON إلى سلسلة عبر Base64Url، وهذه السلسلة هي رأس JWT.
الحمولة
الجزء الثاني من JWT هو جزء الحمولة، ويحتوي بشكل رئيسي على جزء الإعلانات (claims)، وعادةً ما تكون الإعلانات بيانات عن كيان ما، مثل مستخدم. هناك ثلاثة أنواع من الإعلانات:
reigstered: تمثلRegistered claimsبعض الإعلانات المُعرَّفة مسبقًا، بعضها ليس إلزاميًا ولكن لا يزال يُوصى باستخدامه، مثل:iss(الجهة المُصدرة)، وexp(وقت الانتهاء)، وaud(الجمهور).public: يمكن لـPublic claimsأن يُعرِّفها مستخدمو JWT بحرية، ومن الأفضل تجنب التعارض مع أجزاء الإعلانات الأخرى.private claims:这部分声明同样是自定义的,通常用于在服务双方共享一些信息。
مثال على حمولة:
{
"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الاستيراد
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، وتحليله والتحقق منه.
type Token struct {
Raw string // سلسلة الرمز الأصلية، يتم ملء هذا الحقل عند بدء التحليل
Method SigningMethod // الطريقة المستخدمة في التوقيع
Header map[string]interface{} // جزء الرأس من JWT
Claims Claims // جزء الحمولة من JWT
Signature string // جزء التوقيع من JWT، يتم ملء هذا الحقل عند بدء التحليل
Valid bool // هل JWT صالح قانونيًا
}هيكل Token يمثل رمز JWT، واستخدام الحقول يعتمد بشكل رئيسي على كيفية إنشاء/توقيع أو تحليل/تحقق JWT.
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
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 المُعرَّفة مسبقًا
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 مخصصة
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 المخصصة، يجب التأكد من:
Claims القياسية المضمنة من نوع غير مؤشر
إذا كانت من نوع مؤشر، من الأفضل التأكد من تخصيص ذاكرة مناسبة لها قبل التمرير، وإلا سيحدث panic.
مثال 4: تحليل والتحقق من Token باستخدام HMAC
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: معالجة الأخطاء
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 المخصصة.
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 كثيرًا في البنى الموزعة، والعملية التقريبية كالتالي:
- مركز المصادقة ينشئ زوج مفاتيح، ويستخدم المفتاح الخاص لتوقيع JWT، ويعيد JWT للعميل، بينما المفتاح العام يحمله خدمة الأعمال
- العميل يحمل JWT ويقدم طلبًا لخدمة الأعمال، ووحدة الأعمال تستخدم المفتاح العام لتحليل JWT، دون الحاجة للوصول لمركز المصادقة
- إذا نجحت المصادقة تُعاد معلومات الأعمال
- إذا فشلت المصادقة تُعاد معلومات الفشل
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)
}
}