webentwicklung-frage-antwort-db.com.de

Sichern der REST API mit benutzerdefinierten Token (ohne Status, keine Benutzeroberfläche, keine Cookies, keine grundlegende Authentifizierung, kein OAuth, keine Anmeldeseite)

Es gibt viele Richtlinien und Beispielcodes, die zeigen, wie Sie REST API mit Spring Security sichern, aber die meisten davon setzen einen Webclient voraus und sprechen über Anmeldeseite, Umleitung, Verwendung von Cookies usw. Mai Selbst ein einfacher Filter, der nach dem benutzerdefinierten Token im HTTP-Header sucht, könnte ausreichen. Wie implementiere ich die Sicherheit für die unten genannten Anforderungen? Gibt es ein Gist/Github-Projekt, das dasselbe tut? Mein Wissen über die Sicherheit im Frühjahr ist begrenzt Lassen Sie es mich bitte wissen, um dies einfacher mit Federsicherheit umzusetzen.

  • Die REST-API wird vom zustandslosen Backend über HTTPS bereitgestellt
  • der Client kann eine Web-App, eine mobile App, eine App im SPA-Stil oder APIs von Drittanbietern sein
  • keine Basisauthentifizierung, keine Cookies, keine Benutzeroberfläche (keine JSP/HTML/statischen Ressourcen), keine Weiterleitungen, kein OAuth Anbieter.
  • benutzerdefiniertes Token, das in HTTPS-Headern festgelegt ist
  • Die Token-Validierung erfolgt gegen externen Speicher (wie MemCached/Redis/oder sogar ein beliebiges RDBMS)
  • Alle APIs mit Ausnahme ausgewählter Pfade (wie/login,/signup,/public usw.) müssen authentifiziert werden.

Ich benutze Springboot, Spring Security, etc .. bevorzuge eine Lösung mit Java config (kein XML)

37

Meine Beispielanwendung erledigt genau dies - das Sichern von REST - Endpunkten mithilfe von Spring Security in einem zustandslosen Szenario. Einzelne REST Aufrufe werden mithilfe eines HTTP-Headers authentifiziert. Authentifizierungsinformationen werden serverseitig in einem speicherinternen Cache gespeichert und bieten dieselbe Semantik wie die HTTP-Sitzung in einer typischen Webanwendung. Die App nutzt die gesamte Spring Security-Infrastruktur mit einem Minimum an benutzerdefiniertem Code. Keine nackten Filter, kein Code außerhalb der Spring Security-Infrastruktur.

Die Grundidee besteht darin, die folgenden vier Spring Security-Komponenten zu implementieren:

  1. org.springframework.security.web.AuthenticationEntryPoint, Um REST Anrufe abzufangen, für die eine Authentifizierung erforderlich ist, für die jedoch das erforderliche Authentifizierungstoken fehlt, und damit die Anforderungen abzulehnen.
  2. org.springframework.security.core.Authentication, Um die für die API REST erforderlichen Authentifizierungsinformationen zu speichern.
  3. org.springframework.security.authentication.AuthenticationProvider, Um die eigentliche Authentifizierung durchzuführen (für eine Datenbank, einen LDAP-Server, einen Webdienst usw.).
  4. org.springframework.security.web.context.SecurityContextRepository, Um das Authentifizierungstoken zwischen HTTP-Anforderungen zu speichern. Im Beispiel speichert die Implementierung das Token in einer EHCACHE-Instanz.

Das Beispiel verwendet eine XML-Konfiguration, Sie können jedoch problemlos die entsprechende Java - Konfiguration erstellen.

33
manish

Du hast recht, es ist nicht einfach und es gibt nicht viele gute Beispiele da draußen. Die Beispiele, die ich gesehen habe, haben es so gemacht, dass Sie keine anderen Sicherheitsartikel nebeneinander verwenden können. Ich habe in letzter Zeit etwas Ähnliches gemacht, hier ist, was ich getan habe.

Sie benötigen ein benutzerdefiniertes Token, um Ihren Header-Wert zu speichern

public class CustomToken extends AbstractAuthenticationToken {
  private final String value;

  //Getters and Constructor.  Make sure getAutheticated returns false at first.
  //I made mine "immutable" via:

      @Override
public void setAuthenticated(boolean isAuthenticated) {
    //It doesn't make sense to let just anyone set this token to authenticated, so we block it
    //Similar precautions are taken in other spring framework tokens, EG: UsernamePasswordAuthenticationToken
    if (isAuthenticated) {

        throw new IllegalArgumentException(MESSAGE_CANNOT_SET_AUTHENTICATED);
    }

    super.setAuthenticated(false);
}
}

Sie benötigen einen Federsicherheitsfilter, um den Header zu extrahieren und den Manager zu bitten, ihn zu authentifizieren. Dies ist etwa Hervorgehobener Text

public class CustomFilter extends AbstractAuthenticationProcessingFilter {


