it-swarm.com.de

Best Practice für die tokenbasierte REST Authentifizierung mit JAX-RS und Jersey

Ich suche nach einer Möglichkeit, die tokenbasierte Authentifizierung in Jersey zu aktivieren. Ich versuche, keinen bestimmten Rahmen zu verwenden. Ist das möglich?

Mein Plan ist: Ein Benutzer meldet sich für meinen Webdienst an, mein Webdienst generiert ein Token, sendet es an den Client und der Client behält es. Dann sendet der Client für jede Anforderung das Token anstelle von Benutzername und Kennwort.

Ich habe darüber nachgedacht, einen benutzerdefinierten Filter für jede Anforderung und @PreAuthorize("hasRole('ROLE')") zu verwenden, aber ich dachte nur, dass dies viele Anforderungen an die Datenbank verursacht, um zu überprüfen, ob das Token gültig ist.

Oder nicht Filter erstellen und in jede Anfrage ein Param-Token setzen? Damit überprüft jede API zuerst das Token und führt anschließend etwas aus, um die Ressource abzurufen.

422
DevOps85

So funktioniert die tokenbasierte Authentifizierung

Bei der tokenbasierten Authentifizierung tauscht der Client harte Anmeldeinformationen (wie z. B. Benutzername und Passwort) gegen eine Dateneinheit namens token aus. Anstatt die festen Anmeldeinformationen zu senden, sendet der Client bei jeder Anforderung das Token an den Server, um die Authentifizierung und anschließend die Autorisierung durchzuführen.

Mit wenigen Worten, ein auf Token basierendes Authentifizierungsschema folgt diesen Schritten:

  1. Der Client sendet seine Anmeldeinformationen (Benutzername und Kennwort) an den Server.
  2. Der Server authentifiziert die Anmeldeinformationen und generiert, falls sie gültig sind, ein Token für den Benutzer.
  3. Der Server speichert das zuvor generierte Token zusammen mit der Benutzerkennung und einem Ablaufdatum in einem Speicher.
  4. Der Server sendet das generierte Token an den Client.
  5. Der Client sendet das Token bei jeder Anforderung an den Server.
  6. 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.
  7. Sobald die Authentifizierung durchgeführt wurde, führt der Server die Autorisierung durch.
  8. Der Server kann einen Endpunkt zum Aktualisieren von Tokens bereitstellen.

Hinweis: Schritt 3 ist nicht erforderlich, wenn der Server ein signiertes Token (z. B. JWT) ausgegeben hat, mit dem Sie statuslos Authentifizierung).

Was Sie mit JAX-RS 2.0 (Jersey, RESTEasy und Apache CXF) tun können

Diese Lösung verwendet nur die JAX-RS 2.0-API. Keine herstellerspezifische Lösung. Daher sollte es mit JAX-RS 2.0-Implementierungen wie Jersey , RESTEasy und Apache CXF funktionieren.

Erwähnenswert ist, dass Sie sich bei der tokenbasierten Authentifizierung nicht auf die standardmäßigen Sicherheitsmechanismen für Java EE-Webanwendungen verlassen, die vom Servlet-Container angeboten werden und über den web.xml - Deskriptor der Anwendung konfiguriert werden können . Es ist eine benutzerdefinierte Authentifizierung.

Authentifizierung eines Benutzers mit seinem Benutzernamen und Passwort und Ausgabe eines Tokens

Erstellen Sie eine JAX-RS-Ressourcenmethode, die die Anmeldeinformationen (Benutzername und Kennwort) empfängt und überprüft, und stellen 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 beim Überprüfen der Anmeldeinformationen Ausnahmen ausgelöst werden, wird eine Antwort mit dem Status 403 (Verboten) zurückgegeben.

Wenn die Anmeldeinformationen erfolgreich validiert wurden, wird eine Antwort mit dem Status 200 (OK) zurückgegeben und das ausgestellte Token in der Antwortnutzlast an den Client gesendet. Der Client muss das Token bei jeder Anforderung an den Server senden.

