JWT kezelése Java/Jakarta EE környezetben JAX-RS-sel
Egy korábbi posztban (JWT és Spring Security) írtam arról, hogy mi az a JWT token, és hogyan lehet használni Spring Security-vel. Ez a poszt azt írja le, hogy lehet JAX-RS-sel használni (ami a Java/Jakarta EE szabvány része is).
A példámban a Jetty servlet containert, Jersey JAX-RS implementációt fogom használni, és a Java JWT könyvtárat. A szabványos megoldásnak köszönhetően bármelyik komponens cserélhető.
A folyamat nagyon egyszerű:
- A felhasználó bejelentkezik, és ezáltal kap egy HttpOnly Cookie-t, benne a tokennel, melyet minden kéréssel visszaküld a szervernek
- Egy JAX-RS
ContainerRequestFilter
példányon megy keresztül minden kérés. Ez ellenőrzi, kicsomagolja a tokent, és létrehoz egy újSecurityContext
példányt, mely tárolja a bejelentkezett felhasználó adatait, pl. a felhasználónevet és szerepkörét - A JAX-RS végpontban, azaz a
Resource
-ban már paraméter injectionnel elérhető aSecurityContext
, de működik pl. a@RolesAllowed
annotáció is
A poszthoz egy példa projekt is tartozik, mely elérhető a GitHubon.
Indítható az mvn package jetty:run
paranccsal, példa http kérések a src/test/http/hello.http
fájlban találhatóak.
Automata integrációs tesztek is vannak.
Bejelentkezéshez el kell küldeni a következő kérést:
POST http://localhost:8080/api/auth
Content-Type: application/json
{
"username": "user",
"password": "user"
}
Ezt az AuthResource
kapja meg, ez először a JJWT használatával előállít egy tokent:
return Jwts.builder()
.setSubject(username)
.claim(ROLE_CLAIM, role)
.setIssuedAt(new Date(now))
.setExpiration(new Date(now + EXPIRATION))
.signWith(SignatureAlgorithm.HS512, SECRET.getBytes())
.compact();
Látható, hogy a szabványos mezőkön kívül a claim()
metódussal saját mezőket is fel lehet venni,
a kód a felhasználó szerepkörét is eltárolja a tokenben.
Majd visszaad egy cookie-t, mely tartalmazza a tokent:
NewCookie newCookie =
new NewCookie(COOKIE_NAME, token, "/", "http://localhost:8080", "JWT", 30 * 60, true, true);
return Response.ok(new MessageResponse("Successful"))
.cookie(newCookie)
.build();
Ez a http válaszban valahogy így néz ki:
Set-Cookie: token=eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJ1c2VyIiwicm9sZSI6InVzZXIiLCJpYXQiOjE2MjI2MzkwMDgsImV4cCI6MTYyMjY0MDgwOH0.VxwmtuuAUPXlhYZVfX7uHc4RQWt8laR65Kb4YmKNm2azSPtvAkWO8fluVp_H4a2v9j_uvtK4fFE_qBraxmBqzg;Version=1;Comment=JWT;Domain=http://localhost:8080;Path=/;Max-Age=1800;Secure;HttpOnly
Ha ezt bemásoljuk a https://jwt.io/ címen található dekóderbe, akkor a következőt kapjuk:
{
"sub": "user",
"role": "user",
"iat": 1622639008,
"exp": 1622640808
}
Ezt a tokent kell minden alkalommal elküldeni fejlécben:
GET http://localhost:8080/api/hello/user
Accept: application/json
Authorization: Bearer eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJ1c2VyIiwicm9sZSI6InVzZXIiLCJpYXQiOjE2MjI2MzkwMDgsImV4cCI6MTYyMjY0MDgwOH0.VxwmtuuAUPXlhYZVfX7uHc4RQWt8laR65Kb4YmKNm2azSPtvAkWO8fluVp_H4a2v9j_uvtK4fFE_qBraxmBqzg
Ezt a címet a JwtFilter
fogja fogadni, mely egy ContainerRequestFilter
. Fontos, hogy rajta legyen a
@Provider
és @Priority(Priorities.AUTHENTICATION)
annotáció. Az implementálásnál arra is kell figyelni,
hogy a /api/auth
cím esetén ne aktiválódjon, különben a bejelentkezéshez is bejelentkezést fog kérni.
A filter kiveszi a tokent a headerből, ellenőrzi és kicsomagolja a JJWT segítségével.
Jwts.parser()
.setSigningKey(SECRET.getBytes())
.parseClaimsJws(token)
.getBody();
A tokenből olvassa ki
a felhasználónevet és a szerepkört is. Ahhoz, hogy bejelentkeztessen egy felhasználót, egy SecurityContext
interfészt implementáló osztályt kell megvalósítanunk, aminek getUserPrincipal()
metódusa adja vissza a
felhasználó nevét, az isUserInRole(String role)
metódusa pedig ellenőrzi, hogy a felhasználó
rendelkezik-e a paraméterként átadott szerepkörrel.
A vezérlés ezután a HelloResource
-ra ugrik.
@GET
@RolesAllowed("user")
public MessageResponse sayHello(@Context SecurityContext context) {
System.out.println("Username: " + context.getUserPrincipal().getName());
System.out.println("Has user role? " + context.isUserInRole("user"));
return new MessageResponse("Hello JAX-WS!");
}
Itt a @RolesAllowed("user")
annotáció hatására
ellenőrzésre kerül, hogy a felhasználó rendelkezik-e az adott szerepkörrel. Ekkor automatikusan
meghívja a isUserInRole(String role)
metódust. Ha ez false
értéket ad vissza, akkor
403 (Forbidden) státuszkódot kapunk vissza. Ez a deklaratív ellenőrzés.
Azonban a bejelentkezett felhasználót, és szerepköreit programozottan is le lehet kérdezni,
ekkor paraméter injectiont alkalmazva egy SecurityContext
példányt kapunk, ha rajta van
egy @Context
annotáció.
Amire még figyelni kell, hogy ahhoz, hogy a @RolesAllowed
annotációk működjenek,
a Jersey-nek be kell kapcsolni a RolesAllowedDynamicFeature
-t. Ez a következő osztállyal
lehetséges:
@ApplicationPath("api")
public class JerseyConfig extends ResourceConfig {
public JerseyConfig() {
packages("hello");
register(RolesAllowedDynamicFeature.class);
}
}
Az automata tesztesetek elindítanak egy beépített Grizzly Http Servert, és itt is
kell a RolesAllowedDynamicFeature
regisztrálása. Majd egyszerű, szabványos
JAX-RS kliens hívásokkal ellenőrzik a működést.