احراز هویت
احراز هویت روی OTP پیامکی + JWT دوتوکنی استوار است، و برای اینکه کاربر بتواند همزمان از چند دستگاه وارد شود، بهصورت مبتنی بر دستگاه (device-aware) طراحی شده.
جریان ورود (OTP)
ورود دو مرحله دارد:
POST /api/auth/send-otp— با گرفتنِphone، یک کد یکبارمصرف ۶ رقمی از طریق کاوهنگار پیامک میشود.POST /api/auth/verify-otp— باphone+otp، کد اعتبارسنجی میشود. اگر شماره جدید باشد، کاربر و یک تقویم پیشفرض بهصورت خودکار برایش ساخته میشوند. پاسخ شاملِ یکaccessTokenو یکrefreshTokenاست، و اگرdeviceهم فرستاده باشید، یکDeviceDtoهم برمیگردد.
مسیر POST /api/auth/login هم برای ورود با ایمیل/رمز موجود است و همان شکلِ
پاسخ را میدهد.
روی خط فرمان:
# ۱) ارسال OTP
curl -X POST http://localhost:5000/api/auth/send-otp \
-H "Content-Type: application/json" \
-d '{"phone": "09123456789", "name": "Test User"}'
# ۲) اعتبارسنجی OTP (کد را از پاسخ یا SMS بگیرید)
curl -X POST http://localhost:5000/api/auth/verify-otp \
-H "Content-Type: application/json" \
-d '{"phone": "09123456789", "otp": "123456"}'
و پاسخِ verify-otp این شکلی است:
{
"success": true,
"data": {
"accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"refreshToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"isNewUser": false,
"user": {
"id": 1,
"name": "John Doe",
"email": "john@example.com",
"phone": "09123456789",
"role": "USER",
"isPhoneVerified": true,
"notificationSettings": {
"syncNotifications": true,
"eventReminders": true,
"conflictAlerts": true
}
}
}
}
توکنهای JWT
| توکن | طول عمر | کاربرد |
|---|---|---|
| Access token | کوتاه (پیشفرض ۱۵ دقیقه) | در هدر Authorization: Bearer <accessToken> برای تمام درخواستهای محافظتشده |
| Refresh token | طولانی (پیشفرض ۹۰ روز) | فقط برای گرفتن access token جدید از POST /api/auth/refresh |
POST /api/auth/refresh— باrefreshToken، یک access token جدید میدهد و همزمان refresh token را چرخش (rotate) میکند؛ یعنی هر بار یک refresh تازه صادر و ذخیره میشود.POST /api/auth/logout— refresh token را باطل میکند.GET /api/auth/me— اطلاعات کاربر فعلی را برمیگرداند.
نمونهٔ کامل روی خط فرمان:
# استفاده از access token
curl -X GET http://localhost:5000/api/auth/me \
-H "Authorization: Bearer <access-token>"
# چرخش refresh token
curl -X POST http://localhost:5000/api/auth/refresh \
-H "Content-Type: application/json" \
-d '{"refreshToken": "<refresh-token>"}'
# خروج (ابطال refresh token)
curl -X POST http://localhost:5000/api/auth/logout \
-H "Authorization: Bearer <access-token>"
حالت مبتنی بر دستگاه (device-aware)
برای اینکه چند دستگاه همزمان کار کنند، هر دستگاه با یک uid (که کلاینت
میسازد) ثبت میشود:
- در
verify-otpیاloginمیتوانید یک شیءdevice: { uid, platform, name, fcmToken }بفرستید؛ آنگاه یک refresh token جداگانه مخصوص همان دستگاه صادر میشود. POST /api/auth/refreshاگرdeviceUidبگیرد، فقط refresh مخصوص همان دستگاه را چرخش میدهد و access token صادرشده هم حاویِdeviceUidخواهد بود.- تشخیص replay: اگر یک refresh tokenِ چرخشیافته (یعنی قبلاً جایگزین شده) دوباره استفاده شود، سرور این را نشانهٔ سوءاستفاده میداند و کل خانوادهٔ refresh آن دستگاه را باطل میکند — کاربر مجبور به ورود دوباره میشود.
- برای فهرستکردن یا ابطالِ دستگاهها به صفحهٔ دستگاهها مراجعه کنید.
مسیرهای قدیمی (بدون device) هم همچنان کار میکنند و از توکنِ ذخیرهشده روی
User استفاده میکنند.
برای شکلِ دقیقِ request/response به مرجع API مراجعه کنید.
پیادهسازی سمت کلاینت (الگوی refresh خودکار)
الگوی پیشنهادی وقتی access token منقضی شد: یکبار POST /api/auth/refresh
بزنید، token جدید را ذخیره کنید، و درخواستِ اصلی را با token جدید دوباره
ارسال کنید. اگر refresh هم شکست خورد، کاربر را به صفحهٔ ورود هدایت کنید.
این retry را فقط یکبار اجرا کنید، وگرنه حلقهٔ بینهایت میسازد. در مدل
مبتنی بر دستگاه، هنگام refresh مقدار deviceUid را هم بفرستید تا refresh مخصوص
همان دستگاه چرخش کند.
class AuthManager {
// access/refresh token را در EncryptedSharedPreferences نگه دارید
suspend fun makeAuthenticatedRequest(request: suspend () -> Response): Response {
var response = request()
if (response.code == 401) {
refreshAccessToken() // POST /api/auth/refresh با deviceUid
response = request() // یکبار retry با token جدید
}
return response
}
}
خطاهایی که در این مسیر معمولاً میبینید:
| وضعیت HTTP | معنی | اقدام |
|---|---|---|
401 (access منقضی) | access token تمام شده | refresh بزنید و درخواست را دوباره تلاش کنید |
401 (refresh نامعتبر) | refresh token باطل یا قبلاً استفاده شده | کاربر را مجبور به ورود دوباره کنید |
403 | نوع توکن اشتباه یا دسترسی ممنوع | کاربر را به صفحهٔ ورود هدایت کنید |
بهترین رویههای امنیتی
- Access token را در حافظه یا storage کوتاهمدت نگه دارید؛ عمرش کوتاه است (پیشفرض ۱۵ دق یقه) پس ریسکِ نگهداریاش هم کم است.
- Refresh token را فقط در storage رمزنگاریشده بگذارید — مثل
EncryptedSharedPreferencesدر اندروید، یا localStorage با رمزگذاری در وب. هرگز در متنِ ساده. - همیشه انقضای token را اعتبارسنجی کنید و شکستِ refresh را بهخوبی هندل کنید.
متغیرهای محیطی مربوطه
JWT_SECRET=... # کلید امضای access token
JWT_REFRESH_SECRET=... # کلید امضای refresh token (جدا)
JWT_ACCESS_EXPIRES_IN=15m # عمر access (پیشفرض ۱۵ دقیقه)
JWT_REFRESH_EXPIRES_IN=90d # عمر refresh (پیشفرض ۹۰ روز)