2012-05-28 78 views

回答

12

目前Dropwizard不支持開箱即用的HMAC認證,因此您必須編寫自己的認證器。 HMAC身份驗證的典型選擇是使用HTTP授權標頭。以下代碼期望以下列格式此標頭:與HMAC URL編碼之前

Authorization: <algorithm> <apiKey> <digest> 

一個例子是

Authorization: HmacSHA1 abcd-efgh-1234 sdafkljlkansdaflk2354jlkj5345345dflkmsdf 

摘要被從所述主體(編組實體)的含量建作爲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()); 

    } 


} 

希望這有助於你開始了。

+0

此外,你可能想讀一下這個:http://rubydoc.info/gems/warden-hmac-authentication/0.6.1/file/README.md。它提供瞭如何在簽署之前安排請求的各個部分的定義。 –

+0

經過更多研究後,您可能需要使用Jersey的ClientFilter工具對最終請求進行最後一刻修改(例如添加您的自定義授權標頭)。再次,[MultiBit Merchant](https://github.com/gary-rowe/MultiBitMerchant)項目演示此代碼。 –