2012-05-28 14 views

Odpowiedz

12

Obecnie Dropwizard nie obsługuje uwierzytelniania HMAC od razu po wyjęciu z pudełka, więc musisz napisać swój własny uwierzytelniający. Typowym wyborem do uwierzytelniania HMAC jest użycie nagłówka autoryzacji HTTP. Poniższy kod oczekuje ten nagłówek w następującym formacie:

Authorization: <algorithm> <apiKey> <digest> 

Przykładem może być

Authorization: HmacSHA1 abcd-efgh-1234 sdafkljlkansdaflk2354jlkj5345345dflkmsdf 

Trawienie jest zbudowany z treścią ciała (marshalled podmiot) przed kodowaniem URL z HMAC wspólny sekret dołączony jako base64. W przypadku żądania innego niż ciało, takiego jak GET lub HEAD, treść jest traktowana jako kompletna ścieżka URI i parametry z dołączonym tajnym kluczem.

zaimplementować to w sposób, który Dropwizard może pracować z nim wymaga, aby skopiować kod BasicAuthenticator obecny w module dropwizard-auth do własnego kodu i zmodyfikować go tak:

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

powyższe jest nie jest idealny, ale zaczniesz. Możesz zapoznać się z MultiBit Merchant release candidate source code (licencją MIT), aby uzyskać bardziej aktualną wersję i różne klasy pomocnicze.

Następnym krokiem jest zintegrowanie procesu uwierzytelniania z podklasą ResourceTest. Niestety, Dropwizard nie stanowią dobry punkt wejścia dla dostawców uwierzytelniania w v0.4.0, więc może warto wprowadzić swoją klasę bazową, podobny do tego:

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")); 
    } 
} 

Ponownie, powyższy kod nie jest doskonały, ale chodzi o to, aby umożliwić sfałszowanym UserDao dostarczenie standardowego użytkownika ze znanym wspólnym tajnym kluczem. W celu testowania musisz wprowadzić własną implementację UserBuilder.

Wreszcie z wyżej kodu można Dropwizard Resource miał końcowy takiego:

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!"); 
    } 

} 

mogą być badane w teście jednostkowej, która została skonfigurowana tak:

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()); 

    } 


} 

nadzieję, że to pomaga zaczynasz.

+0

Możesz również przeczytać: http://rubydoc.info/gems/warden-hmac-authentication/0.6.1/file/README.md. Zapewnia definicję tego, w jaki sposób części wniosku należy załączyć przed podpisaniem. –

+0

Po dalszych badaniach prawdopodobnie będziesz chciał skorzystać z obiektu ClientFilter na Jersey, aby dokonać ostatniej chwili modyfikacji ostatniego żądania (np. Dodanie własnego niestandardowego nagłówka autoryzacji). Ponownie, projekt [MultiBit Merchant] (https://github.com/gary-rowe/MultiBitMerchant) demonstruje ten kod. –

Powiązane problemy