Let me lead with the fact that I don't understand Spring Web MVC well enough.
That said, I think I have some concerns with the CredentialsBinder API and its associated LoginController implementation.
Executive summary
We need to be able to support determining at runtime per request what kind of Credentials we have received and will be passing into the CentralAuthenticationService.
Discussion
For reference, the following is the class hierarchy for LoginController. Highlighted methods are those involved in the discussion below.
extends
extends
extends
extends
extends
extends
extends
LoginController extends Spring SimpleFormHandler and looks something like this:
protected ModelAndView processFormSubmission(final HttpServletRequest request, final HttpServletResponse response, final Object object, final BindException errors) throws Exception { final Credentials credentials = (Credentials)object; this.credentialsBinder.bind(request, credentials); final String ticketGrantingTicketId = this.centralAuthenticationService.createTicketGrantingTicket(credentials); ...
What we see here is LoginController picking up the credentials from the "object" argument to this method (the Spring Web MVC command / form that this handler is going to handle). It binds the request to this object using its CredentialsBinder and then passes it into the CentralAuthenticationService service object to obtain the TGT string.
In this page I'd like to look carefully at where that command object is coming from and what binding is being done.
We need to look carefully at the "object" argument. The object here is a Command. LoginController extends SimpleFormController which extends AbstractFormController which extends BaseCommandController which extends AbstractController which extends WebContentGenerator which extends WebApplicationObjectSupport which extends ApplicationObjectSupport. In Spring Web MVC FormControllers, the forms play the role of "commands" in the more general CommandController model.
So, in AbstractController there is a final method handleRequest():
public final ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response) throws Exception { // delegate to WebContentGenerator for checking and preparing checkAndPrepare(request, response, this instanceof LastModified); // execute in synchronized block if required if (this.synchronizeOnSession) { HttpSession session = request.getSession(false); if (session != null) { synchronized (session) { return handleRequestInternal(request, response); } } } return handleRequestInternal(request, response); }
As we can see here, it delegates to the method handleRequestInternal(), which is declared to be abstract:
/** * Template method. Subclasses must implement this. * The contract is the same as for handleRequest. * @see #handleRequest */ protected abstract ModelAndView handleRequestInternal(HttpServletRequest request, HttpServletResponse response) throws Exception;
AbstractCommandController implements this abstract method:
/** * Handles two cases: form submissions and showing a new form. * Delegates the decision between the two to isFormSubmission, * always treating requests without existing form session attribute * as new form when using session form mode. */ protected final ModelAndView handleRequestInternal(HttpServletRequest request, HttpServletResponse response) throws Exception { if (isFormSubmission(request)) { if (isSessionForm() && request.getSession().getAttribute(getFormSessionAttributeName()) == null) { // cannot submit a session form if no form object is in the session return handleInvalidSubmit(request, response); } // process submit Object command = getCommand(request); ServletRequestDataBinder binder = bindAndValidate(request, command); return processFormSubmission(request, response, command, binder.getErrors()); } else { return showNewForm(request, response); } }
And in turn delegates to processFormSubmission(). Note that it first binds the request to the command and validates the request.
whereas processFormSubmission() is an abstract method of AbstractFormController:
protected abstract ModelAndView processFormSubmission( HttpServletRequest request, HttpServletResponse response, Object command, BindException errors) throws Exception;
LoginController defaults this Command class:
public void afterPropertiesSet() throws Exception { if (this.getCommandClass() == null) { this.setCommandName("credentials"); this.setCommandClass(UsernamePasswordCredentials.class);
In BaseCommandController, we have:
/** * Retrieve a command object for the given request. * <p>Default implementation calls createCommand. Subclasses can override this. * @param request current HTTP request * @return object command to bind onto * @see #createCommand */ protected Object getCommand(HttpServletRequest request) throws Exception { return createCommand(); }
And the createCommand() method to which it delegates:
/** * Create a new command instance for the command class of this controller. * @return the new command instance * @throws InstantiationException if the command class could not be instantiated * @throws IllegalAccessException if the class or its constructor is not accessible */ protected final Object createCommand() throws InstantiationException, IllegalAccessException { if (this.commandClass == null) { throw new IllegalStateException("Cannot create command without commandClass being set - " + "either set commandClass or override formBackingObject"); } if (logger.isDebugEnabled()) { logger.debug("Creating new command of class [" + this.commandClass.getName() + "]"); } return this.commandClass.newInstance(); }
In summary
To sum up, the "command" argument of type Object to the processFormSubmission() method of LoginController is an instance of the Class that was set using the setCommandClass() method by configuring the commandClass JavaBean property of LoginController or by allowing it to default as implemented in LoginController's afterPropertiesSet().
Okay, so we've determined where that "command" object is coming from. Now let's look at how LoginController uses it:
protected ModelAndView processFormSubmission(final HttpServletRequest request, final HttpServletResponse response, final Object object, final BindException errors) throws Exception { final Credentials credentials = (Credentials)object; this.credentialsBinder.bind(request, credentials); final String ticketGrantingTicketId = this.centralAuthenticationService.createTicketGrantingTicket(credentials); ...
What LoginController does is apply a configured CredentialsBinder to the Object it received as its Command (which it is interpretting as and requires to implement the marker interface "Credentials").
CredentialsBinder looks like this:
/** * Interface for a class that can bind items stored in the request to a particular * credentials implementation. This allows for binding beyond the basic * JavaBean/Request parameter binding that is handled by Spring automatically. */ public interface CredentialsBinder { /** * Method to allow manually binding attributes from the request object to properties of * the credentials. Useful when there is no mapping of attribute to property for the * usual Spring binding to handle. * * @param request The HttpServletRequest from which we wish to bind credentials to * @param credentials The credentials we will be doing custom binding to. */ void bind(HttpServletRequest request, Credentials credentials); /** * * Method to determine if a CredentialsBinder supports a specific class or not. * * @param clazz The class to determine is supported or not * @return true if this class is supported by the CredentialsBinder, false otherwise. */ boolean supports(Class clazz); }
This doesn't appear to get us where we need to be, though. We need to be able to support switching among several different types of Credentials. We might have mapped AuthenticationHandlers for Password credentials and for Kerberos credentials, say. The current LoginController implementation requires us to name the class an instance of which will be our Credentials once for the entire instance of LoginController. Per each request we can only configure the Credentials instance that has already been created for us.
What we need to do is to examine the request and determine what kind of Credentials we need and to instantiate that credentials and configure it accordingly. We need to do so in complete freedom to come back with any kind of Credentials.
Accordingly, here is an alternative formulation of the
Copyright notice
Cited code snippets from The Spring Framework are used here for the purpose of explaining CAS 3's usage of this framework. The Spring Framework is subject to license agreement.