Itt jársz most: Kezdőlap > Alkalmazásfejlesztés > JWT alapú authentikálás I - Tokenek követése

Szűrő megjelenítése

JWT alapú authentikálás I - Tokenek követése

A tokenek (nem) követése

Korábban már említettem a Leaflet nevű alkalmazást, melynek fejlesztése során számos érdekes probléma merült és merül fel. Az egyik ilyen volt a JWT tokenek követése, tehát a lehetőség arra, hogy a kiállított tokenek használatát a rendszer képes legyen felügyelni. A JWT tokenek sajátossága önleíró és önvalidáló mivoltuk, aminek köszönhetően a kiállító rendszer a tokent mindössze az aláírás ellenőrzésével képes validálni. Tehát ha a token aláírása érvényes, akkor a tokent a kérdéses rendszer állította ki. És pont. Vagy nem.

Na most itt szeretném leszögezni, hogy egy ilyen “tényre” alapozni egy rendszer biztonságát minimum felelőtlenségnek érzem. Mi van akkor, ha a privát kulcs megegyezik két rendszer alatt, és mindeközben véletlenül van egy-egy azonos nevű felhasználó is? Nyilván kicsi az esélye, de mégis jobb volna egy megoldás, amivel a kiállított tokenekre a kiállító rendszer 100%-os bizonyossággal tudja azt mondani: igen, ezt én állítottam ki.

A megoldás végül egy “session store” implementálása lett, melynek célja a kiállított tokenek tárolása és ellenőrzése. A fogadott tokenek validálása így már egy három lépcsős folyamat, melynek első két lépését (az aláírás ellenőrzése a privát kulccsal és a lejárati idő ellenőrzése) már a használt (io.jsonwebtoken:jjwt) JWT library elvégzi. A harmadik lépés a session store-ral való összevetés - erről később bővebben, előbb bemutatnám a session store felépítését.

A Session Store felépítése

Először is szükségünk van egy nagyon gyors perzisztens tárra. Ennek oka egyszerű: nem szeretnénk sem az ellenőrzés, sem a token tárolása során túl sokáig várakoztatni a felhasználót, márpedig az ellenőrzés minden request esetén meg fog történni. Átlagos terheléstől, felhasználószámtól, illetve legfőképp a párhuzamosan aktív session-ök számától függően kell megválasztanunk a használt adatbázis típusát - ez esetben egy H2-es in-memory adatbázis mellett döntöttem.

Némi háttérinformáció: a H2 egy teljes egészében Java-ban írt relációs adatbáziskezelő. Klasszikus rokonaihoz hasonlóan képes adatfájlokban is tárolni, azonban képes a memóriában is létrehozni a táblákat, melyek így természetesen az alkalmazás leállításakor elvesznek. Emiatt elsősorban teszteléshez ajánlják, azonban ezen viselkedése jelen esetben kifejezetten hasznos lesz. Másik érdekessége, hogy a JVM-re egyedi instance-ban fut, tehát alapesetben másik processzből nem érhető el a tartalma. (Ez abban az esetben is problémát jelenthet, ha az alkalmazásból több instance fut - ekkor sajnos maradni kell a file alapú variánsnál.) Erre megoldás egy belső TCP szerver indítása, melyen keresztül már bármilyen H2 driverrel rendelkező adatbázis-kezelő alkalmazásból elérhetővé válik az adatbázis. Mivel production környezetben a session store külső elérésére aligha lesz szükség, profilhoz kötött beanként létrehozva a szervert megoldható, hogy csak lokális vagy tesztkörnyezetben induljon el. Bővebb információk a http://www.h2database.com/ címen találhatóak a H2-ről.

Visszatérve tehát a H2 in-memory módban teljesen megfelelő adatbázis motor lesz a session store számára - magasabb felhasználószám esetén azért persze érdemes odafigyelni a memóriafogyasztásra, mivel így minden adat folyamatosan a memóriában van. A sebességre viszont biztosan nem lehet majd panasz, hiszen gyakorlatilag a tokenek ellenőrzése alig több időt vesz igénybe, mintha egy collection-ben keresnénk meg. Az adatbázis egyébként mindössze egy táblából fog állni, melyben a tokent leíró információkat találjuk - bővebben később.

A H2-vel kapcsolatot tartó DAO réteg, a teljesítmény maximalizálása érdekében, egyszerű (a paraméterezhetőség miatt) NamedParameterJdbcTemplate-ekre épül. A tokeneket tároló tábla minden paraméterét kötelező megadni, hiszen csak így biztosítható a tokenek pontos azonosítása. A paraméterek a következők:

  • token: nyilván a kiállított token maga kell, hogy szerepeljen a táblában
  • device_id: eszköz azonosító - a tokent igénylő kliens saját, véletlenszerű azonosítója
  • remote_address: a kliens IP címe
  • username: felhasználónév
  • status: a token aktuális állapota
  • issued: a token igénylésének ideje
  • expires: és végül a token lejárati ideje.

