RESTful Authentifizierung über Spring

262

Problem:
Wir haben eine Spring MVC-basierte RESTful-API, die vertrauliche Informationen enthält. Die API sollte gesichert sein, es ist jedoch nicht wünschenswert, die Anmeldeinformationen des Benutzers (Benutzer / Pass-Kombination) bei jeder Anforderung zu senden. Gemäß den REST-Richtlinien (und den internen Geschäftsanforderungen) muss der Server zustandslos bleiben. Die API wird von einem anderen Server in einem Mashup-Stil verwendet.

Bedarf:

  • Der Client sendet eine Anfrage an .../authenticate(ungeschützte URL) mit Anmeldeinformationen. Der Server gibt ein sicheres Token zurück, das genügend Informationen enthält, damit der Server zukünftige Anforderungen überprüfen und zustandslos bleiben kann. Dies würde wahrscheinlich aus denselben Informationen bestehen wie das Remember-Me-Token von Spring Security .

  • Der Client stellt nachfolgende Anforderungen an verschiedene (geschützte) URLs und hängt das zuvor erhaltene Token als Abfrageparameter (oder weniger wünschenswert als HTTP-Anforderungsheader) an.

  • Vom Kunden kann nicht erwartet werden, dass er Cookies speichert.

  • Da wir Spring bereits verwenden, sollte die Lösung Spring Security verwenden.

Wir haben unsere Köpfe gegen die Wand geschlagen, um dies zum Laufen zu bringen. Hoffentlich hat jemand da draußen dieses Problem bereits gelöst.

Wie könnten Sie angesichts des obigen Szenarios diesen besonderen Bedarf lösen?

Chris Cashwell
quelle
49
Hallo Chris, ich bin mir nicht sicher, ob es die beste Idee ist, dieses Token im Abfrageparameter zu übergeben. Dies wird unabhängig von HTTPS oder HTTP in Protokollen angezeigt. Die Header sind wahrscheinlich sicherer. Nur zur Info. Gute Frage. +1
jmort253
1
Was verstehen Sie von Staatenlosen? Ihre Token-Anforderung kollidiert mit meinem Verständnis von Staatenlosigkeit. Die Antwort auf die HTTP-Authentifizierung scheint mir die einzige zustandslose Implementierung zu sein.
Markus Malkusch
9
@MarkusMalkusch zustandslos bezieht sich auf das Wissen des Servers über die vorherige Kommunikation mit einem bestimmten Client. HTTP ist per Definition zustandslos, und Sitzungscookies machen es statusbehaftet. Die Lebensdauer (und die Quelle) des Tokens sind irrelevant. Der Server kümmert sich nur darum, dass es gültig ist und an einen Benutzer gebunden werden kann (NICHT an eine Sitzung). Das Übergeben eines identifizierenden Tokens beeinträchtigt daher nicht die Statefulness.
Chris Cashwell
1
@ChrisCashwell Wie stellen Sie sicher, dass das Token nicht vom Client gefälscht / generiert wird? Verwenden Sie einen privaten Schlüssel auf der Serverseite, um das Token zu verschlüsseln, dem Client bereitzustellen und ihn dann bei zukünftigen Anforderungen mit demselben Schlüssel zu entschlüsseln? Offensichtlich würde Base64 oder eine andere Verschleierung nicht ausreichen. Können Sie Techniken zur "Validierung" dieser Token erläutern?
Craig Otis
6
Obwohl dies veraltet ist und ich den Code seit über 2 Jahren nicht mehr berührt oder aktualisiert habe, habe ich eine Übersicht erstellt, um diese Konzepte weiter zu erweitern. gist.github.com/ccashwell/dfc05dd8bd1a75d189d1
Chris Cashwell

Antworten:

190

Wir haben es geschafft, dass dies genau so funktioniert, wie es im OP beschrieben ist, und hoffentlich kann jemand anderes die Lösung nutzen. Folgendes haben wir getan:

Richten Sie den Sicherheitskontext folgendermaßen ein:

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

Wie Sie sehen können, haben wir eine benutzerdefinierte Datei erstellt AuthenticationEntryPoint, die im Grunde nur eine zurückgibt, 401 Unauthorizedwenn die Anforderung von uns nicht in der Filterkette authentifiziert wurde 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);
    }
}

