Najlepsza praktyka dla uwierzytelniania opartego na tokenach REST z JAX-RS i Jersey

459

Szukam sposobu włączenia uwierzytelniania opartego na tokenach w Jersey. Staram się nie używać żadnego konkretnego frameworka. Czy to jest możliwe?

Mój plan to: użytkownik rejestruje się w mojej usłudze internetowej, moja usługa internetowa generuje token, wysyła go do klienta, a klient go zatrzyma. Następnie klient dla każdego żądania wyśle ​​token zamiast nazwy użytkownika i hasła.

Myślałem o użyciu niestandardowego filtra dla każdego żądania, @PreAuthorize("hasRole('ROLE')") ale pomyślałem, że powoduje to, że wiele zapytań do bazy danych sprawdza, czy token jest prawidłowy.

A może nie utwórz filtra i w każdym żądaniu umieść token param? Tak, aby każdy interfejs API najpierw sprawdzał token, a następnie wykonywał coś w celu pobrania zasobu.

DevOps85
źródło

Odpowiedzi:

1387

Jak działa uwierzytelnianie oparte na tokenach

W uwierzytelnianiu opartym na tokenach klient wymienia twarde dane uwierzytelniające (takie jak nazwa użytkownika i hasło) na kawałek danych o nazwie token . Dla każdego żądania, zamiast wysyłać twarde dane uwierzytelniające, klient wyśle ​​token do serwera w celu wykonania uwierzytelnienia, a następnie autoryzacji.

W kilku słowach schemat uwierzytelniania oparty na tokenach wykonuje następujące kroki:

  1. Klient wysyła swoje poświadczenia (nazwę użytkownika i hasło) na serwer.
  2. Serwer uwierzytelnia poświadczenia i, jeśli są one prawidłowe, wygeneruje token dla użytkownika.
  3. Serwer przechowuje poprzednio wygenerowany token w pewnej pamięci wraz z identyfikatorem użytkownika i datą ważności.
  4. Serwer wysyła wygenerowany token do klienta.
  5. Klient wysyła token do serwera w każdym żądaniu.
  6. Serwer w każdym żądaniu wyodrębnia token z przychodzącego żądania. Za pomocą tokena serwer wyszukuje dane użytkownika w celu przeprowadzenia uwierzytelnienia.
    • Jeśli token jest prawidłowy, serwer akceptuje żądanie.
    • Jeśli token jest nieprawidłowy, serwer odrzuca żądanie.
  7. Po przeprowadzeniu uwierzytelnienia serwer wykonuje autoryzację.
  8. Serwer może zapewnić punkt końcowy do odświeżania tokenów.

Uwaga: Krok 3 nie jest wymagany, jeśli serwer wydał podpisany token (taki jak JWT, który umożliwia przeprowadzanie uwierzytelniania bezstanowego ).

Co możesz zrobić z JAX-RS 2.0 (Jersey, RESTEasy i Apache CXF)

To rozwiązanie wykorzystuje tylko interfejs API JAX-RS 2.0, co pozwala uniknąć rozwiązań specyficznych dla dostawcy . Powinien więc współpracować z implementacjami JAX-RS 2.0, takimi jak Jersey , RESTEasy i Apache CXF .

Warto wspomnieć, że jeśli używasz uwierzytelniania opartego na tokenach, nie polegasz na standardowych mechanizmach bezpieczeństwa aplikacji sieci Web Java EE oferowanych przez kontener serwletu i konfigurowalnych za pomocą web.xmldeskryptora aplikacji . To niestandardowe uwierzytelnianie.

Uwierzytelnianie użytkownika za pomocą nazwy użytkownika i hasła oraz wydawanie tokena

Utwórz metodę zasobu JAX-RS, która odbiera i sprawdza poświadczenia (nazwę użytkownika i hasło) i wydaje token dla użytkownika:

@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
    }
}

Jeśli podczas sprawdzania poświadczeń zostaną zgłoszone wyjątki, 403zostanie zwrócona odpowiedź o statusie (Zabronione).

