حسین خیراله پور
حسین خیراله پور
خواندن ۵ دقیقه·۱ سال پیش

پیاده سازی احراز هویت با JWT در اندروید(بدون استفاده از کتابخانه)

سلام سلااااام

امیدوارم حالتون خوب باشه ... امروز میخوام نحوه پیاده سازی JWT در اندروید رو بدون استفاده از کتابخونه جانبی بهتون آموزش بدم .

همونطور که میدونید کتابخونه های زیادی برای استفاده از JWT وجود داره؛ ولی چون از یه استانداردی پیروی میکنه طبعا پیاده سازی اون ثابت هست و نیاز به استفاده از کتابخونه و بروزرسانی های بعدش نیست. مزیت بعدی اینه که از اضافه کردن dependency به پروژه تون جلوگیری میکنه و در نهایت دید عمیق تری نسبت به این موضوع پیدا خواهید کرد.

در ابتدا یه توضیح مختصری راجع به ساختار JWT به شما میدم و بعدش میرم سراغ پیاده سازی عملی اون تو اندروید. تمامی سورس کدها در این لینک GitHub موجود هست.(لینک داکیومنت رسمی)

جیسان وب توکن یا JWT چیست ؟

مخفف Json Web Token می باشد که در واقع یک استاندارد و مکانیزم احراز هویت توی وب سایت ها و نرم افزارها هست. از سه بخش اصلی Header, Payload, Signature تشکیل شده:

  • بخش Header: حاوی اطلاعاتی بصورت جیسان بیس۶۴ انکد شده که درباره نوع توکن (معمولاً JWT) و الگوریتم رمزنگاری مورد استفاده (مانند HS256 یا RS256) است. شامل: نوع الگوریتم با کلید alg؛ نوع محتوا با کلید cty؛ نوع توکن با کلید typ و شناسه کلید با کلید kid می باشد.
  • بخش Payload: این بخش حاوی ادعاها (claims) است که اطلاعاتی به صورت جیسان بیس۶۴ انکد شده که درباره کاربر یا دیگر موارد مورد نیاز برای احراز هویت را فراهم می‌کند. این اطلاعات می‌توانند شامل شناسه کاربر، نام کاربری، تاریخ انقضای توکن و غیره باشند.حاوی اطلاعاتی درباره نوع توکن (معمولاً JWT) و الگوریتم رمزنگاری مورد استفاده (مانند HS256 یا RS256) است. شامل: صادر کننده توکن با کلید iss؛ موضوع توکن با کلید sub؛ مخاطب توکن با کلید aud؛ زمان انقضای توکن به ثانیه با کلید exp؛ زمانی که قبل از نباید توکن پذیرفته بشه به ثانیه با کلید nbf؛ زمانی که توکن صادر شده به ثانیه با کلید iat؛ شناسه منحصر به فرد توکن با کلید jti می باشد.
  • بخش Signature: این بخش با استفاده از الگوریتم مشخص شده در Header و یک کلید مخفی (در الگوریتم‌های مبتنی بر HMAC) یا یک جفت کلید عمومی/خصوصی (در الگوریتم‌های مبتنی بر RSA یا ECDSA) ایجاد می‌شود. امضای توکن اطمینان می‌دهد که پیام تغییر نکرده و از منبع معتبر است. که به زبان ساده مقدار رمز شده عبارت (header.payload) هست.

درنهایت ساختار یکپارچه و کامل یه توکن JWT بصورت زیر هست:

val jwt = "$header.$payload.$signature"



سناریوی ما چیه ؟

فرض کنید که ما میخواییم بین کلاینت اندروید و سرور یه احراز هویتی صورت بگیره. به این صورت که ما یه جفت کلید ایجاد میکنیم و کلید عمومی رو به سرور ارسال میکنیم. تو یه درخواست دیگه سرور به ما یه سری اطلاعات رو ارسال میکنه که ما با استفاده از اونا توکن JWT رو ایجاد میکنیم و با استفاده از کلید خصوصی ایجاد شده تو مرحله اول رمزنگاری بخش Signature رو تولید میکنیم و درنهایت توکن کامل ایجاد شده رو برای سرور میفرستیم و از اون طرف میتونه بخش Signature و (Header.Payload) رو با کلید عمومی که قبلا براش ارسال شده Verify کنه و احراز هویت مورد تایید قرار بگیره.



بریم سراغ پیاده سازی :)

۱.تولید کلید: الگوریتم انتخابی ما برای رمزنگاری بخش سوم یا signature ؛ elliptic curve با طول کلید ۲۵۶ هست که تو استاندارد به اسم ES256 شناخته میشه. کلید عمومی رو به سرور میفرستیم و کلید خصوصی رو برای رمزنگاری مرحله ۴ نگه میداریم. کد نحوه تولید کلید بصورت زیر هست.

val keyPairGenerator = KeyPairGenerator.getInstance(&quotEC&quot) keyPairGenerator.initialize(ECGenParameterSpec(&quotsecp256r1&quot)) 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(&quotES256&quot) // elliptic curve with key size 256 type(&quotJWT&quot) // Needed contentType(&quotapplication/json&quot) // Optional keyId(&quotUniqueKeyId&quot) // Optional }.build()


val currentTime = System.currentTimeMillis() / 1000 // current time in seconds val payload = JwtPayloadBuilder().run { issuer(&quotHossein KheirollahPour&quot) subject(&quotPayment&quot) audience(&quothttps://blubank.com&quot) issuedAt(currentTime) notBefore(currentTime) expirationTime(currentTime + 3600)// Expires after 1 hour jwtID(&quotUniqueJwtId1234&quot) }.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(&quotSHA256withECDSA&quot) 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(&quot%s.%s.%s&quot, 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(&quot-----BEGIN PUBLIC KEY-----\n&quot) append(base64Encoded.chunked(64).joinToString(&quot\n&quot)) append(&quot\n-----END PUBLIC KEY-----\n&quot) } }

یه راه دومی هم برای وریفای هست که من براش یه فانکشن نوشتم که اگر خواستید میتونید ازش استفاده کنید:

@RequiresApi(Build.VERSION_CODES.O) fun manualVerifyJWT(publicKey: ECPublicKey, jwt: String): Boolean { val parts = jwt.split(&quot.&quot) if (parts.size != 3) return false val signature = Signature.getInstance(&quotSHA256withECDSA&quot) signature.initVerify(publicKey) signature.update(&quot${parts[0]}.${parts[1]}&quot.toByteArray()) return signature.verify(Base64.getUrlDecoder().decode(parts[2])) }

یه اسکرین شات از خروجی اپ اندروید موجود در گیت هاب براتون میزارم که ببینید:


در نهایت بازم از اینکه این مقاله رو مطالعه و استفاده میکنید تشکر میکنم. امیدوارم که به درک عمیق تر شما از نحوه تولید JWT کمک کرده باشه و بتونید از کدهاش استفاده کنید و نیازی به کتابخانه نداشته باشید. منتظر نظرات شما هستم که قطعا باعث رشد و پیشرفت من خواهد شد.

اندرویدامنیتاحراز هویترمزنگاری
Senior Android Engineer at blu Bank
شاید از این پست‌ها خوشتان بیاید