Warning |
---|
|
Work in progress / Draft / Comments welcome but nonethelesss page not finished... |
Let me lead with the fact that I don't understand Spring Web MVC well enough.
...
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
Haven't I seen this before?
Yes, this is this page all over again.
Panel |
---|
title | LoginController inheritence |
---|
|
For reference, the following is the class hierarchy for LoginController. Highlighted methods are those involved in the discussion below. Tip |
---|
icon | false |
---|
title | LoginController |
---|
|
extends Note |
---|
|
icon | false |
---|
title | SimpleFormController |
---|
|
extends
Note |
---|
icon | false |
---|
title | AbstractFormController |
|
- protected abstract ModelAndView processFormSubmissionshowForm(
final HttpServletRequest request, final HttpServletResponse response, final BindException errors) throws Exception {} - protected ModelAndView processFormSubmission(final HttpServletRequest request, final HttpServletResponse response, final Object object, final BindException errors) throws Exception {}
|
extends
Note |
---|
icon | false |
---|
title | SimpleFormController |
---|
|
|
extends
Note |
---|
icon | false |
---|
title | AbstractFormController |
---|
|
- protected abstract ModelAndView processFormSubmission(
HttpServletRequest request, HttpServletResponse response, Object command, BindException errors) throws Exception; - protected final Object getCommand(HttpServletRequest request) throws Exception;
- protected Object formBackingObject(HttpServletRequest request) throws Exception {};
|
extends
Note |
---|
icon | false |
---|
title | BaseCommandController |
---|
|
- protected Object getCommand(HttpServletRequest request) {}
- protected final Object createCommand() throws InstantiationException, IllegalAccessException;
- protected final ServletRequestDataBinder bindAndValidate(HttpServletRequest request, Object command) throws Exception {}
|
extends
Note |
---|
icon | false |
---|
title | AbstractController |
---|
|
- public final ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response) throws Exception {}
- protected abstract ModelAndView handleRequestInternal(HttpServletRequest request, HttpServletResponse response) throws Exception;
|
extends
Note |
---|
icon | false |
---|
title | WebContentGenerator |
---|
|
|
extends
Note |
---|
icon | false |
---|
title | WebApplicationObjectSupport |
---|
|
|
extends
Note |
---|
icon | false |
---|
title | ApplicationObjectSupport |
---|
|
|
...
In this page I'd like to look carefully at where that command object is coming from and what binding is being done.
Panel |
---|
bgColor | #FFFFCE |
---|
borderStyle | solid |
---|
title | Where is the object coming from? |
---|
borderStyle | solid |
---|
|
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(): Code Block |
---|
title | AbstractController 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: Code Block |
---|
title | AbstractController handleRequestInternal() |
---|
|
/**
* 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: Code Block |
---|
title | AbstractCommandController handleRequestInternal() implementation |
---|
|
/**
* 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. Warning |
---|
| The bindAndValidate() method is implemented in BaseCommandController as: Code Block |
---|
title | BaseCommandController bindAndValidate() |
---|
|
/**
* Bind the parameters of the given request to the given command object.
* @param request current HTTP request
* @param command the command to bind onto
* @return the ServletRequestDataBinder instance for additional custom validation
* @throws Exception in case of invalid state or arguments
*/
protected final ServletRequestDataBinder bindAndValidate(HttpServletRequest request, Object command)
throws Exception {
ServletRequestDataBinder binder = createBinder(request, command);
binder.bind(request);
onBind(request, command, binder.getErrors());
if (this.validators != null && isValidateOnBinding() && !suppressValidation(request)) {
for (int i = 0; i < this.validators.length; i++) {
ValidationUtils.invokeValidator(this.validators[i], command, binder.getErrors());
}
}
onBindAndValidate(request, command, binder.getErrors());
return binder;
}
|
That createBinder method it invokes looks like this: Code Block |
---|
title | BaseCommandController createBinder() |
---|
|
protected ServletRequestDataBinder createBinder(HttpServletRequest request, Object command)
throws Exception {
ServletRequestDataBinder binder = new ServletRequestDataBinder(command, getCommandName());
if (this.messageCodesResolver != null) {
binder.setMessageCodesResolver(this.messageCodesResolver);
}
initBinder(request, binder);
return binder;
}
|
The upshot of the matter is that setter methods for JavaBean properties the names of which correspond to the names of parameters on the HttpServletRequest are invoked with the values of those HttpServletRequest parameters as arguments. |
whereas processFormSubmission() is an abstract method of AbstractFormController: Code Block |
---|
title | processFormSubmission is abstract method of AbstractFormController |
---|
|
protected abstract ModelAndView processFormSubmission(
HttpServletRequest request, HttpServletResponse response, Object command, BindException errors)
throws Exception;
|
LoginController defaults this Command class: Code Block |
---|
title | LoginController afterPropertiesSet() excerpt |
---|
|
public void afterPropertiesSet() throws Exception {
if (this.getCommandClass() == null) {
this.setCommandName("credentials");
this.setCommandClass(UsernamePasswordCredentials.class);
|
In BaseCommandController, we have: Code Block |
---|
title | BaseCommandController getCommand() |
---|
|
/**
* 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: Code Block |
---|
title | BaseCommandController createCommand() |
---|
|
/**
* 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 summaryTo 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(). |
...
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's an alternative interface for the plugin that goes from HttpServletRequests to Credentials:
...
Note |
---|
title | Why several different types of credentials in one LoginController instance? |
---|
|
A draft of this document prompted the question "Why does an instance of a LoginController need to support more than one type of Credentials?" Any given CAS deployment might want to support more than one type of credentials. Say, username/password and also desktop Kerberos. So we'd have two radically different kinds of Credentials. One way to handle this would be to have multiple instances of the LoginController, one for each type of credentials we might handle. But how did we know which LoginController to go to? We probably shouldn't switch on the URL, since after all any given CAS client is going to redirect to the single institutional CAS login URL regardless of end user preference about what kind of credential she wants to present. Another way (I propose) is to give our LoginController a plugin, a RequestToCredentials. This object is responsible for examining the HttpServletRequest and returning the Credentials it represents. It is these credentials that get passed from LoginController into the CentralAuthenticationService for processing. One plausible implementation of this interface will be the implementation that delegates to a List of other implementations, returning true to supports() if any of its children support the presented HttpServletRequest and returning the Credentials returned by the first of its children to support the request. |
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's an alternative interface for the plugin that goes from HttpServletRequests to Credentials:
Code Block |
---|
/**
* Interface for components that know how to extract from HttpServletRequest
* whatever it is that constitutes actual arguments of the request for authentication.
*/
public interface RequestToCredentials {
/**
* Parse an HttpServletRequest and extract from it whatever it is that is necessary as input to the AuthenticationHandler
* which will examine the request for authentication represented by the HttpServletRequest. Return an Object
* encapsulating that extracted information. Specific implementations will return specific objects which in turn specific
* AuthenticationHandler implementations will expect and consume.
* @returns an object representing the relevant information for the authentication request
* @throws RuntimeException - indicates failure
*/
booleanObject supportscredentialsFromHttpServletRequest(HttpServletRequest httpServletRequest);
}
|
Now, we might like to over-ride the BaseCommandController's implmentation of getCommand():
Code Block |
---|
title | BaseCommandController's implementation of getCommand() |
---|
|
protected Object getCommand(HttpServletRequest request) throws Exception {
return createCommand();
}
|
to apply a RequestToCredentials instance that we've added as a dependency of LoginController:
Code Block |
---|
title | a getCommand() implementation that delegates to a RequestToCredentials |
---|
|
protected Object getCommand(HttpServletRequest request) throws Exception {
return this.requestToCredentials.credentialsFromHttpServletRequest(request);
}
|
however, we cannot do this, because AbstractFormController finalized its implementation of getCommand(), which attempts to provide an instance of a JavaBean to back the HTML Form, because after all AbstractFormController is about forms.
Code Block |
---|
title | AbstractFormController's getCommand() implementation |
---|
|
protected final Object getCommand(HttpServletRequest request) throws Exception {
if (!isSessionForm()) {
return formBackingObject(request);
}
HttpSession session = request.getSession(false);
if (session == null) {
throw new ServletException("Must have session when trying to bind");
}
Object formObject = session.getAttribute(getFormSessionAttributeName());
session.removeAttribute(getFormSessionAttributeName());
if (formObject == null) {
throw new ServletException("Form object not found in session");
}
return formObject;
}
|
However, it does have that formBackingObject() method:
Code Block |
---|
title | AbstractFormController formBackingObject() |
---|
|
protected Object formBackingObject(HttpServletRequest request) throws Exception {
return createCommand();
}
|
So we could override that method's implementation to add delegation to our RequestToCredentials instance.
What to do
...
/**
* Returns true if authenticationRequestFromHttpServletRequest() will return an Object for the given
* argument. Returns false if this other method will throw a RuntimeException for the given argument.
*
* This method exists to allow a client of this class to efficiently determine whether it should use this
* RequestToCredentials or whether doing so will only throw an exception.
*/
boolean supports(HttpServletRequest httpServletRequest);
}
|
Now, we might like to over-ride the BaseCommandController's implementation of getCommand():
Code Block |
---|
title | BaseCommandController's implementation of getCommand() |
---|
|
protected Object getCommand(HttpServletRequest request) throws Exception {
return createCommand();
}
|
to apply a RequestToCredentials instance that we've added as a dependency of LoginController:
Code Block |
---|
title | a getCommand() implementation that delegates to a RequestToCredentials |
---|
|
protected Object getCommand(HttpServletRequest request) throws Exception {
return this.requestToCredentials.credentialsFromHttpServletRequest(request);
}
|
however, we cannot do this, because AbstractFormController finalized its implementation of getCommand(), which attempts to provide an instance of a JavaBean to back the HTML Form, because after all AbstractFormController is about forms.
Code Block |
---|
title | AbstractFormController's getCommand() implementation |
---|
|
protected final Object getCommand(HttpServletRequest request) throws Exception {
if (!isSessionForm()) {
return formBackingObject(request);
}
HttpSession session = request.getSession(false);
if (session == null) {
throw new ServletException("Must have session when trying to bind");
}
Object formObject = session.getAttribute(getFormSessionAttributeName());
session.removeAttribute(getFormSessionAttributeName());
if (formObject == null) {
throw new ServletException("Form object not found in session");
}
return formObject;
}
|
However, it does have that formBackingObject() method:
Code Block |
---|
title | AbstractFormController formBackingObject() |
---|
|
protected Object formBackingObject(HttpServletRequest request) throws Exception {
return createCommand();
}
|
So we could override that method's implementation to add delegation to our RequestToCredentials instance.
What to do
Let's not, however. Let's use the Form abstraction provided by AbstractFormController for the portion of the input to LoginController that actually is a form. Credentials are not necessarily a form, they are any object. Spring serves us best by staying out of our way and letting us implement this piece of the Login controller directly.
Our form revisited
Suppose we use the Object argument to processFormSubmission to handle the handful of request parameters that we need to process regardless of the Credentials type.
...
the Object argument to processFormSubmission to handle the handful of request parameters that we need to process regardless of the Credentials type.
Code Block |
---|
|
protected ModelAndView processFormSubmission(final HttpServletRequest request, final HttpServletResponse response, final Object object,
final BindException errors) throws Exception {
final Credentials credentials = (Credentials)object;
protected ModelAndView
processFormSubmission(final HttpServletRequest request, final HttpServletResponse response, final Object object, this.credentialsBinder.bind(request, credentials);
final BindExceptionString errors)ticketGrantingTicketId throws Exception {= this.centralAuthenticationService.createTicketGrantingTicket(credentials);
final CredentialsString credentialsservice = (Credentials)objectrequest.getParameter(WebConstants.SERVICE);
final boolean warn = StringUtils.hasText(request.getParameter(WebConstants.WARN));
|
Code Block |
---|
|
public this.credentialsBinder.bind(request, credentials);class LoginForm {
private String service;
final String ticketGrantingTicketIdprivate = this.centralAuthenticationService.createTicketGrantingTicket(credentials)boolean warn;
final /** String
service = request.getParameter(WebConstants.SERVICE); * Set the Service to which finalthe booleanrequestor warnseeks =to StringUtils.hasText(request.getParameter(WebConstants.WARN));
|
Code Block |
---|
|
public class LoginForm {authenticate.
*/
public privatevoid setService(String service;) {
privatethis.service boolean= warnservice;
}
/**
* Set whether the user Servicewould like to be warned whichupon theSingle requestorSign seeksOn to subsequent authenticateservices.
*/
public void setServicesetWarn(Stringboolean service) {
warn) {
this.warn = warn;
}
... and corresponding getters
}
|
We configure our commandClass property to be LoginForm.class (in the LoginController constructor, say).
The LoginController form handling implementation becomes:
Code Block |
---|
title | LoginController revisited |
---|
|
protected this.service = service;
}
ModelAndView processFormSubmission(final HttpServletRequest request, final HttpServletResponse response, final Object object,
/** final *BindException Seterrors) whetherthrows theException user{
would like to be warned upon Single Sign Onfinal toLoginForm subsequentloginForm services.= (LoginForm) object;
*/ public
void setWarn(boolean warn) { final Object credentials = this.warn = warn;requestToCredentials.credentialsFromHttpServletRequest(request);
} ...final andString correspondingticketGrantingTicketId getters
}= this.centralAuthenticationService.createTicketGrantingTicket(credentials);
|
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.