Jeśli poświadczenia zostaną pomyślnie zweryfikowane, odpowiedź ze statusem 200(OK) zostanie zwrócona, a wydany token zostanie wysłany do klienta w ładunku odpowiedzi. Klient musi wysyłać token do serwera przy każdym żądaniu.

Podczas konsumpcji application/x-www-form-urlencodedklient musi wysłać poświadczenia w następującym formacie w ładunku żądania:

username=admin&password=123456

Zamiast parametrów formularza możliwe jest zawinięcie nazwy użytkownika i hasła w klasę:

public class Credentials implements Serializable {

    private String username;
    private String password;

    // Getters and setters omitted
}

A następnie skonsumuj jako 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
}

Korzystając z tego podejścia, klient musi wysłać poświadczenia w następującym formacie w ładunku żądania:

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

Wydobywanie tokena z żądania i sprawdzanie jego poprawności

Klient powinien wysłać token w standardowym Authorizationnagłówku HTTP żądania. Na przykład:

Authorization: Bearer <token-goes-here>

Nazwa standardowego nagłówka HTTP jest niefortunna, ponieważ przenosi informacje uwierzytelniające , a nie autoryzację . Jest to jednak standardowy nagłówek HTTP do wysyłania poświadczeń na serwer.

JAX-RS zapewnia @NameBindingmeta-adnotację służącą do tworzenia innych adnotacji w celu powiązania filtrów i przechwytywaczy z klasami i metodami zasobów. Zdefiniuj @Securedadnotację w następujący sposób:

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

Powyżej zdefiniowana adnotacja wiążąca nazwę zostanie użyta do udekorowania klasy filtru, która implementuje się ContainerRequestFilter, umożliwiając przechwycenie żądania przed przetworzeniem przez metodę zasobów. ContainerRequestContextMoże być używany do uzyskania dostępu do nagłówków HTTP, a następnie wyodrębnić token:

@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
    }
}

Jeśli podczas sprawdzania tokena 401wystąpią jakiekolwiek problemy, zostanie zwrócona odpowiedź o statusie (Nieautoryzowane). W przeciwnym razie żądanie przejdzie do metody zasobu.

Zabezpieczanie punktów końcowych REST

Aby powiązać filtr uwierzytelniania z metodami zasobów lub klasami zasobów, opatrz je @Securedadnotacjami utworzonymi powyżej. Dla metod i / lub klas, które są opatrzone adnotacjami, filtr zostanie wykonany. Oznacza to, że takie punkty końcowe zostaną osiągnięte tylko wtedy, gdy żądanie zostanie wykonane przy użyciu ważnego tokena.

Jeśli niektóre metody lub klasy nie wymagają uwierzytelnienia, nie dodawaj adnotacji:

@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
        ...
    }
}

W powyższym przykładzie filtr zostanie wykonany tylko dla mySecuredMethod(Long)metody, ponieważ jest opatrzony adnotacją @Secured.

Identyfikacja bieżącego użytkownika

Jest bardzo prawdopodobne, że będziesz musiał znać użytkownika, który wykonuje żądanie, również w interfejsie API REST. Aby to osiągnąć, można zastosować następujące podejścia:

Przesłanianie kontekstu bezpieczeństwa bieżącego żądania

W ramach tej ContainerRequestFilter.filter(ContainerRequestContext)metody SecurityContextmożna ustawić nową instancję dla bieżącego żądania. Następnie zastąp SecurityContext.getUserPrincipal(), zwracając Principalinstancję:

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;
    }
});

Użyj tokena, aby wyszukać identyfikator użytkownika (nazwę użytkownika), który będzie Principalnazwą.

Wstrzyknąć SecurityContextdowolną klasę zasobów JAX-RS:

@Context
SecurityContext securityContext;

To samo można zrobić w metodzie zasobów JAX-RS:

@GET
@Secured
@Path("{id}")
@Produces(MediaType.APPLICATION_JSON)
public Response myMethod(@PathParam("id") Long id, 
                         @Context SecurityContext securityContext) {
    ...
}

A następnie uzyskaj Principal:

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

Korzystanie z CDI (wstrzykiwanie kontekstu i zależności)

