Last updated by simonckenyon 2 years ago
The typical pattern of using web site authentication to access restricted pages involves intercepting access requests for secure pages, redirecting to a login page (possibly off-site) and redirecting back to the originally-requested page after a successful login. Each page can also have a login link to allow explicit logins at any time.


Another option is to also have a login link on each page and use Ajax and DHTML to present a login form within the current page in a popup. The form submits the authentication request via Ajax and displays success or error messages as appropriate.


The Spring Security plugin has support for Ajax logins but you'll need to create your own GSP code. There are only a few necessary changes, and of course the sample code here is pretty basic so you should enhance it for your needs.

The approach I'll show here involves editing your template page(s) to show "You're logged in as ..." text if logged in and a login link if not, along with a hidden login form that's shown using DHTML.

Here's the updated grails-app/views/layouts/main.gsp:

<html>
<head>

<title><g:layoutTitle default='Grails' /></title> <link rel='stylesheet' href='${createLinkTo(dir:'css',file:'main.css')}' /> <link rel='shortcut icon' type='image/x-icon' href='${createLinkTo(dir:'images',file:'favicon.ico')}' />

<g:layoutHead />

<g:javascript library='application' /> <g:javascript library='scriptaculous' />

</head>

<body>

<g:render template='/includes/ajaxLogin'/>

<div id='spinner' class='spinner' style='display:none;'> <img src='${createLinkTo(dir:'images',file:'spinner.gif')}' alt='Spinner' /> </div>

<div class='logo'> <img src='${createLinkTo(dir:'images',file:'grails_logo.jpg')}' alt='Grails' />

<span id='loginLink' style='position: relative; margin-right: 30px; float: right; top: -60px'> <g:isLoggedIn> Logged in as <g:loggedInUsername/> (<g:link controller='logout'>Logout</g:link>) </g:isLoggedIn> <g:isNotLoggedIn> <a href='#' onclick='showLogin(); return false;'>Login</a> </g:isNotLoggedIn> </span>

</div>

<g:layoutBody/>

</body> </html>

The changes to note here include:
  • the scriptaculous library is included to have support for hiding and showing the login form
  • there's an include of the template '/includes/ajaxLogin' (see the code below)
  • and finally there's a <span> positioned in the top-right which shows the username and a logout link when logged in, and a login link otherwise
Here's the content of the login form template (grails-app/includes/_ajaxLogin.gsp) - note that the CSS and Javascript are shown inline, but should be extracted to their own static files:
<style type='text/css' media='screen'>
#ajaxLogin {
   margin: 15px 0px; padding: 0px;
   text-align: center;
   display: none;
   position: absolute;
}
#ajaxLogin .inner {
   width: 260px;
   margin:0px auto;
   text-align:left;
   padding:10px;
   border-top:1px dashed #499ede;
   border-bottom:1px dashed #499ede;
   background-color:#EEF;
}
#ajaxLogin .inner .fheader {
   padding:4px;margin:3px 0px 3px 0;color:#2e3741;font-size:14px;font-weight:bold;
}
#ajaxLogin .inner .cssform p{
   clear: left;
   margin: 0;
   padding: 5px 0 8px 0;
   padding-left: 105px;
   border-top: 1px dashed gray;
   margin-bottom: 10px;
   height: 1%;
}
#ajaxLogin .inner .cssform input[type='text']{
   width: 120px;
}
#ajaxLogin .inner .cssform label{
   font-weight: bold;
   float: left;
   margin-left: -105px;
   width: 100px;
}
#ajaxLogin .inner .login_message {color:red;}
#ajaxLogin .inner .text_ {width:120px;}
#ajaxLogin .inner .chk {height:12px;}
.errorMessage { color: red; }
</style>

<div id='ajaxLogin'> <div class='inner'> <div class='fheader'>Please Login..</div> <form action='${request.contextPath}/j_spring_security_check' method='POST' id='ajaxLoginForm' name='ajaxLoginForm' class='cssform'> <p> <label for='j_username'>Login ID</label> <input type='text' class='text_' name='j_username' value='' /> </p> <p> <label for='j_password'>Password</label> <input type='password' class='text_' name='j_password' value='' /> </p> <p> <label for='j_password'>Remember me</label> <input type='checkbox' class='chk' name='_spring_security_remember_me'> </p> <p> <a href='javascript:void(0)' onclick='authAjax(); return false;'>Login</a> </p> </form> <div style='display: none; text-align: left;' id='loginMessage'></div> </div> </div>

<script type='text/javascript'>

// center the form $('ajaxLogin').style.left = ((document.viewport.getWidth() - 300) / 2) + 'px'; $('ajaxLogin').style.top = ((document.viewport.getHeight() - 300) / 2) + 'px';

function showLogin() { $('ajaxLogin').style.display = 'block'; }

function onSuccessfulLogin(who) { Element.hide('ajaxLogin'); Element.update('loginLink', "Logged in as " + who + " (" + '<g:link controller='logout'>Logout</g:link>' + ")&nbsp;&nbsp;"); }

function authAjax() { Element.update('loginMessage', 'Sending request ...'); Element.show('loginMessage');

var form = document.ajaxLoginForm; var params = Form.serialize(form) + '&spring-security-redirect=/login/ajaxSuccess'; Form.disable(form); new Ajax.Request(form.action, { method: 'POST', postBody: params, onSuccess: function(response) { var responseText = response.responseText || '[]'; var json = responseText.evalJSON(); if (json.success) { onSuccessfulLogin(form.j_username.value); } else if (json.error) { Element.update('loginMessage', "<span class='errorMessage'>" + json.error + '</error>'); Form.enable(document.ajaxLoginForm); } else { Element.update('loginMessage', responseText); Form.enable(document.ajaxLoginForm); } } }); } </script>

The important aspects of this code are:
  • the form posts to the same url as the regular form, 'j_spring_security_check'; in fact the form is identical including the Remember Me checkbox, except that the submit button has been replaced with a hyperlink
  • the Javascript function that submits the form appends a parameter 'spring-security-redirect'. This is new as of Spring Security 2.0. If this parameter is present, it overrides the default behavior of sending a server-side redirect to the default post-login page, but we need to render JSON or XML as the Ajax response instead
  • details of logout are not shown, but this is achieved by redirecting the user to /j_spring_security_logout

So how does it work?

Most Ajax libraries (Prototype, JQuery, and Dojo as of v2.1) include an 'X-Requested-With' header that indicates that the request was made by XMLHttpRequest instead of being triggered by clicking a regular hyperlink or form submit button. The plugin uses this header to detect Ajax login requests, and uses subclasses of some of Spring Security's classes (org.codehaus.groovy.grails.plugins.springsecurity.WithAjaxAuthenticationProcessingFilterEntryPoint, org.codehaus.groovy.grails.plugins.springsecurity.GrailsAccessDeniedHandlerImpland org.codehaus.groovy.grails.plugins.springsecurity.GrailsAuthenticationProcessingFilter) to use different redirect urls for Ajax requests than regular requests. Instead of showing full pages, LoginController has JSON-generating methods ajaxSuccess(), deniedAjax(), and authfail() that generate JSON that the login Javascript code can use to appropriately display success or error messages.