Strategie di autorizzazione API REST

7

Ci sono molte domande qui che trattano i meccanismi di autenticazione e autorizzazione delle API RESTful ma nessuno di loro sembra entrare nei dettagli su come implementare servizi sicuri a livello di applicazione.

Per esempio diciamo che la mia webapp (ho in mente Java, ma questo vale per qualsiasi back-end) ha un sistema di autenticazione sicuro che consente agli utenti dell'API di accedere con un nome utente e una password. Quando l'utente effettua una richiesta, in qualsiasi momento durante la pipeline di elaborazione della richiesta, posso chiamare un metodo getAuthenticatedUser() che restituirà l'utente null se l'utente non ha effettuato l'accesso o un oggetto dominio utente che rappresenta l'utente connesso .

L'API consente agli utenti autenticati di accedere ai propri dati, ad es. un GET a /api/orders/ restituirà l'elenco di ordini dell'utente. Allo stesso modo, un GET a /api/tasks/{task_id} restituirà i dati relativi a quell'attività specifica.

Supponiamo che ci siano diversi oggetti di dominio che possono essere associati all'account di un utente (ordini e attività sono due esempi, potremmo anche avere clienti, fatture, ecc.). Vogliamo solo che gli utenti autenticati siano in grado di accedere ai dati relativi ai propri oggetti, quindi quando un utente effettua una chiamata a /api/invoices/{invoice_id} , è necessario verificare che l'utente sia autorizzato ad accedere a tale risorsa prima di servirla.

La mia domanda è allora, esistono schemi o strategie per affrontare questo problema di autorizzazione? Un'opzione che sto considerando è la creazione di un'interfaccia helper (cioè SecurityUtils.isUserAuthorized(user, object) ), che può essere richiamata durante l'elaborazione della richiesta per garantire che l'utente sia autorizzato a recuperare l'oggetto. Questo non è l'ideale in quanto inquina il codice dell'endpoint dell'applicazione con molte di queste chiamate, ad es.

