Funktionsweise der tokenbasierten Authentifizierung
Bei der tokenbasierten Authentifizierung tauscht der Client feste Anmeldeinformationen (wie Benutzername und Kennwort) gegen ein Datenelement namens Token aus . Anstatt die festen Anmeldeinformationen zu senden, sendet der Client für jede Anforderung das Token an den Server, um die Authentifizierung und anschließend die Autorisierung durchzuführen.
In wenigen Worten, ein auf Token basierendes Authentifizierungsschema folgt diesen Schritten:
- Der Client sendet seine Anmeldeinformationen (Benutzername und Passwort) an den Server.
- Der Server authentifiziert die Anmeldeinformationen und generiert, falls sie gültig sind, ein Token für den Benutzer.
- Der Server speichert das zuvor generierte Token zusammen mit der Benutzerkennung und einem Ablaufdatum in einem Speicher.
- Der Server sendet das generierte Token an den Client.
- Der Client sendet das Token bei jeder Anforderung an den Server.
- Der Server extrahiert in jeder Anforderung das Token aus der eingehenden Anforderung. Mit dem Token sucht der Server nach den Benutzerdetails, um die Authentifizierung durchzuführen.
- Wenn das Token gültig ist, akzeptiert der Server die Anforderung.
- Wenn das Token ungültig ist, lehnt der Server die Anforderung ab.
- Sobald die Authentifizierung durchgeführt wurde, führt der Server die Autorisierung durch.
- Der Server kann einen Endpunkt zum Aktualisieren von Token bereitstellen.
Hinweis: Schritt 3 ist nicht erforderlich, wenn der Server ein signiertes Token ausgestellt hat (z. B. JWT, mit dem Sie eine zustandslose Authentifizierung durchführen können).
Was können Sie mit JAX-RS 2.0 (Jersey, RESTEasy und Apache CXF) tun?
Diese Lösung verwendet nur die JAX-RS 2.0-API, wodurch herstellerspezifische Lösungen vermieden werden . Daher sollte es mit JAX-RS 2.0-Implementierungen wie Jersey , RESTEasy und Apache CXF funktionieren .
Es ist erwähnenswert, dass Sie sich bei Verwendung der tokenbasierten Authentifizierung nicht auf die vom Servlet-Container angebotenen und über den Anwendungsdeskriptor konfigurierbaren Standard-Sicherheitsmechanismen für Java EE-Webanwendungen verlassen web.xml
. Es ist eine benutzerdefinierte Authentifizierung.
Authentifizieren eines Benutzers mit seinem Benutzernamen und Passwort und Ausstellen eines Tokens
Erstellen Sie eine JAX-RS-Ressourcenmethode, die die Anmeldeinformationen (Benutzername und Kennwort) empfängt und überprüft, und geben Sie ein Token für den Benutzer aus:
@Path("/authentication")
public class AuthenticationEndpoint {
@POST
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_FORM_URLENCODED)
public Response authenticateUser(@FormParam("username") String username,
@FormParam("password") String password) {
try {
// Authenticate the user using the credentials provided
authenticate(username, password);
// Issue a token for the user
String token = issueToken(username);
// Return the token on the response
return Response.ok(token).build();
} catch (Exception e) {
return Response.status(Response.Status.FORBIDDEN).build();
}
}
private void authenticate(String username, String password) throws Exception {
// Authenticate against a database, LDAP, file or whatever
// Throw an Exception if the credentials are invalid
}
private String issueToken(String username) {
// Issue a token (can be a random String persisted to a database or a JWT token)
// The issued token must be associated to a user
// Return the issued token
}
}
Wenn bei der Überprüfung der Anmeldeinformationen Ausnahmen ausgelöst werden, wird eine Antwort mit dem Status 403
(Verboten) zurückgegeben.
Wenn die Anmeldeinformationen erfolgreich überprüft wurden, wird eine Antwort mit dem Status 200
(OK) zurückgegeben und das ausgestellte Token wird in der Antwortnutzlast an den Client gesendet. Der Client muss das Token bei jeder Anforderung an den Server senden.
Beim Konsumieren application/x-www-form-urlencoded
muss der Client die Anmeldeinformationen im folgenden Format in der Anforderungsnutzlast senden:
username=admin&password=123456
Anstelle von Formularparametern können Sie den Benutzernamen und das Kennwort in eine Klasse einschließen:
public class Credentials implements Serializable {
private String username;
private String password;
// Getters and setters omitted
}
Und dann verbrauchen Sie es als JSON:
@POST
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
public Response authenticateUser(Credentials credentials) {
String username = credentials.getUsername();
String password = credentials.getPassword();
// Authenticate the user, issue a token and return a response
}
Bei diesem Ansatz muss der Client die Anmeldeinformationen im folgenden Format in der Nutzlast der Anforderung senden:
{
"username": "admin",
"password": "123456"
}
Extrahieren Sie das Token aus der Anforderung und validieren Sie es
Der Client sollte das Token im Standard-HTTP- Authorization
Header der Anforderung senden . Zum Beispiel:
Authorization: Bearer <token-goes-here>
Der Name des Standard-HTTP-Headers ist unglücklich, da er Authentifizierungsinformationen und keine Autorisierung enthält . Dies ist jedoch der Standard-HTTP-Header zum Senden von Anmeldeinformationen an den Server.
JAX-RS bietet @NameBinding
eine Meta-Annotation, mit der andere Annotationen erstellt werden, um Filter und Interceptors an Ressourcenklassen und -methoden zu binden. Definieren Sie eine @Secured
Anmerkung wie folgt:
@NameBinding
@Retention(RUNTIME)
@Target({TYPE, METHOD})
public @interface Secured { }
Die oben definierte Annotation zur Namensbindung wird verwendet, um eine Filterklasse zu dekorieren, die implementiert wird ContainerRequestFilter
, sodass Sie die Anforderung abfangen können, bevor sie von einer Ressourcenmethode verarbeitet wird. Mit ContainerRequestContext
kann auf die HTTP-Anforderungsheader zugegriffen und anschließend das Token extrahiert werden:
@Secured
@Provider
@Priority(Priorities.AUTHENTICATION)
public class AuthenticationFilter implements ContainerRequestFilter {
private static final String REALM = "example";
private static final String AUTHENTICATION_SCHEME = "Bearer";
@Override
public void filter(ContainerRequestContext requestContext) throws IOException {
// Get the Authorization header from the request
String authorizationHeader =
requestContext.getHeaderString(HttpHeaders.AUTHORIZATION);
// Validate the Authorization header
if (!isTokenBasedAuthentication(authorizationHeader)) {
abortWithUnauthorized(requestContext);
return;
}
// Extract the token from the Authorization header
String token = authorizationHeader
.substring(AUTHENTICATION_SCHEME.length()).trim();
try {
// Validate the token
validateToken(token);
} catch (Exception e) {
abortWithUnauthorized(requestContext);
}
}
private boolean isTokenBasedAuthentication(String authorizationHeader) {
// Check if the Authorization header is valid
// It must not be null and must be prefixed with "Bearer" plus a whitespace
// The authentication scheme comparison must be case-insensitive
return authorizationHeader != null && authorizationHeader.toLowerCase()
.startsWith(AUTHENTICATION_SCHEME.toLowerCase() + " ");
}
private void abortWithUnauthorized(ContainerRequestContext requestContext) {
// Abort the filter chain with a 401 status code response
// The WWW-Authenticate header is sent along with the response
requestContext.abortWith(
Response.status(Response.Status.UNAUTHORIZED)
.header(HttpHeaders.WWW_AUTHENTICATE,
AUTHENTICATION_SCHEME + " realm=\"" + REALM + "\"")
.build());
}
private void validateToken(String token) throws Exception {
// Check if the token was issued by the server and if it's not expired
// Throw an Exception if the token is invalid
}
}
Wenn während der Token-Validierung Probleme auftreten, wird eine Antwort mit dem Status 401
(Nicht autorisiert) zurückgegeben. Andernfalls wird die Anforderung an eine Ressourcenmethode weitergeleitet.
Sichern Ihrer REST-Endpunkte
Um den Authentifizierungsfilter an Ressourcenmethoden oder Ressourcenklassen zu binden, kommentieren Sie diese mit der @Secured
oben erstellten Anmerkung. Für die Methoden und / oder Klassen, die mit Anmerkungen versehen sind, wird der Filter ausgeführt. Dies bedeutet, dass solche Endpunkte nur erreicht werden, wenn die Anforderung mit einem gültigen Token ausgeführt wird.
Wenn einige Methoden oder Klassen keine Authentifizierung benötigen, kommentieren Sie sie einfach nicht:
@Path("/example")
public class ExampleResource {
@GET
@Path("{id}")
@Produces(MediaType.APPLICATION_JSON)
public Response myUnsecuredMethod(@PathParam("id") Long id) {
// This method is not annotated with @Secured
// The authentication filter won't be executed before invoking this method
...
}
@DELETE
@Secured
@Path("{id}")
@Produces(MediaType.APPLICATION_JSON)
public Response mySecuredMethod(@PathParam("id") Long id) {
// This method is annotated with @Secured
// The authentication filter will be executed before invoking this method
// The HTTP request must be performed with a valid token
...
}
}
In dem oben gezeigten Beispiel wird der Filter nur für die mySecuredMethod(Long)
Methode ausgeführt, da er mit Anmerkungen versehen ist @Secured
.
Identifizieren des aktuellen Benutzers
Es ist sehr wahrscheinlich, dass Sie den Benutzer kennen müssen, der die Anforderung für Ihre REST-API ausführt. Die folgenden Ansätze können verwendet werden, um dies zu erreichen:
Überschreiben des Sicherheitskontexts der aktuellen Anforderung
Innerhalb Ihrer ContainerRequestFilter.filter(ContainerRequestContext)
Methode kann eine neue SecurityContext
Instanz für die aktuelle Anforderung festgelegt werden. Überschreiben Sie dann die SecurityContext.getUserPrincipal()
und geben Sie eine Principal
Instanz zurück:
final SecurityContext currentSecurityContext = requestContext.getSecurityContext();
requestContext.setSecurityContext(new SecurityContext() {
@Override
public Principal getUserPrincipal() {
return () -> username;
}
@Override
public boolean isUserInRole(String role) {
return true;
}
@Override
public boolean isSecure() {
return currentSecurityContext.isSecure();
}
@Override
public String getAuthenticationScheme() {
return AUTHENTICATION_SCHEME;
}
});
Verwenden Sie das Token, um die Benutzer-ID (Benutzername) nachzuschlagen, die der Principal
Name des Benutzers ist .
Injizieren Sie die SecurityContext
in eine beliebige JAX-RS-Ressourcenklasse:
@Context
SecurityContext securityContext;
Dasselbe kann in einer JAX-RS-Ressourcenmethode durchgeführt werden:
@GET
@Secured
@Path("{id}")
@Produces(MediaType.APPLICATION_JSON)
public Response myMethod(@PathParam("id") Long id,
@Context SecurityContext securityContext) {
...
}
Und dann holen Sie sich Principal
:
Principal principal = securityContext.getUserPrincipal();
String username = principal.getName();
Verwenden von CDI (Context and Dependency Injection)
Wenn Sie das aus irgendeinem Grund nicht überschreiben möchten SecurityContext
, können Sie CDI (Context and Dependency Injection) verwenden, das nützliche Funktionen wie Ereignisse und Produzenten bietet.
Erstellen Sie ein CDI-Qualifikationsmerkmal:
@Qualifier
@Retention(RUNTIME)
@Target({ METHOD, FIELD, PARAMETER })
public @interface AuthenticatedUser { }
Fügen Sie in Ihrem AuthenticationFilter
oben erstellten Kommentar Event
Folgendes hinzu @AuthenticatedUser
:
@Inject
@AuthenticatedUser
Event<String> userAuthenticatedEvent;
Wenn die Authentifizierung erfolgreich ist, lösen Sie das Ereignis aus, das den Benutzernamen als Parameter übergibt (denken Sie daran, dass das Token für einen Benutzer ausgegeben wird und das Token zum Nachschlagen der Benutzerkennung verwendet wird):
userAuthenticatedEvent.fire(username);
Es ist sehr wahrscheinlich, dass es eine Klasse gibt, die einen Benutzer in Ihrer Anwendung darstellt. Nennen wir diese Klasse User
.
Erstellen Sie eine CDI-Bean, um das Authentifizierungsereignis zu behandeln, suchen Sie eine User
Instanz mit dem entsprechenden Benutzernamen und weisen Sie sie dem authenticatedUser
Produzentenfeld zu:
@RequestScoped
public class AuthenticatedUserProducer {
@Produces
@RequestScoped
@AuthenticatedUser
private User authenticatedUser;
public void handleAuthenticationEvent(@Observes @AuthenticatedUser String username) {
this.authenticatedUser = findUser(username);
}
private User findUser(String username) {
// Hit the the database or a service to find a user by its username and return it
// Return the User instance
}
}
Das authenticatedUser
Feld erzeugt eine User
Instanz, die in Container-verwaltete Beans wie JAX-RS-Dienste, CDI-Beans, Servlets und EJBs eingefügt werden kann. Verwenden Sie den folgenden Code, um eine User
Instanz einzufügen (tatsächlich handelt es sich um einen CDI-Proxy):
@Inject
@AuthenticatedUser
User authenticatedUser;
Beachten Sie, dass die CDI - @Produces
Annotation ist unterschiedlich von der JAX-RS @Produces
Anmerkung:
Stellen Sie sicher, dass Sie die CDI- @Produces
Annotation in Ihrer AuthenticatedUserProducer
Bean verwenden.
Der Schlüssel hier ist die mit Anmerkungen versehene Bean, mit @RequestScoped
der Sie Daten zwischen Filtern und Ihren Beans austauschen können. Wenn Sie keine Ereignisse verwenden möchten, können Sie den Filter so ändern, dass der authentifizierte Benutzer in einer Bean mit Anforderungsbereich gespeichert und dann aus Ihren JAX-RS-Ressourcenklassen gelesen wird.
Im Vergleich zu dem Ansatz, der den überschreibt SecurityContext
, können Sie mit dem CDI-Ansatz den authentifizierten Benutzer von anderen Beans als JAX-RS-Ressourcen und -Anbietern abrufen.
Unterstützung der rollenbasierten Autorisierung
Weitere Informationen zur Unterstützung der rollenbasierten Autorisierung finden Sie in meiner anderen Antwort .
Token ausstellen
Ein Token kann sein:
- Undurchsichtig: Zeigt keine anderen Details als den Wert selbst an (wie eine zufällige Zeichenfolge).
- In sich geschlossen: Enthält Details zum Token selbst (wie JWT).
Siehe Details unten:
Zufällige Zeichenfolge als Token
Ein Token kann ausgegeben werden, indem eine zufällige Zeichenfolge generiert und zusammen mit der Benutzer-ID und einem Ablaufdatum in einer Datenbank gespeichert wird. Ein gutes Beispiel für das Generieren einer zufälligen Zeichenfolge in Java finden Sie hier . Sie könnten auch verwenden:
Random random = new SecureRandom();
String token = new BigInteger(130, random).toString(32);
JWT (JSON Web Token)
JWT (JSON Web Token) ist eine Standardmethode zur sicheren Darstellung von Ansprüchen zwischen zwei Parteien und wird vom RFC 7519 definiert .
Es ist ein in sich geschlossenes Token, mit dem Sie Details in Ansprüchen speichern können . Diese Ansprüche werden in der Token-Nutzlast gespeichert, bei der es sich um einen als Base64 codierten JSON handelt . Hier sind einige im RFC 7519 registrierte Ansprüche und deren Bedeutung (lesen Sie den vollständigen RFC für weitere Details):
iss
: Auftraggeber, der den Token ausgestellt hat.
sub
: Auftraggeber, der Gegenstand des JWT ist.
exp
: Ablaufdatum für das Token.
nbf
: Zeitpunkt, zu dem das Token zur Verarbeitung angenommen wird.
iat
: Zeitpunkt, zu dem der Token ausgestellt wurde.
jti
: Eindeutige Kennung für das Token.
Beachten Sie, dass Sie keine vertraulichen Daten wie Kennwörter im Token speichern dürfen.
Die Nutzdaten können vom Client gelesen werden, und die Integrität des Tokens kann leicht überprüft werden, indem seine Signatur auf dem Server überprüft wird. Die Signatur verhindert, dass das Token manipuliert wird.
Sie müssen JWT-Token nicht beibehalten, wenn Sie sie nicht verfolgen müssen. Wenn Sie die Token beibehalten, haben Sie jedoch die Möglichkeit, den Zugriff auf sie zu ungültig zu machen und zu widerrufen. Um den Überblick über JWT-Token zu behalten, können Sie anstelle des gesamten Tokens auf dem Server die Token-ID ( jti
Claim) sowie einige andere Details wie den Benutzer, für den Sie das Token ausgestellt haben, das Ablaufdatum usw. beibehalten.
Wenn Sie Token beibehalten, sollten Sie immer die alten entfernen, um zu verhindern, dass Ihre Datenbank unbegrenzt wächst.
Verwenden von JWT
Es gibt einige Java-Bibliotheken, mit denen JWT-Token ausgestellt und validiert werden können, z.
Unter http://jwt.io finden Sie weitere nützliche Ressourcen für die Arbeit mit JWT .
Umgang mit Token-Widerruf mit JWT
Wenn Sie Token widerrufen möchten, müssen Sie den Überblick behalten. Sie müssen nicht das gesamte Token auf der Serverseite speichern, sondern nur die Token-ID (die eindeutig sein muss) und einige Metadaten, falls erforderlich. Für die Token-ID können Sie die UUID verwenden .
Der jti
Anspruch sollte verwendet werden, um die Token-ID auf dem Token zu speichern. Stellen Sie bei der Validierung des Tokens sicher, dass es nicht widerrufen wurde, indem Sie den Wert des jti
Anspruchs mit den Token-IDs vergleichen, die Sie auf der Serverseite haben.
Widerrufen Sie aus Sicherheitsgründen alle Token für einen Benutzer, wenn dieser sein Kennwort ändert.
Zusätzliche Information
- Es spielt keine Rolle, für welche Art von Authentifizierung Sie sich entscheiden. Immer tut es auf der Spitze eine HTTPS - Verbindung , um zu verhindern Man-in-the-Middle - Angriff .
- Weitere Informationen zu Token finden Sie in dieser Frage von Information Security.
- In diesem Artikel finden Sie einige nützliche Informationen zur tokenbasierten Authentifizierung.
The server stores the previously generated token in some storage along with the user identifier and an expiration date. The server sends the generated token to the client.
Wie ist das RESTful?Unterstützung der rollenbasierten Autorisierung mit der
@Secured
AnmerkungNeben dem in der anderen Antwort gezeigten Authentifizierungsablauf kann die rollenbasierte Autorisierung in den REST-Endpunkten unterstützt werden.
Erstellen Sie eine Aufzählung und definieren Sie die Rollen entsprechend Ihren Anforderungen:
Ändern Sie die
@Secured
zuvor erstellte Namensbindungsanmerkung, um Rollen zu unterstützen:Kommentieren Sie dann die Ressourcenklassen und -methoden mit
@Secured
, um die Autorisierung durchzuführen. Die Methodenanmerkungen überschreiben die Klassenanmerkungen:Erstellen Sie einen Filter mit der
AUTHORIZATION
Priorität, der nach demAUTHENTICATION
zuvor definierten Prioritätsfilter ausgeführt wird .Mit
ResourceInfo
kann die RessourceMethod
und RessourceClass
abgerufen werden, die die Anforderung verarbeiten, und anschließend die@Secured
Anmerkungen daraus extrahiert werden:Wenn der Benutzer keine Berechtigung zum Ausführen des Vorgangs hat, wird die Anforderung mit einem
403
(Verboten) abgebrochen .Informationen zum Benutzer, der die Anforderung ausführt, finden Sie in meiner vorherigen Antwort . Sie können es von der
SecurityContext
(die bereits in der eingestellt sein sollteContainerRequestContext
) erhalten oder mit CDI injizieren, je nachdem, welchen Ansatz Sie wählen.Wenn für eine
@Secured
Anmerkung keine Rollen deklariert sind, können Sie davon ausgehen, dass alle authentifizierten Benutzer auf diesen Endpunkt zugreifen können, ohne die Rollen der Benutzer zu berücksichtigen.Unterstützung der rollenbasierten Autorisierung mit JSR-250-Anmerkungen
Alternativ zur Definition der Rollen in der
@Secured
Anmerkung , wie oben gezeigt, können Sie JSR-250 Anmerkungen berücksichtigen wie@RolesAllowed
,@PermitAll
und@DenyAll
.JAX-RS unterstützt solche Annotationen nicht sofort, kann jedoch mit einem Filter erreicht werden. Hier sind einige Überlegungen, die Sie berücksichtigen sollten, wenn Sie alle unterstützen möchten:
@DenyAll
auf das Verfahren hat Vorrang vor@RolesAllowed
und@PermitAll
auf die Klasse.@RolesAllowed
Die Methode hat Vorrang vor@PermitAll
der Klasse.@PermitAll
Die Methode hat Vorrang vor@RolesAllowed
der Klasse.@DenyAll
kann nicht an Klassen angehängt werden.@RolesAllowed
in der Klasse hat Vorrang vor@PermitAll
der Klasse.Ein Autorisierungsfilter, der JSR-250-Anmerkungen überprüft, könnte also wie folgt aussehen:
Hinweis: Die obige Implementierung basiert auf dem Jersey
RolesAllowedDynamicFeature
. Wenn Sie Jersey verwenden, müssen Sie keinen eigenen Filter schreiben, sondern nur die vorhandene Implementierung verwenden.quelle
user_id
==token.userId
oder so ähnlich ist, aber das wiederholt sich sehr oft.