Autenticazione RESTful tramite Spring


262

Problema:
disponiamo di un'API RESTful basata su Spring MVC che contiene informazioni riservate. L'API deve essere protetta, tuttavia non è consigliabile inviare le credenziali dell'utente (combo utente / pass) con ogni richiesta. Secondo le linee guida REST (e i requisiti aziendali interni), il server deve rimanere apolide. L'API verrà utilizzata da un altro server in un approccio in stile mashup.

Requisiti:

  • Il client invia una richiesta a .../authenticate(URL non protetto) con le credenziali; il server restituisce un token sicuro che contiene informazioni sufficienti per il server per convalidare le richieste future e rimanere apolidi. Probabilmente ciò consisterebbe nelle stesse informazioni del token Remember-Me di Spring Security .

  • Il client invia richieste successive a vari URL (protetti), aggiungendo il token precedentemente ottenuto come parametro di query (o, meno desiderabilmente, un'intestazione di richiesta HTTP).

  • Non ci si può aspettare che il cliente memorizzi i cookie.

  • Poiché utilizziamo già Spring, la soluzione dovrebbe utilizzare Spring Security.

Abbiamo sbattuto la testa contro il muro cercando di farlo funzionare, quindi spero che qualcuno là fuori abbia già risolto questo problema.

Dato lo scenario di cui sopra, come potresti risolvere questa particolare esigenza?


49
Ciao Chris, non sono sicuro che passare quel token nel parametro query sia la migliore idea. Verrà visualizzato nei registri, indipendentemente da HTTPS o HTTP. Le intestazioni sono probabilmente più sicure. Cordiali saluti. Ottima domanda però. +1
jmort253

1
Qual è la tua comprensione degli apolidi? Il tuo requisito di token si scontra con la mia comprensione degli apolidi. La risposta di autenticazione Http mi sembra l'unica implementazione senza stato.
Markus Malkusch,

9
@MarkusMalkusch stateless si riferisce alla conoscenza del server delle comunicazioni precedenti con un determinato client. HTTP è senza stato per definizione e i cookie di sessione lo rendono con stato. La durata (e la fonte, per quella materia) del token sono irrilevanti; al server importa solo che è valido e può essere ricollegato a un utente (NON a una sessione). Passare un token identificativo, quindi, non interferisce con la statualità.
Chris Cashwell,

1
@ChrisCashwell Come si fa a garantire che il token non venga falsificato / generato dal client? Usi una chiave privata sul lato server per crittografare il token, fornirlo al client e quindi utilizzare la stessa chiave per decrittografarlo durante richieste future? Ovviamente Base64 o qualche altro offuscamento non sarebbero sufficienti. Puoi approfondire le tecniche per la "validazione" di questi token?
Craig Otis,

6
Anche se questo è datato e non ho più toccato o aggiornato il codice per oltre 2 anni, ho creato un Gist per espandere ulteriormente questi concetti. gist.github.com/ccashwell/dfc05dd8bd1a75d189d1
Chris Cashwell,

Risposte:


190

Siamo riusciti a farlo funzionare esattamente come descritto nel PO, e speriamo che qualcun altro possa avvalersi della soluzione. Ecco cosa abbiamo fatto:

Imposta il contesto di sicurezza in questo modo:

<security:http realm="Protected API" use-expressions="true" auto-config="false" create-session="stateless" entry-point-ref="CustomAuthenticationEntryPoint">
    <security:custom-filter ref="authenticationTokenProcessingFilter" position="FORM_LOGIN_FILTER" />
    <security:intercept-url pattern="/authenticate" access="permitAll"/>
    <security:intercept-url pattern="/**" access="isAuthenticated()" />
</security:http>

<bean id="CustomAuthenticationEntryPoint"
    class="com.demo.api.support.spring.CustomAuthenticationEntryPoint" />

<bean id="authenticationTokenProcessingFilter"
    class="com.demo.api.support.spring.AuthenticationTokenProcessingFilter" >
    <constructor-arg ref="authenticationManager" />
</bean>

Come puoi vedere, abbiamo creato un'abitudine AuthenticationEntryPoint, che in sostanza restituisce solo 401 Unauthorizedse la richiesta non è stata autenticata nella catena di filtri dal nostro AuthenticationTokenProcessingFilter.

CustomAuthenticationEntryPoint :

public class CustomAuthenticationEntryPoint implements AuthenticationEntryPoint {
    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response,
            AuthenticationException authException) throws IOException, ServletException {
        response.sendError( HttpServletResponse.SC_UNAUTHORIZED, "Unauthorized: Authentication token was either missing or invalid." );
    }
}

AuthenticationTokenProcessingFilter :