Object someEndpoint(int objectId) {
    if (!SecurityUtils.isUserAuthorized(loggedInUser, objectDAO.get(objectId)) {
        throw new UnauthorizedException();
    }
    ...
}

... e poi c'è la questione di implementare questo metodo per ogni tipo di dominio che potrebbe essere un po 'doloroso. Questa potrebbe essere l'unica opzione, ma sarei interessato a sentire i tuoi suggerimenti!

    
posta HJCee 01.08.2016 - 14:39
fonte

3 risposte

7

Ti preghiamo di amore per Dio non creare una classe SecurityUtils !

La tua classe diventerà 10 righe di codice spaghetti in pochi mesi! Dovresti avere un tipo Action (creare, leggere, aggiornare, distruggere, elencare, ecc.) Passato nel tuo metodo isUserAuthorized() , che diventerebbe rapidamente un'istruzione di switch di mille righe con logica sempre più complessa sarebbe difficile fare un test unitario. Non farlo.

Generalmente ciò che faccio, almeno in Ruby on Rails, è che ogni oggetto dominio è responsabile dei propri privilegi di accesso avendo una classe politica per ogni modello . Quindi, il controller richiede la classe della politica se l'utente corrente per la richiesta ha accesso alla risorsa o meno. Ecco un esempio in Ruby, dal momento che non ho mai implementato nulla di simile in Java, ma l'idea dovrebbe essere chiara:

class OrderPolicy

    class Scope < Struct.new(:user, :scope)

        def resolve

            # A user must be logged in to interact with this resource at all
            raise NotAuthorizedException unless user

            # Admin/moderator can see all orders
            if (user.admin? || user.moderator?)
                scope.all
            else
                # Only allow the user to see their own orders
                scope.where(orderer_id: user.id)
            end
        end
    end

    # Constructor, if you don't know Ruby
    def initialize(user, order)
        raise NotAuthorizedException unless user
        @user = user
        @order= order
    end

    # Whitelist what data can be manipulated by each type of user
    def valid_attributes
        if @user.admin?
            [:probably, :want, :to, :let, :admin, :update, :everything]
        elsif @user.moderator?
            [:fewer, :attributes, :but, :still, :most]
        else
            [:regualar, :user, :attributes]
        end
    end

    # Maybe restrict updatable attributes further
    def valid_update_attributes
    end

    # Who can create new orders
    def create?
        true # anyone, and they would have been authenticated already by #initialize
    end

    # Read operation
    def show?
        @user.admin? || @user.moderator? || owns_order
    end

    # Only superusers can update resources
    def update?
        @user.admin? || @user.moderator?
    end

    # Only admins can delete, because it's extremely destructive or whatever
    def destroy?
        @user.admin?
    end

    private

    # A user 'owns' an order if they were the person who submitted the order
    # E.g. superusers can access the order, but they didn't create it
    def owns_order
        @order.orderer_id == @user.id
    end
end

Anche se disponi di risorse nidificate complesse, alcune risorse devono "possedere" le risorse nidificate, quindi la logica di livello superiore è intrinsecamente negativa. Tuttavia, tali risorse nidificate necessitano delle proprie classi di politiche nel caso in cui possano essere aggiornate indipendentemente dalla risorsa "principale".

Nella mia applicazione, che è per il dipartimento della mia università, tutto ruota attorno all'oggetto Course . Non è un'applicazione User -centric. Tuttavia, User s sono iscritti in Course , quindi posso semplicemente assicurarmi che:

@course.users.include? current_user && (whatever_other_logic_I_need)

per qualsiasi risorsa che un particolare User deve modificare, poiché quasi tutte le risorse sono legate a Course . Questo viene fatto nella classe della politica nel metodo owns_whatever .

Non ho fatto molta architettura Java, ma sembra che tu possa creare un'interfaccia Policy , in cui le diverse risorse che devono essere autenticate devono implementare l'interfaccia. Quindi, disponi di tutti i metodi necessari che possono diventare complessi quanto è necessario che siano per oggetto dominio . L'importante è legare la logica al modello stesso, ma allo stesso tempo tenerla in una classe separata (principio di responsabilità singola (SRP)).

Le azioni del tuo controller potrebbero essere qualcosa di simile a:

public List<Order> index(OrderQuery query) {

    authorize(Order.class)
    // you should be auto-rescuing the NotAuthorizedException thrown by
    //the policy class at the controller level (or application level)

    // if the authorization didn't fail/rescue from exception, just render the resource
    List<Order> orders = db.search(query);
    return renderJSON(orders);
}

public Order show(int orderId) {

    authorize(Order.class)
    Order order = db.find(orderId);
    return renderJSON(order);
}
    
risposta data 04.08.2016 - 09:32
fonte
1

Una soluzione più comoda è usare le annotazioni per contrassegnare metodi che richiedono una qualche forma di autorizzazione. Ciò si distingue dal codice aziendale e può essere gestito da Spring Security o codice AOP personalizzato. Se si utilizzano queste annotazioni sui metodi aziendali anziché sugli endpoint, è possibile ottenere un'eccezione quando un utente non autorizzato tenta di chiamarli indipendentemente dal punto di ingresso.

    
risposta data 01.08.2016 - 15:15
fonte
0

Usa sicurezza basata sulle capacità.

Una capacità è un oggetto indimenticabile che funge da prova che si può eseguire una determinata azione. In questo caso:

  • Trasforma ciascun ruolo (insieme di azioni consentite) in un'interfaccia.
  • Le operazioni che richiedono l'autenticazione sono metodi sulle rispettive interfacce. Questi dovrebbero generare un'eccezione se il destinatario non è l'utente corrente della richiesta, se possibile.

Ciò rende impossibile provare a fare qualcosa che l'utente corrente non è autorizzato a fare.

In questo modo è impossibile

    
risposta data 14.08.2016 - 23:01
fonte

Leggi altre domande sui tag