Bei Verwendung von application/x-www-form-urlencoded Muss der Client die Anmeldeinformationen in der Anforderungsnutzlast im folgenden Format 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 verbrauchen Sie es dann 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 in der Nutzlast der Anforderung in folgendem Format senden:

{
  "username": "admin",
  "password": "123456"
}

Das Token aus der Anforderung extrahieren und validieren

Der Client sollte das Token im Standard-HTTP-Header Authorization der Anforderung senden. Zum Beispiel:

Authorization: Bearer <token-goes-here>

Der Name des Standard-HTTP-Headers ist unglücklich, da er Authentifizierung Informationen enthält, nicht Autorisierung. Es 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 Annotation @Secured Wie folgt:

@NameBinding
@Retention(RUNTIME)
@Target({TYPE, METHOD})
public @interface Secured { }

Die oben definierte Annotation für die Namensbindung wird zum Dekorieren einer Filterklasse verwendet, die ContainerRequestFilter implementiert, sodass Sie die Anforderung abfangen können, bevor sie von einer Ressourcenmethode verarbeitet wird. Mit ContainerRequestContext können Sie auf die HTTP-Anforderungsheader zugreifen und dann das Token extrahieren:

@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 oben erstellten Annotation @Secured. Für die mit Annotationen versehenen Methoden und/oder Klassen 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 only für die mySecuredMethod(Long) -Methode ausgeführt, da er mit @Secured Kommentiert ist.

Den aktuellen Benutzer identifizieren

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 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 Benutzerkennung (username) zu suchen, die der Name von Principal ist.

Injizieren Sie 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 erhalten Sie die Principal :

Principal principal = securityContext.getUserPrincipal();
String username = principal.getName();

Verwenden von CDI (Context and Dependency Injection)

Wenn Sie aus irgendeinem Grund das SecurityContext nicht überschreiben möchten, 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 Ihr AuthenticationFilter, das oben erstellt wurde, ein Event ein, das mit @AuthenticatedUser Versehen ist:

@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 Benutzer-ID 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 zur Behandlung des Authentifizierungsereignisses, suchen Sie eine User -Instanz mit dem entsprechenden Benutzernamen und weisen Sie sie dem Feld authenticatedUser producer 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 Feld authenticatedUser erzeugt eine UserInstanz, die in von Containern verwaltete Beans wie JAX-RS-Services, 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 anders von der JAX-RS @Produces Annotation ist:

Stellen Sie sicher, dass Sie die Annotation CDI @Produces in Ihrer Bean AuthenticatedUserProducer verwenden.

Der Schlüssel hier ist das mit @RequestScoped kommentierte Bean, mit dem 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 SecurityContext überschreibt, können Sie mit dem CDI-Ansatz den authentifizierten Benutzer aus 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 meinem anderen Antwort .

Token ausstellen

Ein Token kann sein:

  • Undurchsichtig: Zeigt keine anderen Details als den Wert selbst an (wie eine zufällige Zeichenfolge)
  • Eigenständig: 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 Benutzerkennung und einem Ablaufdatum in einer Datenbank gespeichert wird. Ein gutes Beispiel für das Generieren einer zufälligen Zeichenfolge in Java ist 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 durch RFC 7519 definiert.

Es ist ein in sich geschlossenes Token, mit dem Sie Details in Claims speichern können. Diese Ansprüche werden in der Token-Payload gespeichert, die ein JSON ist, das als Base64 codiert ist. Hier sind einige Ansprüche, die im RFC 7519 registriert sind, und was sie bedeuten (lesen Sie den vollständigen RFC für weitere Details):

  • iss : Principal, der das Token ausgegeben hat.
  • sub : Principal, der Gegenstand des JWT ist.
  • exp : Ablaufdatum für das Token.
  • nbf : Zeitpunkt, zu dem der Token zur Verarbeitung angenommen wird.
  • iat : Zeitpunkt, an dem das Token ausgestellt wurde.
  • jti : Eindeutiger Bezeichner 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 auf einfache Weise überprüft werden, indem die 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 diese Token zu ungültig zu machen und zu widerrufen. Um den Überblick über JWT-Token zu behalten, anstatt das gesamte Token auf dem Server zu behalten, können Sie die Token-ID ( jti claim) zusammen mit einigen anderen Details, z. B. dem von Ihnen verwendeten Benutzer, beibehalten ausgestellt das Token für, das Ablaufdatum, etc.

