Redis bilan tayyorlangan bir martalik kod SSO

Redis bilan tayyorlangan bir martalik kod SSO

- Tokenning toʻgʻridan-toʻgʻri ko‘rsatishni kamaytirish uchun vaqtinchalik kod berish, iste'mol qilish, takroriy so'rovlarni boshqarish tajribasi -

1. Amalga oshirish fonini

Ushbu ish ichki ish tizimidan alohida qo‘lda kirishsiz so‘rov xizmat ekraniga kirish uchun SSO oqimini tashkil etish jarayonida boshlandi. Foydalanuvchi ichki ish tizimida ma'lum menyu yoki maqsad ekranini tanlashi va xizmat ekraniga tabiiy ravishda o‘tishi kerak edi. Dastlab, tasdiqlash serveridan olingan accessToken va refreshToken to‘g‘ridan-to‘g‘ri javob sifatida berilsa, bu oddiy echimdek ko‘rindi.

Biroq, token haqiqiy tasdiqlash malumoti hisoblanadi. Bu qiymat tashqi javob, URL so'rovi, brauzer tarixi, server loglari, skrinshotlar, messenger taqsimoti kabi joylarda qolsangiz, xavfsizlik uchun xavfli bo‘ladi. Ayniqsa, ekran o‘zgartirish uchun brauzer ochiladigan tuzilma bo‘lsa, URL yoki front logda qiymat qolish ehtimolini ham hisobga olish kerak edi. Shu sababli, tokenni to'g'ridan-to'g'ri uzatmasdan, qisqa vaqt davomida amal qiladigan bir martalik loginCode'ni uzatishga va frontga alohida tasdiqlash API orqali tokenni almashish tuzilmasini tanladi.

Ushbu maqolada haqiqiy ishda qo'llaniladigan tuzilmani ochiq blogga moslashtirib umumlashtirib tushuntiraman. Haqiqiy loyiha paket nomi, ichki domen, server manzili, kasalxona nomi, bemor identifikatsiya qiymatlari kabi narsalar oshkor qilinmaydi, texnologiya tanlovining foni va muammoni hal qilish jarayoni bilan bog'liq bo'ladi.

2. Umumiy tuzilma

