Keresés tartalomra
Kategóriák
Címkék
- Java
- Spring
- Python
- IoC
- Android
- DI
- Dagger
- Thymeleaf
- Markdown
- JDK11
- AOP
- Aspect
- Captcha
- I18n
- JavaSpark
- Microframework
- Testing
- JUnit
- Security
- JWT
- REST
- Database
- JPA
- Gépház
- WebFlux
- ReactiveProgramming
- Microservices
- Continuous Integration
- CircleCI
- Deployment Pipeline
- Docker
- Mocking
- LogProcessing
- PlantUML
- UML
- Modellezés
- OAuth2
- Node.js
- DevOps
- Websocket
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ábandevice_id
: eszköz azonosító - a tokent igénylő kliens saját, véletlenszerű azonosítójaremote_address
: a kliens IP címeusername
: felhasználónévstatus
: a token aktuális állapotaissued
: a token igénylésének idejeexpires
: é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.
Komment írásához jelentkezz be
Bejelentkezés
Még senki nem szólt hozzá ehhez a bejegyzéshez.