Wenn Sie Token behalten, sollten Sie immer die alten Token entfernen, um ein unbegrenztes Anwachsen Ihrer Datenbank zu verhindern.

JWT verwenden

Es gibt einige Java Bibliotheken, mit denen JWT-Token ausgegeben und validiert werden können, beispielsweise:

Weitere nützliche Ressourcen für die Arbeit mit JWT finden Sie unter http://jwt.io .

Behandlung der Token-Aktualisierung mit JWT

Akzeptiere only gültige (und nicht abgelaufene) Token zur Erfrischung. Es liegt in der Verantwortung des Kunden, die Token vor dem im Anspruch exp angegebenen Ablaufdatum zu aktualisieren.

Sie sollten verhindern, dass die Token auf unbestimmte Zeit aktualisiert werden. Im Folgenden finden Sie einige Ansätze, die Sie in Betracht ziehen könnten.

Sie können die Aktualisierung des Tokens verfolgen, indem Sie Ihrem Token zwei Ansprüche hinzufügen (die Namen der Ansprüche liegen bei Ihnen):

  • refreshLimit: Gibt an, wie oft das Token aktualisiert werden kann.
  • refreshCount: Gibt an, wie oft das Token aktualisiert wurde.

Aktualisieren Sie das Token daher nur, wenn die folgenden Bedingungen erfüllt sind:

  • Das Token ist nicht abgelaufen (exp >= now).
  • Die Häufigkeit, mit der das Token aktualisiert wurde, ist geringer als die Häufigkeit, mit der das Token aktualisiert werden kann (refreshCount < refreshLimit).

Und beim Auffrischen des Tokens:

  • Aktualisieren Sie das Ablaufdatum (exp = now + some-amount-of-time).
  • Erhöhen Sie die Anzahl der Aktualisierungen des Tokens (refreshCount++).

Alternativ dazu können Sie die Anzahl der Erfrischungen protokollieren, indem Sie einen Anspruch angeben, der das absolute Ablaufdatum angibt (was dem oben beschriebenen Anspruch refreshLimit ziemlich ähnlich ist). Vor dem absoluten Verfallsdatum sind beliebig viele Erfrischungen zulässig.

Ein weiterer Ansatz besteht darin, ein separates, langlebiges Aktualisierungstoken auszugeben, mit dem kurzlebige JWT-Token ausgegeben werden.

Der beste Ansatz hängt von Ihren Anforderungen ab.

Behandlung des Token-Widerrufs mit JWT

Wenn Sie Token widerrufen möchten, müssen Sie den Überblick behalten. Sie müssen nicht das gesamte Token serverseitig speichern, sondern nur die Token-ID (die eindeutig sein muss) und ggf. einige Metadaten. Für die Token-ID können Sie UUID verwenden.

Der Anspruch jti sollte verwendet werden, um die Token-ID auf dem Token zu speichern. Stellen Sie beim Überprüfen des Tokens sicher, dass es nicht widerrufen wurde, indem Sie den Wert des Anspruchs jti mit den Tokenkennungen vergleichen, die Sie auf der Serverseite haben.

Sperren 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 der Authentifizierung Sie sich entscheiden. Führen Sie dies immer über einer HTTPS-Verbindung aus, um den Man-in-the-Middle-Angriff zu verhindern.
  • Schauen Sie sich diese Frage von Information Security an, um weitere Informationen zu Token zu erhalten.
  • In diesem Artikel Hier finden Sie einige nützliche Informationen zur tokenbasierten Authentifizierung.
1307
cassiomolin

Diese Antwort handelt von Autorisierung und ist eine Ergänzung von meine vorherige Antwort about Authentifizierung