Ishlab chiqish tuzilmasi asosan to‘rt sohadan iborat. Birinchisi ichki ish tizimidagi so'rovni olib login kodini beruvchi adapter vazifasini bajaruvchi serverdir. Ikkinchisi loginCode va tokenJsonni vaqtincha saqlovchi Redisdir. Uchinchisi frontdan kelgan loginCode'ni iste'mol qilib tokenni qaytaruvchi account rolidagi tasdiqlash serveridir. Nihoyat, front URI so'rov parametrlarida loginCode'ni o'qib consume (iste'mol qilish) API'ni chaqirib tokenni saqlaydi.

Umumiy oqim quyidagicha.

업무 시스템
  -> adapter: 진입 URI 발급 요청
  -> adapter: 인증 서버에서 직원 token 발급
  -> Redis: tokenJson, loginCode, userKey 저장
  -> adapter: loginCode가 포함된 rsltUri 반환
  -> front: rsltUri로 진입 후 loginCode 추출
  -> account: loginCode consume 요청
  -> Redis: tokenJson 반환 및 key 삭제
  -> front: token 저장 후 목표 화면 표시

Ushbu tuzilmaning asosiy jihati tokenni ekran o‘tkazish yo‘nalishida yuklamaslikdir. Ish tizimi va front o'rtasida faqat token o‘rniga bir martalik loginCode uzatiladi. Account server loginCode'ni olganda Redisdan tokenJsonni olib, shu bilan birga mazkur kodni bekor qiladi.

3. Token o‘rniga loginCode'ni uzatish sababi

accessToken va refreshToken tasdiqlangan foydalanuvchini tasdiqlovchi qiymatlardir. Shu sababdan, tokenni to'g'ridan-to'g'ri URL'ga kiritishdan qochish kerak edi. Token qiymati to'g'ridan-to'g'ri oshkora bo'lsa, bu amal muddati davomida tegishli foydalanuvchi huquqlari bilan API'ni chaqirish mumkin.

loginCode ham tokenni olishda ishlatiladigan qiymat bo'lganligi sababli nohuquqiy ahamiyatga ega deb hisoblamaslik kerak. Biroq, tokenning o'zidan farqli o‘laroq, qisqa TTLga ega va bir marta ishlatilgandan so‘ng darhol o‘chiriladi. Ya'ni loginCode tokenni to'g'ridan-to'g'ri almashtiruvchi qiymat emas, balki Redisda vaqtincha saqlangan tokenni bir marta olish uchun valyuta vazifasini bajaradi.

accessToken / refreshToken
  - 실제 인증 자격증명입니다.
  - URL이나 외부 응답에 직접 노출하지 않는 것이 안전합니다.
loginCode
  - Redis에 저장된 tokenJson을 꺼내기 위한 일회용 교환권입니다.
  - TTL과 GETDEL 성격의 소비 로직으로 사용 범위를 제한합니다.

4. Redis kalitlarini loyihalash

Redisda bir nechta turdagi kalitlarni saqladim. Dastlab loginCode ni kalit sifatida faqat tokenJsonni saqlash kerakligini o'ylagandim, ammo haqiqiy qo'llash jarayonida takroriy berish, bir vaqtning o'zida so‘rovlar, iste'mol qilinganidan keyin tozalash kabi jarayonlarni hisobga olish kerak edi.

codeKey
app:sso:login-code:{loginCode}
-> tokenJson
userKey
app:sso:login-code:user:{userHash}
-> loginCode
codeUserKey
app:sso:login-code:code-user:{loginCode}
-> userKey
lockKey
app:sso:login-code:lock:{userHash}
-> LOCKED

codeKey loginCode orqali tokenJsonni topish uchun kalitdir. userKey bir xil kirish birligida allaqachon berilgan foydalanuvchilarda foydalanilmagan loginCode mavjudligini tekshirish uchun kalitdir. codeUserKey account loginCode'ni olganda userKey’ni topib o'chirish uchun yaratilgan orqa tarafli kalitdir. lockKey bir vaqtda bir necha muammo so‘rovi kelganda faqat bitta so‘rovni haqiqiy berish uchun boshqarish uchun kalitdir.

5. URI bo'linishi va kirish maqsadlarini ajratish

Ish tizimida foydalanuvchilar xizmat ekranini ochish usuli ikkita edi. Biri ma'lum maqsad ekraniga to'g'ridan-to'g'ri kirish, ikkinchisi esa shaxsiy dashbordga kirish. Shuning uchun adapter oddiy loginCode ni qaytarishdan tashqari, front-end ochishi kerak bo'lgan rsltUri ni yaratib qaytarishini o'zgartirdik.

대상 식별값이 있는 경우:
/service/staff/survey/response?loginCode={code}&targetId={targetId}
대상 식별값이 없는 경우:
/service/staff/dashboard/personal?loginCode={code}

Ommaviy maqolada haqiqiy domen, haqiqiy yo'l, haqiqiy identifikator nomi umumlashtirilgan. Muhim nuqta shundaki, loginCode ekran ko'chirish URI ga kiritilgan va front-end ushbu qiymatni o'qib account consume API ni chaqirishi strukturasidir.

private String buildLoginUri(String loginCode, String targetId) {
    if (hasText(targetId)) {
        return baseUrl + "/staff/survey/response"
                + "?loginCode=" + loginCode
                + "&targetId=" + targetId;
    }
    return baseUrl + "/staff/dashboard/personal"
            + "?loginCode=" + loginCode;
}

6. userHash mezonini o'zgartirish sabablari

Ishlab chiqish jarayonida eng ko'p o'ylab qolgan qism userHash ga nimalarni kiritish edi. Dastlab, userHash ni xodimlar ma'lumotlari asosida yaratish kifoya deb o'yladim. Haqiqiy tokenning subyektlari xodimlar bo'lib, maqsad identifikatsiya qiymati esa ekran ko'chirish uchun ishlatiladigan qiymat deb hisoblangan.

Biroq, URI bo'linishi talablariga yana bir bor qarasak, maqsad identifikatsiya qiymati bo'lgan va bo'lmagan hollarda turli kirish maqsadlari mavjud. Bir xil xodim qisqa vaqt ichida maqsad A ekranini va maqsad B ekranini ketma-ket ochishi mumkin va ikkita ekran bir vaqtning o'zida brauzer tablarida ochilishi mumkin. Bu paytda userHash ni faqat xodimlar asosida yaratishga harakat qilsak, turli ekran kirish so'rovlari bir xil userKey bilan bog'lanishi mumkin.

직원 기준으로만 userHash를 만들면:
직원 A + 대상 111 -> userHash H1 -> loginCode X
직원 A + 대상 222 -> userHash H1 -> loginCode X
결과:
두 화면이 같은 loginCode를 공유할 수 있습니다.
loginCode는 일회용이므로 한 화면이 먼저 consume하면 다른 화면은 실패할 수 있습니다.

Shuning uchun oxir-oqibat userHash ni xodimlar ma'lumotlari va kirish maqsadini birga hisoblashga moslashtirdik. Bu tokenning subyektini o'zgartirish emas. Tokenning foydalanuvchisi hali ham xodimdir. Biroq Redis ning takroriy berish mezonini “xodim” birligi emas, “xodim + kirish maqsadi” birligi bo'yicha ajratdik.

직원 + 진입 대상 기준으로 userHash를 만들면:
직원 A + 대상 111 -> userHash H111 -> loginCode X
직원 A + 대상 222 -> userHash H222 -> loginCode Y
직원 A + 대시보드 -> userHash HD -> loginCode Z

7. Bir vaqtning o'zida so'rovlarni to'xtatish uchun lockKey

userKey dan foydalanib bir vaqtning o'zida muammolarni to'liq hal qilib bo'lmaydi. Masalan, bir xil foydalanuvchi bir xil ekran uchun so'rov yuborganida, u deyarli bir vaqtning o'zida bir necha marta kelishi mumkin, bir nechta so'rovlar userKey hali yo'q deb hisoblaydi. Shunday qilib, bir necha marta loginCode berilishi mumkin.

Buni to'xtatish uchun Redis ning setIfAbsent tabiatidan foydalangan lockKey dan foydalanildi. Lockni olingan so'rovlar haqiqiy token berish va Redis saqlashni amalga oshiradi, lockni olmagan so'rovlar qisqa vaqt kutib turib, userKey da saqlangan mavjud loginCode ni qaytaradi.

locked = setIfAbsent(lockKey, "LOCKED", lockTtl)
if (!locked) {
    existingCode = waitAndGet(userKey)
    return buildLoginUri(existingCode, targetId)
}
try {
    token = loginOrJoin(user)
    code = createLoginCode()
    saveCodeKeys(code, token, userKey)
    return buildLoginUri(code, targetId)
} finally {
    delete(lockKey)
}

Bu usulda bir vaqtning o'zida bir xil kirish birligiga ko'plab muammo so'rovlari tushsa ham, bitta ishlatilmaydigan loginCode ni saqlab qolishga muvaffaq bo'ldik. Biroq, maqsad identifikatsiya qiymati boshqa ekran bo'lsa, userHash boshqa hisoblangan bo'lib, turli loginCode larni bo'lishi mumkin.

8. Saqlash tartibi va codeUserKey ning roli

Adapter Redis ga saqlaganda codeKey, codeUserKey, userKey tartibida saqladi. userKey adapterning “allaqachon berilgan code bor” deb hisoblaydigan mezondir. Shuning uchun haqiqiy tokenJson saqlanishidan oldin userKey avval saqlansa, boshqa so'rovlar hali tayyor bo'lmagan code ni olishlari mumkin.

set(codeKey, tokenJson, ttl)
set(codeUserKey, userKey, ttl)
set(userKey, loginCode, ttl)

codeUserKey consume muvaffaqiyatidan so'ng userKey ni tartibga solish uchun zarur. Account serveri front-end dan faqat loginCode ni oladi. Shuning uchun account userHash ni to'g'ridan-to'g'ri hisoblay olmaydi. Adapter berish nuqtasida codeUserKey ni yaratganda, account consume nuqtasida codeUserKey orqali userKey ni topib o'chirish imkoniyatiga ega bo'ladi.

9. accountning consume ishlov berishi va GETDEL

account serverida frontdan olingan loginCode ni qabul qilib, Redis dan tokenJson ni olish va bir vaqtning o'zida tegishli kalitni o'chirishda ishlatdim. Bu yerda oddiy GET emas, balki GETDEL xususiyatiga ega getAndDelete dan foydalandim.

tokenJson = getAndDelete(codeKey)

if (tokenJson is blank) {
    throw expiredOrInvalid
}

userKey = getAndDelete(codeUserKey)
if (userKey exists) {
    delete(userKey)
}

return parseToken(tokenJson)

Ushbu API DB ni o'zgartirmasdan faqat Redis kalitini iste'mol qilish vazifasini bajaradi, shuning uchun alohida DB tranzaksiyasini ishlatmadim. Shuningdek, login oldidan chaqirilishi kerak bo'lgan umumiy API dan farqli o'laroq permitAll maqsadida belgilandi.

10. Nima uchun TTL yetarli emas

TTL va GETDEL farqli muammolarni hal qiladi. TTL “qachongacha foydalanish mumkinligini” cheklaydi. Biroq GETDEL “necha marta foydalanish mumkinligini” cheklaydi.

TTLni faqat ishlatganda loginCode bir ma'lum vaqtdan so'ng avtomatik o'chiriladi. Biroq TTL tugamay turib, bir xil loginCode bilan bir necha marta token olish mumkin. Shunday qilib, bu bir martalik login kodiga emas, balki cheklov vaqti davomida qayta ishlatilishi mumkin bo'lgan login kodiga aylanadi.

TTL만 있는 경우:
1번째 consume ABC -> token 반환
2번째 consume ABC -> token 반환
3번째 consume ABC -> token 반환

GETDEL을 사용하는 경우:
1번째 consume ABC -> token 반환 + ABC 삭제
2번째 consume ABC -> expired or invalid

Shuningdek, GET va DELETE ni ajratish orqali bir vaqtning o'zida ikki so'rov GET ga muvaffaqiyatli o'tishi mumkin. Redisning GETDEL yoki Spring Data Redis'ning getAndDelete funksiyalari ko'rish va o'chirishni atomik tarzda bajaradi, shuning uchun bir xil loginCode bilan bir vaqtning o'zida bir nechta iste'mol qilish so'rovi keladigan bo'lsa, faqat biri muvaffaqiyatli bo'lishi mumkin. Shunday qilib, TTL xuddi o'z vaqtida to'xtab qolgan kodlarni tozalash vositasi sifatida, GETDEL esa kodni haqiqatan ham bir martalik qilish vositasi sifatida qaralishi mumkin.

11. Sinov natijalari

Mahalliy va rivojlanish tizimlarida quyidagi narsalarni tekshirdim. adapter ning rsltUri ni normal ravishda qaytarganini va maqsad identifikator qiymati bo'lsa va yo'q bo'lsa URI qanday to'g'ri branch shakllanishini tekshirdim. account iste'mol API sini chaqirganimda token normal qaytarildi va bir xil loginCode ni ikkinchi marta iste'mol qilganimda expired or invalid qaytarilishini ham ko'rdim.

Shuningdek, iste'mol muvaffaqiyatli o'tgandan so'ng Redis da codeKey, codeUserKey, userKey o'chirilishini ham ko'rdim. userHash asoslarini xodim + kirish maqsadi bo'yicha sozlaganimdan so'ng, bir biri bilan bog'liq bo'lmagan maqsadli ekran kirish so'rovlarining bir xil loginCode ni ulashmasligi ham ko'rildi.

확인한 항목
- URI 발급 성공
- targetId의 유무에 따른 화면 분기 성공
- loginCode consume 성공
- 동일 loginCode 재사용 실패
- consume 후 Redis key 정리 확인
- 서로 다른 진입 대상의 loginCode 분리 확인

12. Amalda o'rgangan narsalarim

Ushbu ish davomida SSO integratsiyasi faqat tokenni qabul qilib, uzatishdan iborat emasligini o'rgandim. Tokenni qanchalik keng ko'lamda ochiq qoldirish, vaqtinchalik kodni necha soniya davomida saqlash, bir marta ishlatilgan kodni qanday yo'q qilish, takroriy so'rovlarni qanday birlikda hisoblash kabi ko'plab mezonlarni aniq belgilash kerak edi.

Xususan, userHash da maqsad identifikator qiymatini qo'shish yoki qo'shmaslik masalasi oddiy bir amalga oshirish muammosi emas, balki siyosat qaroridir. Dastlab, foydalanuvchi xodim bo'lgani uchun xodim ma'lumotlari etarli deb o'yladim. Ammo ekran kirish birligi maqsadlarga qarab o'zgaradi va bir vaqtning o'zida bir nechta ekranlarni ochish imkoniyati hisobga olinganda, takroriy berish mezonlarini yanada mayda qilib ajratish zaruriyati borasida xulosa qildim.

GETDEL dan foydalanish sababi ham dastlab faqat kalitni tezda o'chirish bilan bog'liq deb o'ylash oson edi. Biroq aslida loginCode ni haqiqatan ham bir martalik qilish uchun asosiy siyosat edi. TTL vaqt chegarasi, GETDEL esa foydalanish muddati chegarasidir. Bu ikkisi o'zaro mantiqiy aloqalar emas, balki birga ishlatilishi kerak bo'lgan to'ldiruvchi munosabatlardir.

13. Xulosa

Oxir oqibat, ushbu ish ichki ish tizimidan xizmat ekrani orqali kirish jarayonida tokenni to'g'ridan-to'g'ri ochib qo'ymasdan, Redis asosidagi bir martalik loginCode orqali xavfsiz token almashish tuzilmasini qo'llagan misol hisoblanadi. adapter foydalanuvchi ma'lumotlari va kirish maqsadlariga asoslangan URI ni shakllantiradi va account loginCode ni atomik ravishda iste'mol qilib, tokenni qaytaradi.

Amalga oshirish jarayonida oddiygina bir API qo'shishdan tashqari, Redis kalitini loyihalash, bir vaqtda bajarilish nazorati, GETDEL asosidagi bir martalik iste'mol, iste'moldan so'ng kalitlarni tartibga solish, ekran bo'linishi va frontend integratsiyasini ham hisobga oldik. Ushbu tajriba orqali autentifikatsiya oqimida nafaqat normal ishlash, balki yuzaga kelish imkoniyati, takroriy chaqiruvlar, muvaffaqiyatsizlik senariylarini ham loyihalash zarurligini sezdim.

Wade

Site footer