1. Trang chủ >
  2. Công Nghệ Thông Tin >
  3. Quản trị mạng >

Chapter 29. Examples for Chapter 15

Bạn đang xem bản rút gọn của tài liệu. Xem và tải ngay bản đầy đủ của tài liệu tại đây (7.32 MB, 392 trang )


src/main/java/com/restfully/shop/features/OTPAuthenticated.java

@Target({ElementType.METHOD, ElementType.TYPE})

@Retention(RetentionPolicy.RUNTIME)

@NameBinding

public @interface OTPAuthenticated

{

}



When declared on a JAX-RS method, this annotation will trigger the binding of a

ContainerRequestFilter that implements the OTP algorithm using the @NameBind

ing technique discussed in “Name Bindings” on page 181. To apply a name binding, the

OTPAuthenticated annotation interface is annotated with @NameBinding.

With our custom annotation defined, let’s take a look at the filter that implements the

OTP algorithm:

src/main/java/com/restfuly/shop/features/OneTimePasswordAuthenticator.java

@OTPAuthenticated

@Priority(Priorities.AUTHENTICATION)

public class OneTimePasswordAuthenticator implements ContainerRequestFilter

{



The OneTimePasswordAuthenticator class is annotated with @OTPAuthenticated. This

completes the @NameBinding we started when we implemented the @OTPAuthentica

ted annotation interface. The class is also annotated with @Priority. This annotation

affects the ordering of filters as they are applied to a JAX-RS method. We’ll discuss

specifically why we need this later in the chapter, but you usually want authentication

filters to run before any other filter.

protected Map userSecretMap;

public OneTimePasswordAuthenticator(Map userSecretMap)

{

this.userSecretMap = userSecretMap;

}



Our filter will be a singleton object and will be initialized with a map. The key of the

map will be a username, while the value will be the secret password used by the user to

create a one-time password.

@Override

public void filter(ContainerRequestContext requestContext) throws IOException

{

String authorization = requestContext.getHeaderString(

HttpHeaders.AUTHORIZATION);

if (authorization == null) throw new NotAuthorizedException("OTP");

String[] split = authorization.split(" ");

final String user = split[0];

String otp = split[1];



342



|



Chapter 29: Examples for Chapter 15



www.it-ebooks.info



In the first part of our filter() method, we parse the Authorization header that was

sent by the client. The username and encoded password are extracted from the header

into the user and otp variables.

String secret = userSecretMap.get(user);

if (secret == null) throw new NotAuthorizedException("OTP");

String regen = OTP.generateToken(secret);

if (!regen.equals(otp)) throw new NotAuthorizedException("OTP");



Next, our filter() method looks up the secret of the user in its map and generates its

own one-time password. This token is compared to the value sent in the Authoriza

tion header. If they match, then the user is authenticated. If the user does not exist or

the one-time password is not validated, then a 401, “Not Authorized,” response is sent

back to the client.

final SecurityContext securityContext =

requestContext.getSecurityContext();

requestContext.setSecurityContext(new SecurityContext()

{

@Override

public Principal getUserPrincipal()

{

return new Principal()

{

@Override

public String getName()

{

return user;

}

};

}

@Override

public boolean isUserInRole(String role)

{

return false;

}

@Override

public boolean isSecure()

{

return securityContext.isSecure();

}

@Override

public String getAuthenticationScheme()

{

return "OTP";

}

});



Example ex15_1: Custom Security



www.it-ebooks.info



|



343



After the user is authenticated, the filter() method creates a custom SecurityCon

text implementation within an inner anonymous class. It then overrides the existing

SecurityContext by calling ContainerRequestContext.setSecurityContext(). The

SecurityContext.getUserPrincipal() is implemented to return a Principal initial‐

ized with the username sent in the Authorization header. Other JAX-RS code can now

inject this custom SecurityContext to find out who the user principal is.

The algorithm for generating a one-time password is pretty simple. Let’s take a look:

src/main/java/com/restfully/shop/features/OTP.java

public class OTP

{

public static String generateToken(String secret)

{

long minutes = System.currentTimeMillis() / 1000 / 60;

String concat = secret + minutes;

MessageDigest digest = null;

try

{

digest = MessageDigest.getInstance("MD5");

}

catch (NoSuchAlgorithmException e)

{

throw new IllegalArgumentException(e);

}

byte[] hash = digest.digest(concat.getBytes(Charset.forName("UTF-8")));

return Base64.encodeBytes(hash);

}

}



OTP is a simple class. It takes any arbitrary password and combines it with the current

time in minutes to generate a new String object. An MD5 hash is done on this String

object. The hash bytes are then Base 64–encoded using a RESTEasy-specific library and

returned as a String.



The @OTPAuthenticated annotation is then applied to two methods in the Customer

Resource class to secure access to them:

src/main/java/com/restfully/shop/services/CustomerResource.java

@GET

@Path("{id}")

@Produces("application/xml")

@OTPAuthenticated

public Customer getCustomer(@PathParam("id") int id)

{

...

}

@PUT



344



|



Chapter 29: Examples for Chapter 15



www.it-ebooks.info



@Path("{id}")

@Consumes("application/xml")

@OTPAuthenticated

@AllowedPerDay(1)

public void updateCustomer(@PathParam("id") int id, Customer update)

{

...

}



The getCustomer() and updateCustomer() methods are now required to be OTP

authenticated.



Allowed-per-Day Access Policy

The next custom security feature we’ll implement is an allowed-per-day access policy.

The idea is that for a certain JAX-RS method, we’ll specify how many times each user

is allowed to execute that method per day. We will do this by applying the @Allowed

PerDay annotation to a JAX-RS method:

src/main/java/com/restfuly/shop/features/AllowedPerDay.java

@Target({ElementType.METHOD, ElementType.TYPE})

@Retention(RetentionPolicy.RUNTIME)

@NameBinding

public @interface AllowedPerDay

{

int value();

}



As with @OTPAuthenticated, we’ll use a @NameBinding to bind the annotation to a spe‐

cific ContainerRequestFilter. Let’s take a look at that filter:

src/main/java/com/restfuly/shop/features/PerDayAuthorizer.java

@AllowedPerDay(0)

@Priority(Priorities.AUTHORIZATION)

public class PerDayAuthorizer implements ContainerRequestFilter

{



The PerDayAuthorizer class is annotated with @AllowedPerDay. This completes the

@NameBinding we started when we implemented the @AllowedPerDay annotation in‐

terface. The class is also annotated with @Priority. This annotation affects the ordering



of filters as they are applied to a JAX-RS method. We want this filter to run after any

authentication code, but before any application code, as we are figuring out whether

or not a user is allowed to invoke the request. If we did not annotate the

OneTimePasswordAuthenticator and PerDayAuthorizer classes with the @Priority

annotation, it is possible that the PerDayAuthorizer would be invoked before the One

TimePasswordAuthenticator filter. The PerDayAuthorizer needs to know the



Example ex15_1: Custom Security



www.it-ebooks.info



|



345



authenticated user created in the OneTimePasswordAuthenticator filter; otherwise, it

won’t work.

@Context

ResourceInfo info;



We inject a ResourceInfo instance into the filter instance using the @Context annota‐

tion. We’ll need this variable to know the current JAX-RS method that is being invoked.

public void filter(ContainerRequestContext requestContext) throws IOException

{

SecurityContext sc = requestContext.getSecurityContext();

if (sc == null) throw new ForbiddenException();

Principal principal = sc.getUserPrincipal();

if (principal == null) throw new ForbiddenException();

String user = principal.getName();



The filter() method first obtains the SecurityContext from the ContainerRequest

Context.getSecurityContext() method. If the context is null or the user principal is

null, it returns a 403, “Forbidden,” response to the client by throwing a ForbiddenEx

ception.

if (!authorized(user))

{

throw new ForbiddenException();

}

}



The username value is passed to the authorized() method to check the permission. If

the method returns false, a 401, “Forbidden,” response is sent back to the client via a

ForbiddenException.

protected static class UserMethodKey

{

String username;

Method method;

public UserMethodKey(String username, Method method)

{

this.username = username;

this.method = method;

}

@Override

public boolean equals(Object o)

{

if (this == o) return true;

if (o == null || getClass() != o.getClass()) return false;

UserMethodKey that = (UserMethodKey) o;

if (!method.equals(that.method)) return false;



346



|



Chapter 29: Examples for Chapter 15



www.it-ebooks.info



if (!username.equals(that.username)) return false;

return true;

}

@Override

public int hashCode()

{

int result = username.hashCode();

result = 31 * result + method.hashCode();

return result;

}

}

protected Map count =

new HashMap();



The filter instance remembers how many times in a day a particular user invoked a

particular JAX-RS method. It stores this information in the count variable map. This

map is keyed by a custom UserMethodKey class, which contains the username and JAXRS method that is being tracked.

protected long today = System.currentTimeMillis();

protected synchronized boolean authorized(String user, AllowedPerDay allowed)

{

if (System.currentTimeMillis() > today + (24 * 60 * 60 * 1000))

{

today = System.currentTimeMillis();

count.clear();

}



The authorized() method is synchronized, as this filter may be concurrently accessed

and we need to do this policy check atomically. It first checks to see if a day has elapsed.

If so, it resets the today variable and clears the count map.

UserMethodKey key = new UserMethodKey(user, info.getResourceMethod());

Integer counter = count.get(user);

if (counter == null)

{

counter = 0;

}



The authorized() method then checks to see if the current user and method are already

being tracked and counted.

AllowedPerDay allowed =

info.getResourceMethod().getAnnotation(AllowedPerDay.class);

if (allowed.value() > counter)

{

count.put(user, counter + 1);

return true;



Example ex15_1: Custom Security



www.it-ebooks.info



|



347



}

return false;

}

}



The method then extracts the AllowedPerDay annotation from the current JAX-RS

method that is being invoked. This annotation will contain the number of times per day

that a user is allowed to invoke the current JAX-RS method. If this value is greater than

the current count for that user for that method, then we update the counter and return

true. Otherwise, the policy check has failed and we return false.

We then apply this functionality to a JAX-RS resource method by using the @Allowed

PerDay annotation:

src/main/java/com/restfully/shop/services/CustomerResource.java

@PUT

@Path("{id}")

@Consumes("application/xml")

@OTPAuthenticated

@AllowedPerDay(1)

public void updateCustomer(@PathParam("id") int id, Customer update)

{

...

}



A user will now only be able to invoke the updateCustomer() method once per day.

The last thing we have to do is initialize our deployment. Our Application class needs

to change a little bit to enable this:

src/main/java/com/restfully/shop/services/ShoppingApplication/java

@ApplicationPath("/services")

public class ShoppingApplication extends Application

{

private Set singletons = new HashSet();

public ShoppingApplication()

{

singletons.add(new CustomerResource());

HashMap userSecretMap = new HashMap();

userSecretMap.put("bburke", "geheim");

singletons.add(new OneTimePasswordAuthenticator(userSecretMap));

singletons.add(new PerDayAuthorizer());

}

@Override

public Set getSingletons()

{

return singletons;



348



|



Chapter 29: Examples for Chapter 15



www.it-ebooks.info



}

}



The ShoppingApplication class populates the user-secret map that must be used to

construct the singleton OneTimePasswordAuthenticator instance. The PerDayAuthor

izer class is also a singleton and instantiated by this constructor.



The client code

The first thing we do on the client side is to implement a ClientRequestFilter that

sets up the Authorization header that will be sent to the server:

src/main/java/com/restfully/shop/features/OneTimePasswordGenerator.java

public class OneTimePasswordGenerator implements ClientRequestFilter

{

protected String user;

protected String secret;

public OneTimePasswordGenerator(String user, String secret)

{

this.user = user;

this.secret = secret;

}

@Override

public void filter(ClientRequestContext requestContext) throws IOException

{

String otp = OTP.generateToken(secret);

requestContext.getHeaders().putSingle

(HttpHeaders.AUTHORIZATION, user + " " + otp);

}

}



This filter is very simple. It is constructed with the username and password we will use

to generate the one-time password. The filter() method generates the one-time pass‐

word by calling the OTP.generateToken() method we described earlier in this chapter.

The filter() method then generates and sets the Authorization header for the HTTP

request.

The client test code is the same as ex06_1 except that we set it up to use OTP authenti‐

cation. Let’s take a look:

src/test/java/com/restfully/shop/test/CustomerResourceTest.java

@Test

public void testCustomerResource() throws Exception

{

System.out.println("*** Create a new Customer ***");

Customer newCustomer = new Customer();

newCustomer.setFirstName("Bill");

newCustomer.setLastName("Burke");



Example ex15_1: Custom Security



www.it-ebooks.info



|



349



newCustomer.setStreet("256 Clarendon Street");

newCustomer.setCity("Boston");

newCustomer.setState("MA");

newCustomer.setZip("02115");

newCustomer.setCountry("USA");

Response response = client.target(

"http://localhost:8080/services/customers")

.request().post(Entity.xml(newCustomer));

if (response.getStatus() != 201) throw new RuntimeException

("Failed to create");

String location = response.getLocation().toString();

System.out.println("Location: " + location);

response.close();



The testCustomerResource() method starts off the same way as in ex06_1. It creates

a customer and obtains its URI from the response. Creating a customer is not authen‐

ticated so we do not need to worry about setting up authorization here.

System.out.println("*** GET Created Customer **");

Customer customer = null;

WebTarget target = client.target(location);

try

{

customer = target.request().get(Customer.class);

Assert.fail(); // should have thrown an exception

}

catch (NotAuthorizedException e)

{

}



This particular code shows what happens when an unauthenticated request is made. It

makes a GET request on the new customer’s URI that fails with a NotAuthorizedEx

ception because we have not set up our OTP filter yet.

target.register(new OneTimePasswordGenerator("bburke", "geheim"));



We register an instance of our OneTimePasswordGenerator filter initialized with our

username and static password. We can now make an authenticated GET request without

error.

customer = target.request().get(Customer.class);

System.out.println(customer);



To show our allowed-per-day policy in action, the code executes a customer update

twice.

customer.setFirstName("William");

response = target.request().put(Entity.xml(customer));

if (response.getStatus() != 204)

throw new RuntimeException("Failed to update");

++++





350



|



Chapter 29: Examples for Chapter 15



www.it-ebooks.info



++++

// Show the update

System.out.println("**** After Update ***");

customer = target.request().get(Customer.class);

System.out.println(customer);

// only allowed to update once per day

customer.setFirstName("Bill");

response = target.request().put(Entity.xml(customer));

Assert.assertEquals(Response.Status.FORBIDDEN, response.getStatusInfo());

}



The first invocation succeeds, but the second fails because we are allowed to invoke this

method only once per day.



Build and Run the Example Program

Perform the following steps:

1. Open a command prompt or shell terminal and change to the ex15_1 directory of

the workbook example code.

2. Make sure your PATH is set up to include both the JDK and Maven, as described

in Chapter 17.

3. Perform the build and run the example by typing maven install.



Example ex15_1: JSON Web Encryption

In Chapter 15, you learned a little bit about JSON Web Encryption (JWE) and how it

can be used to encrypt HTTP message body or header values. This example augments

the customer chat client implemented in Chapter 27. Chat clients will use a shared secret

to encrypt and decrypt the messages they send to and receive from the chat server. Chat

clients that know the shared secret see the decrypted message, while clients that don’t

know it see only the JWE encoding. Let’s take a look at the code:

src/main/java/ChatClient.java

public class ChatClient

{

public static void main(String[] args) throws Exception

{

String name = args[0];

final String secret = args[1];



The ChatClient first starts out by storing the name and secret password that the client

will use. It obtains these values from the command line.



Example ex15_1: JSON Web Encryption



www.it-ebooks.info



|



351



final Client client = new ResteasyClientBuilder()

.connectionPoolSize(3)

.build();

WebTarget target = client.target("http://localhost:8080/services/chat");

target.request().async().get(new InvocationCallback()

{

@Override

public void completed(Response response)

{

Link next = response.getLink("next");

String message = response.readEntity(String.class);

try

{

JWEInput encrypted = new JWEInput(message);

message = encrypted.decrypt(secret).readContent(String.class);

}

catch (Exception ignore)

{

//e.printStackTrace();

}

System.out.println();

System.out.print(message);

System.out.println();

System.out.print("> ");

client.target(next).request().async().get(this);

}

@Override

public void failed(Throwable throwable)

{

System.err.println("FAILURE!");

}

});



The code then implements the receive loop we discussed in Chapter 27. The difference

is that it uses the RESTEasy org.jboss.resteasy.jose.jwe.JWEInput class to decrypt

the received message. A JWEInput instance is initialized with the received text message.

The JWEInput.decrypt() method decrypts the JWE with the shared secret. The read

Context() method extracts the decrypted bytes into a String object that we can output

to the console. If the message is not a JWE or if the wrong secret is used, then the original

received text message is outputted to the console.

Let’s now take a look at how sending a message has changed:

while (true)

{

System.out.print("> ");

BufferedReader br = new BufferedReader

(new InputStreamReader(System.in));

String message = name + ": " + br.readLine();



352



|



Chapter 29: Examples for Chapter 15



www.it-ebooks.info



Xem Thêm