A DAO réteg fölött található service API jelenleg négy műveletet támogat, melyek a tárolás, validálás, visszavonás és takarítás - ezekről bővebben a következő fejezetben.

Validálási folyamat

Miután a felhasználó sikeresen authentikálja magát és kiállításra kerül a token, az authentikációs folyamat rögtön elküldi azt a session store-nak. A token mentésekor kötelező paraméterként várt eszköz azonosító (X-Device-ID header paraméter) és a kliens IP címe a HttpServletRequest objektumból származik, melyet a controller tud biztosítani.

@Override
public String claimToken(LoginContextVO loginContextVO)

	authenticationManager.authenticate(createUsernamePasswordAuthentication(loginContextVO));
	UserDetails userDetails = 
			userDetailsService.loadUserByUsername(loginContextVO.getUsername());
	JWTAuthenticationAnswerModel authenticationAnswerModel = 
			jwtComponent.generateToken(userDetails);
	storeToken(loginContextVO, authenticationAnswerModel);

	return authenticationAnswerModel.getToken();
}

// …

private void storeToken(LoginContextVO loginContextVO,
						JWTAuthenticationAnswerModel authenticationAnswerModel) {
	sessionStoreService.storeToken(ClaimedTokenContext.getBuilder()
			.withToken(authenticationAnswerModel.getToken())
			.withRemoteAddress(loginContextVO.getRemoteAddress())
			.withDeviceID(loginContextVO.getDeviceID())
			.build());
}

A kliens a továbbiakban a kiállított tokennel kommunikál, Bearer típusú Authorization headerben átadva azt. A validálást a JWT authentikáláshoz implementált provider végzi. Mivel a JWTAuthenticationFilter már korábban összeállította az Authentication objektumot a token alapján (a token parse-olása során az első két, már korábban említett validációs lépés végrehajtódik), a provider már csak átadja a session store-nak az Authentication objektumot validálásra. Sikeres validálás esetén az Authentication objektumot authentikáltnak jelöljük és a felhasználói kérés továbbmehet.

@Override
public Authentication authenticate(Authentication authentication)
			throws AuthenticationException {

	JWTAuthenticationToken jwtAuthenticationToken = (JWTAuthenticationToken) authentication;
	SessionStoreValidationStatus status = 
			sessionStoreService.validateToken(jwtAuthenticationToken);
	if (status != SessionStoreValidationStatus.VALID) {
		throw new SessionStoreValidationException(
			String.valueOf(authentication.getPrincipal()), status);
	}
	authentication.setAuthenticated(true);

	return authentication;
}

A session store validálás többféle problémát is észrevehet a tokennel kapcsolatban:

  • A token nem található a store-ban: bár az aláírás alapján a token megnyitható volt, a store-ban nincs róla információ tárolva. Ebben az esetben a tokent egy másik (szerveren futó) instance hozta létre.
  • A token nem aktív státuszban van: a felhasználó kijelentkezett vagy egy korábbi request során sikertelen volt a validálás. A session store mindkét esetben automatikusan invalidálja a tokent.
  • A token aktív státuszban van, de az IP cím és/vagy az eszközazonosító eltér: a kliens egy másik klienstől származó tokennel próbál kommunikálni - hétköznapi nevén ezt session-lopásnak hívják.

További lehetőségek

A session store követni tudja a kijelentkezés során visszavont tokeneket is. Erre azért van szükség, mert ha a kliens eltárolja a tokent, akkor a kijelentkezés gyakorlatilag teljesen értelmetlen - a “kijelentkezett” tokent továbbra is tudja küldeni a kliens. Ez esetben viszont a token visszavontnak lesz megjelölve a session store-ban, a következő kérés alkalmával pedig sikertelen lesz a validálás.

Egy másik nagyon fontos szempont a store időszakos takarítása. Mivel a tokenek (beállítástól függően) néhány órán belül ígyis-úgyis lejárnak, a lejárt tokenek követésére már semmi szükség. Ez könnyen megvalósítható egy ütemezett feladat implementálásával, mely időnként törli a már lejárt tokeneket.

A Session Store forráskódja

Kommentek

Komment írásához jelentkezz be
Bejelentkezés

Még senki nem szólt hozzá ehhez a bejegyzéshez.