Jeśli z jakiegoś powodu nie chcesz przesłonić SecurityContext, możesz użyć CDI (wstrzykiwanie kontekstu i zależności), które zapewnia przydatne funkcje, takie jak zdarzenia i producenci.

Utwórz kwalifikator CDI:

@Qualifier
@Retention(RUNTIME)
@Target({ METHOD, FIELD, PARAMETER })
public @interface AuthenticatedUser { }

W AuthenticationFilterutworzonym powyżej wstrzyknij Eventadnotację @AuthenticatedUser:

@Inject
@AuthenticatedUser
Event<String> userAuthenticatedEvent;

Jeśli uwierzytelnienie się powiedzie, uruchom zdarzenie przekazujące nazwę użytkownika jako parametr (pamiętaj, że token jest wydawany użytkownikowi, a token zostanie użyty do wyszukania identyfikatora użytkownika):

userAuthenticatedEvent.fire(username);

Jest bardzo prawdopodobne, że w twojej aplikacji istnieje klasa reprezentująca użytkownika. Nazwijmy tę klasę User.

Utwórz komponent bean CDI do obsługi zdarzenia uwierzytelnienia, znajdź Userinstancję z odpowiednią nazwą użytkownika i przypisz ją do authenticatedUserpola producenta:

@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
    }
}

authenticatedUserPola powoduje Userwystąpienie, które mogą być wstrzykiwane do kontenerów udało ziaren, takich jak usługi JAX-RS, CDI fasoli, serwletów i EJB. Użyj poniższego kodu, aby wstrzyknąć Userinstancję (w rzeczywistości jest to proxy CDI):

@Inject
@AuthenticatedUser
User authenticatedUser;

Uwaga: @Producesadnotacja CDI różni się od @Producesadnotacji JAX-RS :

Upewnij się, że używasz @Producesadnotacji CDI w swojej AuthenticatedUserProducerfasoli.

Kluczem jest tutaj fasola opatrzona adnotacjami @RequestScoped, umożliwiająca współdzielenie danych między filtrami a ziarnami. Jeśli nie chcesz używać zdarzeń, możesz zmodyfikować filtr, aby przechowywać uwierzytelnionego użytkownika w komponencie bean o zasięgu żądania, a następnie odczytać go z klas zasobów JAX-RS.

W porównaniu z podejściem, które zastępuje podejście SecurityContext, podejście CDI pozwala uzyskać uwierzytelnionego użytkownika z komponentów bean innych niż zasoby i dostawcy JAX-RS.

Obsługa autoryzacji opartej na rolach

Proszę odnieść się do mojej drugiej odpowiedzi, aby uzyskać szczegółowe informacje na temat obsługi autoryzacji opartej na rolach.

Wydawanie tokenów

Tokenem może być:

  • Nieprzezroczysty: nie ujawnia żadnych szczegółów poza samą wartością (jak ciąg losowy)
  • Samodzielny: zawiera szczegółowe informacje o samym tokenie (np. JWT).

Szczegóły poniżej:

Losowy ciąg jako token

Token można wydać, generując losowy ciąg znaków i utrwalając go w bazie danych wraz z identyfikatorem użytkownika i datą ważności. Dobry przykład generowania losowego ciągu znaków w Javie można zobaczyć tutaj . Możesz także użyć:

Random random = new SecureRandom();
String token = new BigInteger(130, random).toString(32);

JWT (token sieciowy JSON)

JWT (JSON Web Token) to standardowa metoda bezpiecznego reprezentowania roszczeń między dwiema stronami i jest zdefiniowana w RFC 7519 .

Jest to samodzielny token i umożliwia przechowywanie szczegółów w roszczeniach . Roszczenia te są przechowywane w ładunku tokena, który jest kodem JSON zakodowanym jako Base64 . Oto niektóre roszczenia zarejestrowane w RFC 7519 i ich znaczenie (przeczytaj pełne RFC w celu uzyskania dalszych szczegółów):

  • iss: Zleceniodawca, który wydał token.
  • sub: Zleceniodawca będący przedmiotem JWT.
  • exp: Data ważności tokena.
  • nbf: Czas, w którym token zacznie być akceptowany do przetwarzania.
  • iat: Czas, w którym token został wydany.
  • jti: Unikalny identyfikator tokena.