public class AuthenticationTokenProcessingFilter extends GenericFilterBean {

    @Autowired UserService userService;
    @Autowired TokenUtils tokenUtils;
    AuthenticationManager authManager;

    public AuthenticationTokenProcessingFilter(AuthenticationManager authManager) {
        this.authManager = authManager;
    }

    @Override
    public void doFilter(ServletRequest request, ServletResponse response,
            FilterChain chain) throws IOException, ServletException {
        @SuppressWarnings("unchecked")
        Map<String, String[]> parms = request.getParameterMap();

        if(parms.containsKey("token")) {
            String token = parms.get("token")[0]; // grab the first "token" parameter

            // validate the token
            if (tokenUtils.validate(token)) {
                // determine the user based on the (already validated) token
                UserDetails userDetails = tokenUtils.getUserFromToken(token);
                // build an Authentication object with the user's info
                UsernamePasswordAuthenticationToken authentication = 
                        new UsernamePasswordAuthenticationToken(userDetails.getUsername(), userDetails.getPassword());
                authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails((HttpServletRequest) request));
                // set the authentication into the SecurityContext
                SecurityContextHolder.getContext().setAuthentication(authManager.authenticate(authentication));         
            }
        }
        // continue thru the filter chain
        chain.doFilter(request, response);
    }
}

Ovviamente, TokenUtilscontiene del codice privato (e molto specifico per il caso) e non può essere facilmente condiviso. Ecco la sua interfaccia:

public interface TokenUtils {
    String getToken(UserDetails userDetails);
    String getToken(UserDetails userDetails, Long expiration);
    boolean validate(String token);
    UserDetails getUserFromToken(String token);
}

Questo dovrebbe farti iniziare bene. Buona codifica. :)


È necessario autenticare il token quando il token viene inviato con la richiesta. Che ne dici di ottenere direttamente le informazioni sul nome utente e impostarle nel contesto / richiesta corrente?
Fisher,

1
@Prima Non li conservo da nessuna parte ... l'idea del token è che deve essere passata con ogni richiesta, e può essere decostruita (parzialmente) per determinarne la validità (da qui il validate(...)metodo). Questo è importante perché voglio che il server rimanga apolide. Immagino che potresti usare questo approccio senza dover usare Spring.
Chris Cashwell,

1
Se il client è un browser, come può essere memorizzato il token? o devi ripetere l'autenticazione per ogni richiesta?
principiante_12

2
ottimi consigli. @ChrisCashwell - la parte che non riesco a trovare è dove convalidi le credenziali dell'utente e rispedisci un token? Immagino che dovrebbe essere da qualche parte nell'impianto dell'endpoint / autenticare. ho ragione ? In caso contrario, qual è l'obiettivo di / autenticare?
Yonatan Maman

3
cosa c'è dentro AuthenticationManager?
MoienGK,

25

Potresti prendere in considerazione l' autenticazione con accesso digest . Essenzialmente il protocollo è il seguente:

  1. La richiesta viene effettuata dal cliente
  2. Il server risponde con una stringa nonce univoca
  3. Il client fornisce un nome utente e una password (e alcuni altri valori) con hash md5 con nonce; questo hash è noto come HA1
  4. Il server è quindi in grado di verificare l'identità del cliente e fornire i materiali richiesti
  5. La comunicazione con il nonce può continuare fino a quando il server non fornisce un nuovo nonce (viene utilizzato un contatore per eliminare gli attacchi di riproduzione)

Tutta questa comunicazione viene effettuata attraverso le intestazioni, che, come sottolinea jmort253, sono generalmente più sicure della comunicazione di materiale sensibile nei parametri url.

L'autenticazione con accesso digest è supportata da Spring Security . Nota che, sebbene i documenti affermino che devi avere accesso alla password in chiaro del tuo client, puoi autenticarti con successo se hai l'hash HA1 per il tuo client.


1
Sebbene questo sia un possibile approccio, i numerosi round trip che devono essere effettuati per recuperare un token lo rendono un po 'indesiderabile.
Chris Cashwell,

Se il client segue la specifica di autenticazione HTTP, questi round trip si verificano solo alla prima chiamata e quando 5. accade.
Markus Malkusch,

5

Per quanto riguarda i token che trasportano informazioni, i token Web JSON ( http://jwt.io ) sono una tecnologia geniale. Il concetto principale è quello di incorporare elementi di informazione (attestazioni) nel token e quindi firmare l'intero token in modo che l'estremità di convalida possa verificare che le affermazioni siano effettivamente attendibili.

Uso questa implementazione Java: https://bitbucket.org/b_c/jose4j/wiki/Home

C'è anche un modulo Spring (spring-security-jwt), ma non ho esaminato ciò che supporta.


Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.