如何使用Dropwizard测试HMAC身份验证?

时间:2012-05-28 10:23:25

标签: dropwizard

我刚开始使用Dropwizard 0.4.0,我想要一些HMAC身份验证方面的帮助。有人有什么建议吗?

提前谢谢。

1 个答案:

答案 0 :(得分:12)

目前,Dropwizard不支持开箱即用的HMAC身份验证,因此您必须编写自己的身份验证器。 HMAC身份验证的典型选择是使用HTTP Authorization标头。以下代码期望此标头采用以下格式:

Authorization: <algorithm> <apiKey> <digest>

一个例子是

Authorization: HmacSHA1 abcd-efgh-1234 sdafkljlkansdaflk2354jlkj5345345dflkmsdf

摘要是在URL编码之前根据正文(编组实体)的内容构建的,其中HMAC共享密钥作为base64附加。对于非正文请求,例如GET或HEAD,内容将被视为完整的URI路径和附加了密钥的参数。

要以Dropwizard可以使用它的方式实现这一点,您需要将dropwizard-auth模块中存在的BasicAuthenticator代码复制到您自己的代码中并使用以下代码修改它:

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

以上并不完美,但它会让你开始。您可能需要参考MultiBit Merchant release candidate source code(MIT许可证)以获取更新版本和各种支持类。

下一步是将身份验证过程集成到您的ResourceTest子类中。不幸的是,Dropwizard没有为v0.4.0中的身份验证提供程序提供良好的入口点,因此您可能希望引入自己的基类,类似于:

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

同样,上述代码并不完美,但其目的是允许模拟UserDao为标准用户提供已知的共享密钥。您必须为测试目的引入自己的UserBuilder实现。

最后,使用上面的代码,Dropwizard资源有一个像这样的端点:

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

}

可以使用如下配置的单元测试进行测试:

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

  }


}

希望这有助于您开始使用。