سلام سلااااام
امیدوارم حالتون خوب باشه ... امروز میخوام نحوه پیاده سازی JWT در اندروید رو بدون استفاده از کتابخونه جانبی بهتون آموزش بدم .
همونطور که میدونید کتابخونه های زیادی برای استفاده از JWT وجود داره؛ ولی چون از یه استانداردی پیروی میکنه طبعا پیاده سازی اون ثابت هست و نیاز به استفاده از کتابخونه و بروزرسانی های بعدش نیست. مزیت بعدی اینه که از اضافه کردن dependency به پروژه تون جلوگیری میکنه و در نهایت دید عمیق تری نسبت به این موضوع پیدا خواهید کرد.
در ابتدا یه توضیح مختصری راجع به ساختار JWT به شما میدم و بعدش میرم سراغ پیاده سازی عملی اون تو اندروید. تمامی سورس کدها در این لینک GitHub موجود هست.(لینک داکیومنت رسمی)
مخفف Json Web Token می باشد که در واقع یک استاندارد و مکانیزم احراز هویت توی وب سایت ها و نرم افزارها هست. از سه بخش اصلی Header, Payload, Signature تشکیل شده:
درنهایت ساختار یکپارچه و کامل یه توکن JWT بصورت زیر هست:
فرض کنید که ما میخواییم بین کلاینت اندروید و سرور یه احراز هویتی صورت بگیره. به این صورت که ما یه جفت کلید ایجاد میکنیم و کلید عمومی رو به سرور ارسال میکنیم. تو یه درخواست دیگه سرور به ما یه سری اطلاعات رو ارسال میکنه که ما با استفاده از اونا توکن JWT رو ایجاد میکنیم و با استفاده از کلید خصوصی ایجاد شده تو مرحله اول رمزنگاری بخش Signature رو تولید میکنیم و درنهایت توکن کامل ایجاد شده رو برای سرور میفرستیم و از اون طرف میتونه بخش Signature و (Header.Payload) رو با کلید عمومی که قبلا براش ارسال شده Verify کنه و احراز هویت مورد تایید قرار بگیره.
۱.تولید کلید: الگوریتم انتخابی ما برای رمزنگاری بخش سوم یا signature ؛ elliptic curve با طول کلید ۲۵۶ هست که تو استاندارد به اسم ES256 شناخته میشه. کلید عمومی رو به سرور میفرستیم و کلید خصوصی رو برای رمزنگاری مرحله ۴ نگه میداریم. کد نحوه تولید کلید بصورت زیر هست.
val keyPairGenerator = KeyPairGenerator.getInstance("EC") keyPairGenerator.initialize(ECGenParameterSpec("secp256r1")) val keyPair = keyPairGenerator.generateKeyPair() val publicKey = keyPair.public as ECPublicKey // Send to server val privateKey = keyPair.private as ECPrivateKey // Use for signing in step 4
۲. تولید Claims: ما برای تولید header و payload از ویژگی های زیر استفاده میکنیم.
val header = JwtHeaderBuilder().run { algorithm("ES256") // elliptic curve with key size 256 type("JWT") // Needed contentType("application/json") // Optional keyId("UniqueKeyId") // Optional }.build()
val currentTime = System.currentTimeMillis() / 1000 // current time in seconds val payload = JwtPayloadBuilder().run { issuer("Hossein KheirollahPour") subject("Payment") audience("https://blubank.com") issuedAt(currentTime) notBefore(currentTime) expirationTime(currentTime + 3600)// Expires after 1 hour jwtID("UniqueJwtId1234") }.build()
۳. آماده سازی داده از جیسان header , payload: ما تو این بخش جیسان هر بخش رو به بیس۶۴ انکد میکنیم و در نهایت اونها رو باهم ترکیب میکنیم و داده نهایی برای رمزنگاری مرحله بعد رو آماده میکنیم.
val headerBytes = Base64.getUrlEncoder().withoutPadding().encodeToString(header.toByteArray(StandardCharsets.UTF_8)).toByteArray(StandardCharsets.UTF_8) val payloadBytes = Base64.getUrlEncoder().withoutPadding().encodeToString(payload.toByteArray(StandardCharsets.UTF_8)).toByteArray(StandardCharsets.UTF_8) val contentBytes = ByteArray(headerBytes.size + 1 + payloadBytes.size) System.arraycopy(headerBytes, 0, contentBytes, 0, headerBytes.size) contentBytes[headerBytes.size] = '.'.code.toByte() System.arraycopy(payloadBytes, 0, contentBytes, headerBytes.size + 1, payloadBytes.size)
۴. آماده سازی داده signature: در این بخش ما با استفاده از الگوریتم SHA256ECDSA و کلید خصوصی ایجاد شده تو مرحله اول؛ داده مرحله قبل رو ساین میکنیم.
نکته: در نهایت که داده رو آماده کردیم با استفاده از فانکشن DERToJOSE به فرمت درست تبدیل میکنیم که برای جلوگیری از طولانی شدن کدها اینجا قید نکردم ولی داخل این بخش سورس کد موجود هست.
val signatureInstance = Signature.getInstance("SHA256withECDSA") signatureInstance.initSign(privateKey) signatureInstance.update(contentBytes) val signatureBytes = signatureInstance.sign() val convertedSignatureBytes = DERToJOSE(signatureBytes) // This function exist in source code val signature = Base64.getUrlEncoder().withoutPadding().encodeToString(convertedSignatureBytes)
۵. ساخت توکن نهایی: در این بخش داده های تولید شده توی مراحل قبل به هم متصل میشن.
val jwt = String.format("%s.%s.%s", header, payload, signature)
خیلی عالی شد .. بالاخره توکن JWT نهایی رو ساختیم :)
در نهایت شما میتونید با استفاده از کد زیر کلید عمومی خودتون رو استخراج کنید و در سایت هایی مثل این سایت و این سایت و این سایت توکن JWT خودتون رو verify یا تست کنید و ببینید که درست تولید شده یا خیر.
@RequiresApi(Build.VERSION_CODES.O) fun convertECPublicKeyToPEM(ecPublicKey: ECPublicKey): String { val base64Encoded = Base64.getEncoder().encodeToString(ecPublicKey.encoded) return buildString { append("-----BEGIN PUBLIC KEY-----\n") append(base64Encoded.chunked(64).joinToString("\n")) append("\n-----END PUBLIC KEY-----\n") } }
یه راه دومی هم برای وریفای هست که من براش یه فانکشن نوشتم که اگر خواستید میتونید ازش استفاده کنید:
@RequiresApi(Build.VERSION_CODES.O) fun manualVerifyJWT(publicKey: ECPublicKey, jwt: String): Boolean { val parts = jwt.split(".") if (parts.size != 3) return false val signature = Signature.getInstance("SHA256withECDSA") signature.initVerify(publicKey) signature.update("${parts[0]}.${parts[1]}".toByteArray()) return signature.verify(Base64.getUrlDecoder().decode(parts[2])) }
یه اسکرین شات از خروجی اپ اندروید موجود در گیت هاب براتون میزارم که ببینید:
در نهایت بازم از اینکه این مقاله رو مطالعه و استفاده میکنید تشکر میکنم. امیدوارم که به درک عمیق تر شما از نحوه تولید JWT کمک کرده باشه و بتونید از کدهاش استفاده کنید و نیازی به کتابخانه نداشته باشید. منتظر نظرات شما هستم که قطعا باعث رشد و پیشرفت من خواهد شد.