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

459

Ich suche nach einer Möglichkeit, die tokenbasierte Authentifizierung in Jersey zu aktivieren. Ich versuche, kein bestimmtes Framework 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 bei. Dann sendet der Client für jede Anforderung das Token anstelle des Benutzernamens und des Kennworts.

Ich habe überlegt, für jede Anforderung einen benutzerdefinierten Filter zu verwenden, @PreAuthorize("hasRole('ROLE')") aber ich dachte nur, dass dies dazu führt, dass viele Anforderungen an die Datenbank prüfen, ob das Token gültig ist.

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

DevOps85
quelle

Antworten:

1388

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:

  1. Der Client sendet seine Anmeldeinformationen (Benutzername und Passwort) 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 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-urlencodedmuss 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- AuthorizationHeader 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 @NameBindingeine Meta-Annotation, mit der andere Annotationen erstellt werden, um Filter und Interceptors an Ressourcenklassen und -methoden zu binden. Definieren Sie eine @SecuredAnmerkung 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 ContainerRequestContextkann 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 @Securedoben 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 SecurityContextInstanz für die aktuelle Anforderung festgelegt werden. Überschreiben Sie dann die SecurityContext.getUserPrincipal()und geben Sie eine PrincipalInstanz 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 PrincipalName des Benutzers ist .

Injizieren Sie die SecurityContextin 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 AuthenticationFilteroben erstellten Kommentar EventFolgendes 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 UserInstanz mit dem entsprechenden Benutzernamen und weisen Sie sie dem authenticatedUserProduzentenfeld 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 authenticatedUserFeld erzeugt eine UserInstanz, 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 UserInstanz einzufügen (tatsächlich handelt es sich um einen CDI-Proxy):

@Inject
@AuthenticatedUser
User authenticatedUser;

Beachten Sie, dass die CDI - @ProducesAnnotation ist unterschiedlich von der JAX-RS @ProducesAnmerkung:

Stellen Sie sicher, dass Sie die CDI- @ProducesAnnotation in Ihrer AuthenticatedUserProducerBean verwenden.

Der Schlüssel hier ist die mit Anmerkungen versehene Bean, mit @RequestScopedder 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 ( jtiClaim) 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 jtiAnspruch 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 jtiAnspruchs 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.
Cassiomolin
quelle
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?
Scottysseus
3
@scottyseus Die Token-basierte Authentifizierung funktioniert so, wie sich der Server das von ihm ausgestellte Token merkt. Sie können JWT-Token für die zustandslose Authentifizierung verwenden.
Cassiomolin
Was ist mit dem Senden eines Hash-Passworts anstelle eines einfachen (Hash mit einem vom Server generierten Nonce)? Erhöht es die Sicherheitsstufe (zum Beispiel, wenn https nicht verwendet wird)? Im Falle eines Mannes in der Mitte - er wird in der Lage sein, die eine Sitzung zu entführen, aber zumindest wird er das Passwort nicht erhalten
Denis Itskovich
15
Ich kann nicht glauben, dass dies nicht in der offiziellen Dokumentation steht.
Daniel M.
2
@grep In REST gibt es keine Sitzung auf der Serverseite. Folglich wird der Sitzungsstatus auf der Clientseite verwaltet.
Cassiomolin
98

Diese Antwort dreht sich alles um Autorisierung und ist eine Ergänzung meiner vorherigen Antwort zur Authentifizierung

Warum noch eine Antwort? Ich habe versucht, meine vorherige Antwort zu erweitern, indem ich Details zur Unterstützung von JSR-250-Anmerkungen hinzugefügt habe. Die ursprüngliche Antwort wurde jedoch viel zu lang und überschritt die maximale Länge von 30.000 Zeichen . Daher habe ich die gesamten Autorisierungsdetails in diese Antwort verschoben, wobei sich die andere Antwort auf die Durchführung der Authentifizierung und die Ausgabe von Token konzentrierte.


Unterstützung der rollenbasierten Autorisierung mit der @SecuredAnmerkung

Neben 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:

public enum Role {
    ROLE_1,
    ROLE_2,
    ROLE_3
}

Ändern Sie die @Securedzuvor erstellte Namensbindungsanmerkung, um Rollen zu unterstützen:

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

Kommentieren Sie dann 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 AUTHORIZATIONPriorität, der nach dem AUTHENTICATIONzuvor definierten Prioritätsfilter ausgeführt wird .

Mit ResourceInfokann die Ressource Methodund Ressource Classabgerufen werden, die die Anforderung verarbeiten, und anschließend die @SecuredAnmerkungen daraus extrahiert werden:

@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(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 sollte ContainerRequestContext) erhalten oder mit CDI injizieren, je nachdem, welchen Ansatz Sie wählen.

Wenn für eine @SecuredAnmerkung 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 @SecuredAnmerkung , wie oben gezeigt, können Sie JSR-250 Anmerkungen berücksichtigen wie @RolesAllowed, @PermitAllund @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:

Ein Autorisierungsfilter, der JSR-250-Anmerkungen überprüft, könnte also wie folgt 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, sondern nur die vorhandene Implementierung verwenden.

Cassiomolin
quelle
Gibt es ein Github-Repository mit dieser eleganten Lösung?
Daniel Ferreira Castro
6
@ DanielFerreiraCastro Natürlich. Schauen Sie hier .
Cassiomolin
Gibt es eine gute Möglichkeit zu überprüfen, ob eine Anfrage von einem autorisierten Benutzer stammt UND dass der Benutzer die Daten ändern kann, weil er die Daten "besitzt" (z. B. kann ein Hacker sein Token nicht verwenden, um den Namen eines anderen Benutzers zu ändern)? Ich weiß, dass ich an jedem Endpunkt überprüfen kann, ob user_id== token.userIdoder so ähnlich ist, aber das wiederholt sich sehr oft.
mFeinstein
@mFeinstein Eine Antwort darauf würde sicherlich mehr Zeichen erfordern, als ich hier in den Kommentaren eingeben kann. Um Ihnen eine Richtung zu geben, können Sie nach Sicherheit auf Zeilenebene suchen .
Cassiomolin
Ich kann viele Themen in Datenbanken sehen, denn wenn ich nach Sicherheit auf
Zeilenebene suche