    public CustomFilter(RequestMatcher requestMatcher) {
        super(requestMatcher);

        this.setAuthenticationSuccessHandler((request, response, authentication) -> {
        /*
         * On success the desired action is to chain through the remaining filters.
         * Chaining is not possible through the success handlers, because the chain is not accessible in this method.
         * As such, this success handler implementation does nothing, and chaining is accomplished by overriding the successfulAuthentication method as per:
         * http://docs.spring.io/autorepo/docs/spring-security/3.2.4.RELEASE/apidocs/org/springframework/security/web/authentication/AbstractAuthenticationProcessingFilter.html#successfulAuthentication(javax.servlet.http.HttpServletRequest,%20javax.servlet.http.HttpServletResponse,%20javax.servlet.FilterChain,%20org.springframework.security.core.Authentication)
         * "Subclasses can override this method to continue the FilterChain after successful authentication."
         */
        });

    }



    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
            throws AuthenticationException, IOException, ServletException {


        String tokenValue = request.getHeader("SOMEHEADER");

        if(StringUtils.isEmpty(tokenValue)) {
            //Doing this check is kinda dumb because we check for it up above in doFilter
            //..but this is a public method and we can't do much if we don't have the header
            //also we can't do the check only here because we don't have the chain available
           return null;
        }


        CustomToken token = new CustomToken(tokenValue);
        token.setDetails(authenticationDetailsSource.buildDetails(request));

        return this.getAuthenticationManager().authenticate(token);
    }



    /*
     * Overriding this method to maintain the chaining on authentication success.
     * http://docs.spring.io/autorepo/docs/spring-security/3.2.4.RELEASE/apidocs/org/springframework/security/web/authentication/AbstractAuthenticationProcessingFilter.html#successfulAuthentication(javax.servlet.http.HttpServletRequest,%20javax.servlet.http.HttpServletResponse,%20javax.servlet.FilterChain,%20org.springframework.security.core.Authentication)
     * "Subclasses can override this method to continue the FilterChain after successful authentication."
     */
    @Override
    protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException {


        //if this isn't called, then no auth is set in the security context holder
        //and subsequent security filters can still execute.  
        //so in SOME cases you might want to conditionally call this
        super.successfulAuthentication(request, response, chain, authResult);

        //Continue the chain
        chain.doFilter(request, response);

    }


}

Registrieren Sie Ihren benutzerdefinierten Filter in der Frühjahrssicherheitskette

 @Configuration
 public static class ResourceEndpointsSecurityConfig extends WebSecurityConfigurerAdapter {        

      //Note, we don't register this as a bean as we don't want it to be added to the main Filter chain, just the spring security filter chain
      protected AbstractAuthenticationProcessingFilter createCustomFilter() throws Exception {
        CustomFilter filter = new CustomFilter( new RegexRequestMatcher("^/.*", null));
        filter.setAuthenticationManager(this.authenticationManagerBean());
        return filter;
      }

       @Override
       protected void configure(HttpSecurity http) throws Exception {                  

            http
            //fyi: This adds it to the spring security proxy filter chain
            .addFilterBefore(createCustomFilter(), AnonymousAuthenticationFilter.class)
       }
}

Ein benutzerdefinierter Authentifizierungsanbieter zum Überprüfen des mit dem Filter extrahierten Tokens.

public class CustomAuthenticationProvider implements AuthenticationProvider {


    @Override
    public Authentication authenticate(Authentication auth)
            throws AuthenticationException {

        CustomToken token = (CustomToken)auth;

        try{
           //Authenticate token against redis or whatever you want

            //This i found weird, you need a Principal in your Token...I use User
            //I found this to be very redundant in spring security, but Controller param resolving will break if you don't do this...anoying
            org.springframework.security.core.userdetails.User principal = new User(...); 

            //Our token resolved to a username so i went with this token...you could make your CustomToken take the principal.  getCredentials returns "NO_PASSWORD"..it gets cleared out anyways.  also the getAuthenticated for the thing you return should return true now
            return new UsernamePasswordAuthenticationToken(principal, auth.getCredentials(), principal.getAuthorities());
        } catch(Expection e){
            //TODO throw appropriate AuthenticationException types
            throw new BadCredentialsException(MESSAGE_AUTHENTICATION_FAILURE, e);
        }


    }

    @Override
    public boolean supports(Class<?> authentication) {
        return CustomToken.class.isAssignableFrom(authentication);
    }


}

Registrieren Sie schließlich Ihren Provider als Bean, damit der Authentifizierungsmanager ihn in einer @ Configuration-Klasse findet. Sie könnten es wahrscheinlich auch nur @Component, ich bevorzuge diese Methode

@Bean
public AuthenticationProvider createCustomAuthenticationProvider(injectedDependencies)  {
    return new CustomAuthenticationProvider(injectedDependencies);
}
9
Chris DaMour

