5

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

先感谢您。

4

1 回答 1

12

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

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 候选发布源代码(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());

  }


}

希望这可以帮助您入门。

于 2012-05-28T12:49:24.810 回答