2012-05-28 18 views

risposta

12

Al momento Dropwizard non supporta l'autenticazione HMAC immediatamente, pertanto è necessario scrivere il proprio autenticatore. Una scelta tipica per l'autenticazione HMAC consiste nell'utilizzare l'intestazione di autorizzazione HTTP. Il codice seguente prevede questa intestazione nel seguente formato:

Authorization: <algorithm> <apiKey> <digest> 

Un esempio sarebbe

Authorization: HmacSHA1 abcd-efgh-1234 sdafkljlkansdaflk2354jlkj5345345dflkmsdf 

Il digest è costruito dal contenuto del corpo (entità marshalling) prima della codifica URL con l'HMAC segreto condiviso aggiunto come base64. Per una richiesta non relativa al corpo, come GET o HEAD, il contenuto viene considerato come il percorso e i parametri URI completi con la chiave segreta aggiunta.

Per attuare questo in un modo che Dropwizard può lavorare con essa richiede di copiare il codice BasicAuthenticator presente nel modulo dropwizard-auth nel proprio codice e modificarlo con qualcosa di simile:

import com.google.common.base.Optional; 
import com.sun.jersey.api.core.HttpContext; 
import com.sun.jersey.server.impl.inject.AbstractHttpContextInjectable; 
import com.yammer.dropwizard.auth.AuthenticationException; 
import com.yammer.dropwizard.auth.Authenticator; 

import javax.ws.rs.WebApplicationException; 
import javax.ws.rs.core.HttpHeaders; 
import javax.ws.rs.core.MediaType; 
import javax.ws.rs.core.Response; 

class HmacAuthInjectable<T> extends AbstractHttpContextInjectable<T> { 
    private static final String PREFIX = "HmacSHA1"; 
    private static final String HEADER_VALUE = PREFIX + " realm=\"%s\""; 

    private final Authenticator<HmacCredentials, T> authenticator; 
    private final String realm; 
    private final boolean required; 

    HmacAuthInjectable(Authenticator<HmacCredentials, T> authenticator, String realm, boolean required) { 
    this.authenticator = authenticator; 
    this.realm = realm; 
    this.required = required; 
    } 

    public Authenticator<HmacCredentials, T> getAuthenticator() { 
    return authenticator; 
    } 

    public String getRealm() { 
    return realm; 
    } 

    public boolean isRequired() { 
    return required; 
    } 

    @Override 
    public T getValue(HttpContext c) { 

    try { 
     final String header = c.getRequest().getHeaderValue(HttpHeaders.AUTHORIZATION); 
     if (header != null) { 

     final String[] authTokens = header.split(" "); 

     if (authTokens.length != 3) { 
      // Malformed 
      HmacAuthProvider.LOG.debug("Error decoding credentials (length is {})", authTokens.length); 
      throw new WebApplicationException(Response.Status.BAD_REQUEST); 
     } 

     final String algorithm = authTokens[0]; 
     final String apiKey = authTokens[1]; 
     final String signature = authTokens[2]; 
     final String contents; 

     // Determine which part of the request will be used for the content 
     final String method = c.getRequest().getMethod().toUpperCase(); 
     if ("GET".equals(method) || 
      "HEAD".equals(method) || 
      "DELETE".equals(method)) { 
      // No entity so use the URI 
      contents = c.getRequest().getRequestUri().toString(); 
     } else { 
      // Potentially have an entity (even in OPTIONS) so use that 
      contents = c.getRequest().getEntity(String.class); 
     } 

     final HmacCredentials credentials = new HmacCredentials(algorithm, apiKey, signature, contents); 

     final Optional<T> result = authenticator.authenticate(credentials); 
     if (result.isPresent()) { 
      return result.get(); 
     } 
     } 
    } catch (IllegalArgumentException e) { 
     HmacAuthProvider.LOG.debug(e, "Error decoding credentials"); 
    } catch (AuthenticationException e) { 
     HmacAuthProvider.LOG.warn(e, "Error authenticating credentials"); 
     throw new WebApplicationException(Response.Status.INTERNAL_SERVER_ERROR); 
    } 

    if (required) { 
     throw new WebApplicationException(Response.status(Response.Status.UNAUTHORIZED) 
     .header(HttpHeaders.AUTHORIZATION, 
      String.format(HEADER_VALUE, realm)) 
     .entity("Credentials are required to access this resource.") 
     .type(MediaType.TEXT_PLAIN_TYPE) 
     .build()); 
    } 
    return null; 
    } 
} 