Der Code sichert alle Endpunkte - aber ich bin mir sicher, dass Sie damit spielen können :). Das Token wird mit Spring Boot Starter Security in Redis gespeichert und Sie müssen unser eigenes UserDetailsService definieren, das Sie an AuthenticationManagerBuilder übergeben.

Lange Rede, kurzer Sinn - Kopieren, Einfügen von EmbeddedRedisConfiguration und SecurityConfig und Ersetzen von AuthenticationManagerBuilder in Ihre Logik.

HTTP:

Anforderndes Token - Senden grundlegender HTTP-Authentifizierungsinhalte in einem Anforderungsheader. Ein Token wird in einem Antwortheader zurückgegeben.

http --print=hH -a user:password localhost:8080/v1/users

GET /v1/users HTTP/1.1
Accept: */*
Accept-Encoding: gzip, deflate
Authorization: Basic dXNlcjpwYXNzd29yZA==
Connection: keep-alive
Host: localhost:8080
User-Agent: HTTPie/0.9.3

HTTP/1.1 200 OK
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Content-Length: 4
Content-Type: text/plain;charset=UTF-8
Date: Fri, 06 May 2016 09:44:23 GMT
Expires: 0
Pragma: no-cache
Server: Apache-Coyote/1.1
X-Application-Context: application
X-Content-Type-Options: nosniff
X-Frame-Options: DENY
X-XSS-Protection: 1; mode=block
x-auth-token: cacf4a97-75fe-464d-b499-fcfacb31c8af

Gleiche Anfrage, aber mit Token:

http --print=hH localhost:8080/v1/users 'x-auth-token: cacf4a97-75fe-464d-b499-fcfacb31c8af'

GET /v1/users HTTP/1.1
Accept: */*
Accept-Encoding: gzip, deflate
Connection: keep-alive
Host: localhost:8080
User-Agent: HTTPie/0.9.3
x-auth-token:  cacf4a97-75fe-464d-b499-fcfacb31c8af

HTTP/1.1 200 OK
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Content-Length: 4
Content-Type: text/plain;charset=UTF-8
Date: Fri, 06 May 2016 09:44:58 GMT
Expires: 0
Pragma: no-cache
Server: Apache-Coyote/1.1
X-Application-Context: application
X-Content-Type-Options: nosniff
X-Frame-Options: DENY
X-XSS-Protection: 1; mode=block

Wenn Sie einen falschen Benutzernamen/ein falsches Passwort oder einen falschen Token übergeben, erhalten Sie 401.

Java

Ich habe diese Abhängigkeiten zu build.gradle Hinzugefügt

compile("org.springframework.session:spring-session-data-redis:1.0.1.RELEASE")
compile("org.springframework.boot:spring-boot-starter-security")
compile("org.springframework.boot:spring-boot-starter-web")
compile("com.github.kstyrc:embedded-redis:0.6")

Dann Redis Konfiguration

@Configuration
@EnableRedisHttpSession
public class EmbeddedRedisConfiguration {

    private static RedisServer redisServer;

    @Bean
    public JedisConnectionFactory connectionFactory() throws IOException {
        redisServer = new RedisServer(Protocol.DEFAULT_PORT);
        redisServer.start();
        return new JedisConnectionFactory();
    }

    @PreDestroy
    public void destroy() {
        redisServer.stop();
    }

}

Sicherheitskonfiguration:

@Configuration
@EnableWebSecurity
@Order(SecurityProperties.ACCESS_OVERRIDE_ORDER)
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    UserService userService;

    @Override
    protected void configure(AuthenticationManagerBuilder builder) throws Exception {
        builder.userDetailsService(userService);
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .authorizeRequests()
                .anyRequest().authenticated()
                .and()
                .requestCache()
                .requestCache(new NullRequestCache())
                .and()
                .httpBasic();
    }

    @Bean
    public HttpSessionStrategy httpSessionStrategy() {
        return new HeaderHttpSessionStrategy();
    }
}

Normalerweise finden Sie in Tutorials AuthenticationManagerBuilder mit inMemoryAuthentication, aber es gibt viel mehr Auswahlmöglichkeiten (LDAP, ...). Schauen Sie sich einfach die Klassendefinition an. Ich verwende userDetailsService, wofür das Objekt UserDetailsService erforderlich ist.

Und schließlich mein Benutzerdienst mit CrudRepository.

@Service
public class UserService implements UserDetailsService {

    @Autowired
    UserRepository userRepository;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        UserAccount userAccount = userRepository.findByEmail(username);
        if (userAccount == null) {
            return null;
        }
        return new User(username, userAccount.getPassword(), AuthorityUtils.commaSeparatedStringToAuthorityList("ROLE_USER"));
    }
}
4
radeklos

Ein weiteres Beispielprojekt, das JWT verwendet - Jhipster

Versuchen Sie, eine Microservice-Anwendung mit JHipster zu generieren. Es generiert eine Vorlage mit sofortiger Integration zwischen Spring Security und JWT.

https://jhipster.github.io/security/

0
Dhananjay