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
public OneTimePasswordAuthenticator(Map
{
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
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