【问题标题】:How can I test HMAC authentication using Dropwizard?如何使用 Dropwizard 测试 HMAC 身份验证?
【发布时间】:2012-05-28 10:23:24
【问题描述】:

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

提前谢谢你。

【问题讨论】:

    标签: dropwizard


    【解决方案1】:

    目前 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 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());
    
      }
    
    
    }
    

    希望这可以帮助您入门。

    【讨论】:

    • 另外,您可能想阅读以下内容:rubydoc.info/gems/warden-hmac-authentication/0.6.1/file/…。它定义了在签名之前应如何安排请求的各个部分。
    • 而且,经过更多研究,您可能希望使用 Jersey 的 ClientFilter 工具对最终请求进行最后修改(例如,添加自定义 Authorization 标头)。同样,MultiBit Merchant 项目演示了这段代码。
    猜你喜欢
    • 2017-03-15
    • 2017-08-20
    • 2018-05-28
    • 1970-01-01
    • 2017-11-21
    • 1970-01-01
    • 2015-03-22
    • 2014-11-23
    • 2015-02-10
    相关资源
    最近更新 更多