Quanto sopra è non perfetto, ma ti farà iniziare. Si consiglia di fare riferimento allo MultiBit Merchant release candidate source code (licenza MIT) per una versione più aggiornata e le varie classi di supporto.

Il passaggio successivo consiste nell'integrare il processo di autenticazione nella sottoclasse ResourceTest. Purtroppo, Dropwizard non fornisce un buon punto di ingresso per i provider di autenticazione a v0.4.0, quindi si consiglia di introdurre la propria classe di base, simile a questo:

import com.google.common.collect.Lists; 
import com.google.common.collect.Sets; 
import com.sun.jersey.api.client.Client; 
import com.sun.jersey.test.framework.AppDescriptor; 
import com.sun.jersey.test.framework.JerseyTest; 
import com.sun.jersey.test.framework.LowLevelAppDescriptor; 
import com.xeiam.xchange.utils.CryptoUtils; 
import com.yammer.dropwizard.bundles.JavaBundle; 
import com.yammer.dropwizard.jersey.DropwizardResourceConfig; 
import com.yammer.dropwizard.jersey.JacksonMessageBodyProvider; 
import com.yammer.dropwizard.json.Json; 
import org.codehaus.jackson.map.Module; 
import org.junit.After; 
import org.junit.Before; 
import org.multibit.mbm.auth.hmac.HmacAuthProvider; 
import org.multibit.mbm.auth.hmac.HmacAuthenticator; 
import org.multibit.mbm.persistence.dao.UserDao; 
import org.multibit.mbm.persistence.dto.User; 
import org.multibit.mbm.persistence.dto.UserBuilder; 

import java.io.UnsupportedEncodingException; 
import java.security.GeneralSecurityException; 
import java.util.List; 
import java.util.Set; 

import static org.mockito.Mockito.mock; 
import static org.mockito.Mockito.when; 

/** 
* A base test class for testing Dropwizard resources. 
*/ 
public abstract class BaseResourceTest { 
    private final Set<Object> singletons = Sets.newHashSet(); 
    private final Set<Object> providers = Sets.newHashSet(); 
    private final List<Module> modules = Lists.newArrayList(); 

    private JerseyTest test; 

    protected abstract void setUpResources() throws Exception; 

    protected void addResource(Object resource) { 
    singletons.add(resource); 
    } 

    public void addProvider(Object provider) { 
    providers.add(provider); 
    } 

    protected void addJacksonModule(Module module) { 
    modules.add(module); 
    } 

    protected Json getJson() { 
    return new Json(); 
    } 

    protected Client client() { 
    return test.client(); 
    } 

    @Before 
    public void setUpJersey() throws Exception { 
    setUpResources(); 
    this.test = new JerseyTest() { 
     @Override 
     protected AppDescriptor configure() { 
     final DropwizardResourceConfig config = new DropwizardResourceConfig(); 
     for (Object provider : JavaBundle.DEFAULT_PROVIDERS) { // sorry, Scala folks 
      config.getSingletons().add(provider); 
     } 
     for (Object provider : providers) { 
      config.getSingletons().add(provider); 
     } 
     Json json = getJson(); 
     for (Module module : modules) { 
      json.registerModule(module); 
     } 
     config.getSingletons().add(new JacksonMessageBodyProvider(json)); 
     config.getSingletons().addAll(singletons); 
     return new LowLevelAppDescriptor.Builder(config).build(); 
     } 
    }; 
    test.setUp(); 
    } 

    @After 
    public void tearDownJersey() throws Exception { 
    if (test != null) { 
     test.tearDown(); 
    } 
    } 

    /** 
* @param contents The content to sign with the default HMAC process (POST body, GET resource path) 
* @return 
*/ 
    protected String buildHmacAuthorization(String contents, String apiKey, String secretKey) throws UnsupportedEncodingException, GeneralSecurityException { 
    return String.format("HmacSHA1 %s %s",apiKey, CryptoUtils.computeSignature("HmacSHA1", contents, secretKey)); 
    } 