Pamiętaj, że nie możesz przechowywać poufnych danych, takich jak hasła, w tokenie.

Klient może odczytać ładunek, a integralność tokena można łatwo sprawdzić, weryfikując jego podpis na serwerze. Podpis zapobiega manipulowaniu tokenem.

Nie musisz utrwalać tokenów JWT, jeśli nie musisz ich śledzić. Chociaż utrzymując tokeny, będziesz mieć możliwość unieważnienia i cofnięcia dostępu do nich. Aby śledzić tokeny JWT, zamiast utrwalać cały token na serwerze, możesz zachować identyfikator tokena ( jtiroszczenie) wraz z innymi szczegółami, takimi jak użytkownik, dla którego wystawiłeś token, data ważności itp.

Podczas utrwalania tokenów zawsze należy rozważyć usunięcie starych, aby zapobiec nieograniczonemu rozwojowi bazy danych.

Korzystanie z JWT

Istnieje kilka bibliotek Java do wydawania i sprawdzania poprawności tokenów JWT, takich jak:

Aby znaleźć inne świetne zasoby do pracy z JWT, zajrzyj na http://jwt.io .

Obsługa odwołania tokenu za pomocą JWT

Jeśli chcesz odwołać tokeny, musisz je śledzić. Nie musisz przechowywać całego tokena po stronie serwera, przechowywać tylko identyfikator tokena (który musi być unikalny) i niektóre metadane, jeśli potrzebujesz. Jako identyfikator tokena możesz użyć UUID .

jtiRoszczenia powinny być wykorzystywane do przechowywania identyfikatora tokena na token. Podczas sprawdzania poprawności tokena upewnij się, że nie został on odwołany, sprawdzając wartość jtiroszczenia względem identyfikatorów tokena, które posiadasz po stronie serwera.

Ze względów bezpieczeństwa odwołaj wszystkie tokeny dla użytkownika, gdy zmieni hasło.

Dodatkowe informacje

  • Nie ma znaczenia, jakiego rodzaju uwierzytelnienia zdecydujesz się użyć. Zawsze rób to na połączeniu HTTPS, aby zapobiec atakowi typu man-in-the-middle .
  • Spójrz na to pytanie z działu Bezpieczeństwa Informacji, aby uzyskać więcej informacji na temat tokenów.
  • W tym artykule znajdziesz przydatne informacje na temat uwierzytelniania opartego na tokenach.
kasiomolina
źródło
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. Jak to jest RESTful?
Scottysseus
3
@ scottyseus Uwierzytelnianie oparte na tokenach działa według sposobu, w jaki serwer zapamiętuje wydany token. Tokenów JWT można używać do uwierzytelniania bezstanowego.
kasiomolina
Co powiesz na przesłanie zaszyfrowanego hasła zamiast zwykłego (zaszyfrowanego z nonce generowanej przez serwer)? Czy podnosi poziom bezpieczeństwa (na przykład, gdy nie używa https)? W przypadku mężczyzny pośrodku - będzie mógł przejąć jedną sesję, ale przynajmniej nie dostanie hasła
Denis Itskovich
15
Nie mogę uwierzyć, że nie ma tego w oficjalnej dokumentacji.
Daniel M.,
2
@grep W REST nie ma czegoś takiego jak sesja po stronie serwera. W związku z tym stanem sesji zarządza się po stronie klienta.
kasiomolina
98

Ta odpowiedź dotyczy autoryzacji i stanowi uzupełnienie mojej poprzedniej odpowiedzi na temat uwierzytelniania

Dlaczego kolejna odpowiedź? Próbowałem rozszerzyć moją poprzednią odpowiedź, dodając szczegółowe informacje na temat obsługi adnotacji JSR-250. Jednak oryginalna odpowiedź stała się zbyt długa i przekroczyła maksymalną długość 30 000 znaków . Przeniosłem więc wszystkie szczegóły autoryzacji do tej odpowiedzi, pozostawiając drugą odpowiedź skoncentrowaną na przeprowadzaniu uwierzytelniania i wydawaniu tokenów.