TokenUtilsEnthält offensichtlich einen geheimen (und sehr fallspezifischen) Code und kann nicht ohne weiteres geteilt werden. Hier ist seine Schnittstelle:

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

Das sollte Sie zu einem guten Start bringen. Viel Spaß beim Codieren. :) :)

Chris Cashwell
quelle
Ist es notwendig, das Token zu authentifizieren, wenn das Token mit der Anforderung sendet? Wie wäre es, wenn Sie die Benutzernameninformationen direkt abrufen und im aktuellen Kontext / in der aktuellen Anfrage einstellen würden?
Fisher
1
@Spring Ich speichere sie nirgendwo ... Die ganze Idee des Tokens ist, dass es bei jeder Anfrage übergeben werden muss und (teilweise) dekonstruiert werden kann, um seine Gültigkeit zu bestimmen (daher die validate(...)Methode). Dies ist wichtig, da der Server zustandslos bleiben soll. Ich würde mir vorstellen, dass Sie diesen Ansatz verwenden könnten, ohne Spring verwenden zu müssen.
Chris Cashwell
1
Wie kann das Token gespeichert werden, wenn der Client ein Browser ist? oder müssen Sie die Authentifizierung für jede Anforderung wiederholen?
Anfänger_
2
tolle tipps. @ChrisCashwell - Der Teil, den ich nicht finden kann, ist, wo Sie die Benutzeranmeldeinformationen überprüfen und ein Token zurücksenden. Ich würde vermuten, dass es irgendwo in der Implikation des / authenticate-Endpunkts sein sollte. habe ich recht ? Wenn nicht, was ist das Ziel von / authentifizieren?
Yonatan Maman
3
Was ist im AuthenticationManager enthalten?
MoienGK
25

Sie könnten die Digest Access-Authentifizierung in Betracht ziehen . Im Wesentlichen lautet das Protokoll wie folgt:

  1. Die Anfrage wird vom Kunden gestellt
  2. Der Server antwortet mit einer eindeutigen Nonce-Zeichenfolge
  3. Der Client gibt einen Benutzernamen und ein Passwort (und einige andere Werte) ein, die md5 mit dem Nonce gehasht hat. Dieser Hash ist als HA1 bekannt
  4. Der Server kann dann die Identität des Kunden überprüfen und die angeforderten Materialien bereitstellen
  5. Die Kommunikation mit dem Nonce kann fortgesetzt werden, bis der Server ein neues Nonce bereitstellt (ein Zähler wird verwendet, um Wiederholungsangriffe zu eliminieren).

Die gesamte Kommunikation erfolgt über Header, was, wie jmort253 hervorhebt, im Allgemeinen sicherer ist als die Kommunikation von sensiblem Material in den URL-Parametern.

Die Digest Access-Authentifizierung wird von Spring Security unterstützt . Beachten Sie, dass Sie sich erfolgreich authentifizieren können, wenn Sie den HA1-Hash für Ihren Client haben , obwohl in den Dokumenten angegeben ist, dass Sie Zugriff auf das Klartextkennwort Ihres Clients haben müssen.

Tim Pote
quelle
1
Dies ist zwar ein möglicher Ansatz, aber die verschiedenen Roundtrips, die zum Abrufen eines Tokens durchgeführt werden müssen, machen ihn etwas unerwünscht.
Chris Cashwell
Wenn Ihr Client die HTTP-Authentifizierungsspezifikation befolgt, werden diese Roundtrips nur beim ersten Anruf und wenn 5. durchgeführt.
Markus Malkusch
5

In Bezug auf Token, die Informationen enthalten, sind JSON-Web-Token ( http://jwt.io ) eine brillante Technologie. Das Hauptkonzept besteht darin, Informationselemente (Ansprüche) in das Token einzubetten und dann das gesamte Token zu signieren, damit das Validierungsende überprüfen kann, ob die Ansprüche tatsächlich vertrauenswürdig sind.

Ich verwende diese Java-Implementierung: https://bitbucket.org/b_c/jose4j/wiki/Home

Es gibt auch ein Spring-Modul (spring-security-jwt), aber ich habe nicht untersucht, was es unterstützt.

Leif John
quelle