    protected void setUpAuthenticator() { 
    User user = UserBuilder 
     .getInstance() 
     .setUUID("abc123") 
     .setSecretKey("def456") 
     .build(); 

    // 
    UserDao userDao = mock(UserDao.class); 
    when(userDao.getUserByUUID("abc123")).thenReturn(user); 

    HmacAuthenticator authenticator = new HmacAuthenticator(); 
    authenticator.setUserDao(userDao); 

    addProvider(new HmacAuthProvider<User>(authenticator, "REST")); 
    } 
} 

Anche in questo caso, il codice di cui sopra non è perfetto, ma l'idea è di permettere a un utente teso di derubare di fornire a un utente standard una chiave segreta condivisa conosciuta. Dovresti introdurre la tua implementazione UserBuilder a scopo di test.

Infine, con il codice di sopra di una risorsa Dropwizard che ha avuto un endpoint come questo:

import com.google.common.base.Optional; 
import com.yammer.dropwizard.auth.Auth; 
import com.yammer.metrics.annotation.Timed; 
import org.multibit.mbm.core.Saying; 
import org.multibit.mbm.persistence.dto.User; 

import javax.ws.rs.GET; 
import javax.ws.rs.Path; 
import javax.ws.rs.Produces; 
import javax.ws.rs.QueryParam; 
import javax.ws.rs.core.MediaType; 
import java.util.concurrent.atomic.AtomicLong; 

@Path("/") 
@Produces(MediaType.APPLICATION_JSON) 
public class HelloWorldResource { 
    private final String template; 
    private final String defaultName; 
    private final AtomicLong counter; 

    public HelloWorldResource(String template, String defaultName) { 
    this.template = template; 
    this.defaultName = defaultName; 
    this.counter = new AtomicLong(); 
    } 

    @GET 
    @Timed 
    @Path("/hello-world") 
    public Saying sayHello(@QueryParam("name") Optional<String> name) { 
    return new Saying(counter.incrementAndGet(), 
     String.format(template, name.or(defaultName))); 
    } 

    @GET 
    @Timed 
    @Path("/secret") 
    public Saying saySecuredHello(@Auth User user) { 
    return new Saying(counter.incrementAndGet(), 
     "You cracked the code!"); 
    } 

} 

potrebbe essere testato con una prova di unità che è stato configurato in questo modo:

import org.junit.Test; 
import org.multibit.mbm.core.Saying; 
import org.multibit.mbm.test.BaseResourceTest; 

import javax.ws.rs.core.HttpHeaders; 

import static org.junit.Assert.assertEquals; 

public class HelloWorldResourceTest extends BaseResourceTest { 


    @Override 
    protected void setUpResources() { 
    addResource(new HelloWorldResource("Hello, %s!","Stranger")); 

    setUpAuthenticator(); 
    } 

    @Test 
    public void simpleResourceTest() throws Exception { 

    Saying expectedSaying = new Saying(1,"Hello, Stranger!"); 

    Saying actualSaying = client() 
     .resource("/hello-world") 
     .get(Saying.class); 

    assertEquals("GET hello-world returns a default",expectedSaying.getContent(),actualSaying.getContent()); 

    } 


    @Test 
    public void hmacResourceTest() throws Exception { 

    String authorization = buildHmacAuthorization("/secret", "abc123", "def456"); 

    Saying actual = client() 
     .resource("/secret") 
     .header(HttpHeaders.AUTHORIZATION, authorization) 
     .get(Saying.class); 

    assertEquals("GET secret returns unauthorized","You cracked the code!", actual.getContent()); 

    } 


} 

Spero che questo aiuti hai iniziato

+0

Inoltre, si consiglia di avere una lettura di questo: http://rubydoc.info/gems/warden-hmac-authentication/0.6.1/file/README.md. Fornisce una definizione di come le parti della richiesta devono essere disposte prima della firma. –

+0

E, dopo ulteriori ricerche, probabilmente vorrai utilizzare la funzione ClientFilter di Jersey per apportare modifiche all'ultimo momento alla richiesta finale (ad esempio aggiungendo nell'intestazione Autorizzazione personalizzata). Anche in questo caso, il progetto [MultiBit Merchant] (https://github.com/gary-rowe/MultiBitMerchant) dimostra questo codice. –

Problemi correlati