Obsługa autoryzacji opartej na rolach z @Securedadnotacją

Oprócz przepływu uwierzytelniania pokazanego w drugiej odpowiedzi , autoryzacja oparta na rolach może być obsługiwana w punktach końcowych REST.

Utwórz wyliczenie i zdefiniuj role zgodnie z własnymi potrzebami:

public enum Role {
    ROLE_1,
    ROLE_2,
    ROLE_3
}

Zmień @Securedutworzoną wcześniej adnotację wiązania nazwy, aby obsługiwała role:

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

A następnie dodaj adnotacje do klas zasobów i metod, @Securedaby wykonać autoryzację. Adnotacje metod zastąpią adnotacje klas:

@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
        ...
    }
}

Utwórz filtr z AUTHORIZATIONpriorytetem, który jest wykonywany po AUTHENTICATIONuprzednio zdefiniowanym filtrze priorytetu.

ResourceInfoMogą być wykorzystywane w celu uzyskania zasobów Methodi zasobów Class, które będą obsługiwać żądania, a następnie wyodrębnić @Securedadnotacje z nich:

@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
    }
}

Jeśli użytkownik nie ma uprawnień do wykonania operacji, żądanie jest przerywane za pomocą 403(Zabronione).

Aby poznać użytkownika, który wykonuje żądanie, zobacz moją poprzednią odpowiedź . Możesz go pobrać z SecurityContext(który powinien być już ustawiony w ContainerRequestContext) lub wstrzyknąć go za pomocą CDI, w zależności od wybranego podejścia.

Jeśli @Securedadnotacja nie ma zadeklarowanych ról, możesz założyć, że wszyscy uwierzytelnieni użytkownicy mogą uzyskać dostęp do tego punktu końcowego, niezależnie od ról, które mają użytkownicy.

Obsługa autoryzacji opartej na rolach za pomocą adnotacji JSR-250

Alternatywnie do definiowania ról w @Securedadnotacji, jak wykazano powyżej, można rozważyć JSR-250, takie jak adnotacje @RolesAllowed, @PermitAlli @DenyAll.

JAX-RS nie obsługuje takich adnotacji od razu po wyjęciu z pudełka, ale można to osiągnąć za pomocą filtra. Oto kilka kwestii, o których należy pamiętać, jeśli chcesz wesprzeć je wszystkie:

Filtr autoryzacji, który sprawdza adnotacje JSR-250, może wyglądać następująco:

@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.");
    }
}

Uwaga: powyższa implementacja oparta jest na Jersey RolesAllowedDynamicFeature. Jeśli używasz Jersey, nie musisz pisać własnego filtru, wystarczy użyć istniejącej implementacji.

kasiomolina
źródło
Czy jest dostępne repozytorium github z tym eleganckim rozwiązaniem?
Daniel Ferreira Castro
6
@DanielFerreiraCastro Oczywiście. Spójrz tutaj .
kasiomolina
Czy istnieje dobry sposób na sprawdzenie, czy żądanie pochodzi od autoryzowanego użytkownika ORAZ, że użytkownik MOŻE zmienić dane, ponieważ „jest właścicielem” danych (np. Aby haker nie mógł użyć swojego tokena do zmiany nazwy innego użytkownika)? Wiem, że mogę sprawdzić w każdym punkcie końcowym, czy user_id== token.userIdlub coś podobnego, ale jest to bardzo powtarzalne.
mFeinstein
@mFeinstein Odpowiedź na to z pewnością wymagałaby więcej znaków, niż mogę wpisać tutaj w komentarzach. Aby dać ci jakiś kierunek, możesz poszukać bezpieczeństwa na poziomie wiersza .
kasiomolina
Widzę wiele tematów w bazach danych, ponieważ kiedy szukam bezpieczeństwa na poziomie wiersza, otworzę to jako nowe pytanie
mFeinstein