Warum noch eine Antwort? Ich habe versucht, meine vorherige Antwort zu erweitern, indem ich Details zur Unterstützung von JSR- hinzufügte. 250 Anmerkungen. Allerdings wurde die ursprüngliche Antwort übrigens zu lang und überschritt das maximale Länge von 30.000 Zeichen . Daher habe ich die gesamten Autorisierungsdetails in diese Antwort verschoben und die andere Antwort auf die Durchführung der Authentifizierung und die Ausgabe von Tokens konzentriert.


Unterstützung der rollenbasierten Autorisierung mit der Anmerkung _@Secured_

Neben dem Authentifizierungsablauf, der im anderen answer angegeben ist, kann die rollenbasierte Autorisierung auf den REST Endpunkten unterstützt werden.

Erstellen Sie eine Aufzählung und definieren Sie die Rollen gemäß Ihren Anforderungen:

_public enum Role {
    ROLE_1,
    ROLE_2,
    ROLE_3
}
_

Ändern Sie die zuvor erstellte _@Secured_ -Namensbindungsannotation, um Rollen zu unterstützen:

_@NameBinding
@Retention(RUNTIME)
@Target({TYPE, METHOD})
public @interface Secured {
    Role[] value() default {};
}
_

Kommentieren Sie anschließend die Ressourcenklassen und -methoden mit _@Secured_, um die Autorisierung durchzuführen. Die Methodenanmerkungen überschreiben die Klassenanmerkungen:

_@Path("/example")
@Secured({Role.ROLE_1})
public class ExampleResource {

    @GET
    @Path("{id}")
    @Produces(MediaType.APPLICATION_JSON)
    public Response myMethod(@PathParam("id") Long id) {
        // This method is not annotated with @Secured
        // But it's declared within a class annotated with @Secured({Role.ROLE_1})
        // So it only can be executed by the users who have the ROLE_1 role
        ...
    }

    @DELETE
    @Path("{id}")    
    @Produces(MediaType.APPLICATION_JSON)
    @Secured({Role.ROLE_1, Role.ROLE_2})
    public Response myOtherMethod(@PathParam("id") Long id) {
        // This method is annotated with @Secured({Role.ROLE_1, Role.ROLE_2})
        // The method annotation overrides the class annotation
        // So it only can be executed by the users who have the ROLE_1 or ROLE_2 roles
        ...
    }
}
_

Erstellen Sie einen Filter mit der Priorität AUTHORIZATION , der nach dem zuvor definierten Prioritätsfilter AUTHENTICATION ausgeführt wird.

Mit ResourceInfo können die Ressource Method und die Ressource Class abgerufen werden, die die Anforderung verarbeiten und dann die _@Secured_ -Anmerkungen aus extrahieren Sie:

_@Secured
@Provider
@Priority(Priorities.AUTHORIZATION)
public class AuthorizationFilter implements ContainerRequestFilter {

    @Context
    private ResourceInfo resourceInfo;

    @Override
    public void filter(ContainerRequestContext requestContext) throws IOException {

        // Get the resource class which matches with the requested URL
        // Extract the roles declared by it
        Class<?> resourceClass = resourceInfo.getResourceClass();
        List<Role> classRoles = extractRoles(resourceClass);

        // Get the resource method which matches with the requested URL
        // Extract the roles declared by it
        Method resourceMethod = resourceInfo.getResourceMethod();
        List<Role> methodRoles = extractRoles(resourceMethod);

        try {

            // Check if the user is allowed to execute the method
            // The method annotations override the class annotations
            if (methodRoles.isEmpty()) {
                checkPermissions(classRoles);
            } else {
                checkPermissions(methodRoles);
            }

        } catch (Exception e) {
            requestContext.abortWith(
                Response.status(Response.Status.FORBIDDEN).build());
        }
    }

    // Extract the roles from the annotated element
    private List<Role> extractRoles(AnnotatedElement annotatedElement) {
        if (annotatedElement == null) {
            return new ArrayList<Role>();
        } else {
            Secured secured = annotatedElement.getAnnotation(Secured.class);
            if (secured == null) {
                return new ArrayList<Role>();
            } else {
                Role[] allowedRoles = secured.value();
                return Arrays.asList(allowedRoles);
            }
        }
    }

