1. Task
In this task, we 'll explore the Java dynamic proxy mechanism. In essence, we'll secure an application service using the JDK dynamic proxies. Our security mechanism requires that we must restrict the access checking the current user’s privileges. Correspondingly, we must discard unauthorized calls.
2. Sample Application
Let's look at the sample application.
We want to secure the DataService operations:
public interface DataService {
@Authorized(allowed = "read")
void read();
@Authorized(allowed = "update")
void update();
@Authorized(allowed = "delete")
void delete();
}
The interface contains three methods and defines the authorization metadata using annotations on the methods.
The @Authorized annotation enables us to define the allowed privileges for an operation.
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Inherited
public @interface Authorized {
String[] allowed();
}
Note that this is a basic annotation with a single attribute, allowed.
Next, we'll provide an implementation for DataService:
public class DataServiceImpl implements DataService {
@Override
public void read() {
System.out.println("Read the value...");
}
@Override
public void update() {
System.out.println("Edited the value...");
}
@Override
public void delete() {
System.out.println("Deleted the value...");
}
}
As we can see, our implementation doesn't perform any authorization checks in the methods.
Then we have the User class that holds the user's privileges:
public class User {
private List<String> privileges;
// Getters and setters...
}
So far, our service implementation DataServiceImpl doesn't use the privileges defined in User. But, in a moment, we'll put some security checks employing the User class.
3. Use JDK Dynamic Proxy to Manage Access
In order to create a Java dynamic proxy, we must first provide an InvocationHandler implementation. The InvocationHandler contains a single method invoke where we can either forward the request or drop it.
We'll implement the authorization checks in the DynamicDataServiceProxy class:
public class DynamicDataServiceProxy implements InvocationHandler {
private final DataService dataService;
private final User user;
public DynamicDataServiceProxy(DataService dataService, User user) {
this.dataService = dataService;
this.user = user;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
Authorized authorized = method.getDeclaredAnnotation(Authorized.class);
if (authorized != null) {
String[] allowedPrivileges = authorized.allowed();
if (isAllowed(allowedPrivileges)) {
return method.invoke(dataService, args);
} else {
return null;
}
}
return method.invoke(dataService, args);
}
// Other code...
}
In this handler, we're comparing the user's privileges with the ones defined on the method. If the user has one of the required privileges, the operation commences.
Now that we've defined the proxy logic in an InvocationHandler, we'll next create a proxy instance:
private static DataService getDynamicProxy(DataService dataService, User user) {
return (DataService)
Proxy.newProxyInstance(
dataService.getClass().getClassLoader(),
dataService.getClass().getInterfaces(),
new DynamicDataServiceProxy(dataService, user));
}
Here, we're invoking the Proxy newProxyInstance method. Note that we're passing the interfaces for the proxy to implement. Also, we're passing the invocation handler instance. The resulting proxy is of type DataService and uses the handler to secure the method calls.
Next, we have a test application:
public static void main(String[] args) {
DataService dataService = new DataServiceImpl();
System.out.println("Read-only user...");
User readOnlyUser = new User();
readOnlyUser.setPrivileges(Lists.newArrayList("read"));
DataService dataServiceProxy = getDynamicProxy(dataService, readOnlyUser);
dataServiceProxy.read();
dataServiceProxy.update();
dataServiceProxy.delete();
System.out.println("Admin user...");
User adminUser = new User();
adminUser.setPrivileges(Lists.newArrayList("read", "update", "delete"));
dataServiceProxy = getDynamicProxy(dataService, adminUser);
dataServiceProxy.read();
dataServiceProxy.update();
dataServiceProxy.delete();
}
We're first defining a read-only user and then an admin user.
A sample run prints:
## Read-only user...
Read the value...
## Admin user...
Read the value...
Edited the value...
Deleted the value...
As expected, the read-only user can't edit or delete a value, whereas the admin user performs all operations.
4. Summary
In this tutorial, we've investigated how we can use the JDK dynamic proxies to create a simple security mechanism for an application service.
As always the source code for all examples is available on Github.