    private void checkPermissions(List<Role> allowedRoles) throws Exception {
        // Check if the user contains one of the allowed roles
        // Throw an Exception if the user has not permission to execute the method
    }
}
_

Wenn der Benutzer keine Berechtigung zum Ausführen des Vorgangs hat, wird die Anforderung mit einem _403_ (Forbidden) abgebrochen.

Informationen zum Benutzer, der die Anforderung ausführt, finden Sie unter meine vorherige Antwort . Sie können es von SecurityContext (das bereits in ContainerRequestContext eingestellt sein sollte) beziehen oder es mit CDI injizieren, je nachdem, welchen Ansatz Sie wählen.

Wenn für eine _@Secured_ -Annotation keine Rollen deklariert wurden, 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 oben gezeigten Definition der Rollen in der Annotation _@Secured_ können Sie auch JSR-250-Annotationen wie @RolesAllowed , @PermitAll in Betracht ziehen = und @DenyAll .

JAX-RS unterstützt solche Annotationen nicht, kann jedoch mit einem Filter erstellt werden. Hier einige Überlegungen, die Sie berücksichtigen sollten, wenn Sie alle unterstützen möchten:

Ein Berechtigungsfilter, der JSR-250-Annotationen überprüft, könnte also folgendermaßen aussehen:

_@Provider
@Priority(Priorities.AUTHORIZATION)
public class AuthorizationFilter implements ContainerRequestFilter {

    @Context
    private ResourceInfo resourceInfo;

    @Override
    public void filter(ContainerRequestContext requestContext) throws IOException {

        Method method = resourceInfo.getResourceMethod();

        // @DenyAll on the method takes precedence over @RolesAllowed and @PermitAll
        if (method.isAnnotationPresent(DenyAll.class)) {
            refuseRequest();
        }

        // @RolesAllowed on the method takes precedence over @PermitAll
        RolesAllowed rolesAllowed = method.getAnnotation(RolesAllowed.class);
        if (rolesAllowed != null) {
            performAuthorization(rolesAllowed.value(), requestContext);
            return;
        }

        // @PermitAll on the method takes precedence over @RolesAllowed on the class
        if (method.isAnnotationPresent(PermitAll.class)) {
            // Do nothing
            return;
        }

        // @DenyAll can't be attached to classes

        // @RolesAllowed on the class takes precedence over @PermitAll on the class
        rolesAllowed = 
            resourceInfo.getResourceClass().getAnnotation(RolesAllowed.class);
        if (rolesAllowed != null) {
            performAuthorization(rolesAllowed.value(), requestContext);
        }

        // @PermitAll on the class
        if (resourceInfo.getResourceClass().isAnnotationPresent(PermitAll.class)) {
            // Do nothing
            return;
        }

        // Authentication is required for non-annotated methods
        if (!isAuthenticated(requestContext)) {
            refuseRequest();
        }
    }

    /**
     * Perform authorization based on roles.
     *
     * @param rolesAllowed
     * @param requestContext
     */
    private void performAuthorization(String[] rolesAllowed, 
                                      ContainerRequestContext requestContext) {

        if (rolesAllowed.length > 0 && !isAuthenticated(requestContext)) {
            refuseRequest();
        }

        for (final String role : rolesAllowed) {
            if (requestContext.getSecurityContext().isUserInRole(role)) {
                return;
            }
        }

        refuseRequest();
    }

    /**
     * Check if the user is authenticated.
     *
     * @param requestContext
     * @return
     */
    private boolean isAuthenticated(final ContainerRequestContext requestContext) {
        // Return true if the user is authenticated or false otherwise
        // An implementation could be like:
        // return requestContext.getSecurityContext().getUserPrincipal() != null;
    }

    /**
     * Refuse the request.
     */
    private void refuseRequest() {
        throw new AccessDeniedException(
            "You don't have permissions to perform this action.");
    }
}
_

Hinweis: Die obige Implementierung basiert auf dem Jersey RolesAllowedDynamicFeature . Wenn Sie Jersey verwenden, müssen Sie keinen eigenen Filter schreiben. Verwenden Sie einfach die vorhandene Implementierung.

80
cassiomolin