I’ve been wanting to learn Grails for sometime and finally had the time to try out this web application framework. Grails is a very impressive framework, it is really about productivity and simplicity. Some of the benefits are scaffolding, testing framework, easy to create plugin and seamless integration with Spring and Hibernate. As part of my first attempt, I am customizing the Spring Security Login using ExtJS in a simple Grails application. The focus here is on the integration with Spring Security and ExtJS rather than Grails. I am using SpringSource Tool Suite (STS 2.7.1) to develop the demo application. The application is to display Quotes randomly and ability to maintain them. Let’s begin the integration and customization steps.
Step 1: Install the Spring Security Plugin
Spring Security is not built-in Grails, to begin the integration execute the following Grails command.
install-plugin spring-security-core
Step 2: Generate Domain Classes
This step will create basic domain classes needed to store user information and the controller that handle the authentication. After executing the command below you will see several files being added to your project, such as LoginController, auth.gsp, denied.gsp, User, Role, UserRole, RequestMap. The domain class name for user and role is up to you to define and if you have an existing user domain, you must decide to integrate it (possibly by extending your domain class to the generated user class). Just a note that the RequestMap is an option and will look into it at later step.
s2-quickstart org.xaab.qotd User Role RequestMap
Step 3: Define URL mapping for login and logout
In order the make the login and logout controller reachable, the URL mappings must be added into UrlMapping.groovy
class UrlMappings {
static mappings = {
...
"/login/$action?"(controller: "login")
"/logout/$action?"(controller: "logout")
}
}
Step 4: Add Access Control to the Controller
The need for this step depends on how you wants to control the access. You can use @Secured annotation to limit access to certain part of the application. In this example, I am not using annotation to limit the access, instead I define dynamics request maps which is shown in the Step 5. The following code is just an example of how @Secured can be used in the controller.
package org.example
import grails.plugins.springsecurity.Secured
class PostController {
...
@Secured(['ROLE_USER'])
def followAjax = { ... }
@Secured(['ROLE_USER', 'IS_AUTHENTICATED_FULLY'])
def addPostAjax = { ... }
def global = { ... }
@Secured(['ROLE_USER'])
def timeline = { ... }
@Secured(['IS_AUTHENTICATED_REMEMBERED'])
def personal = { ... }
}
Step 5: Add Dynamic Request Maps
The request maps class was generated in Step 2 and used to limit access by defining the access control at the URL. Here I create the request map in BootStrap.groovy for demo purpose, in your actual application this may be defined in configuration file or database. I don’t think you should choose between using annotation and request map. You can strike balance by implementing both to handle general and fine-grain access control.
import org.xaab.qotd.*
class BootStrap {
def springSecurityService
def init = { servletContext ->
...
new Requestmap(url: '/**', configAttribute: 'IS_AUTHENTICATED_FULLY').save()
new Requestmap(url: '/quote/**', configAttribute: 'ROLE_ADMIN,IS_AUTHENTICATED_FULLY').save()
new Requestmap(url: '/js/**', configAttribute: 'IS_AUTHENTICATED_ANONYMOUSLY').save()
new Requestmap(url: '/css/**', configAttribute: 'IS_AUTHENTICATED_ANONYMOUSLY').save()
new Requestmap(url: '/images/**', configAttribute: 'IS_AUTHENTICATED_ANONYMOUSLY').save()
new Requestmap(url: '/ext-4.0.2/**', configAttribute: 'IS_AUTHENTICATED_ANONYMOUSLY').save()
new Requestmap(url: '/quote/random', configAttribute: 'IS_AUTHENTICATED_ANONYMOUSLY').save()
new Requestmap(url: '/login/**', configAttribute: 'IS_AUTHENTICATED_ANONYMOUSLY').save()
new Requestmap(url: '/logout/**', configAttribute: 'IS_AUTHENTICATED_ANONYMOUSLY').save()
...
}
def destroy = {
}
}
Step 6: Amend related GSP to add login and logout link
Amend related GSP (random.gsp) to ensure you have the login and logout link. Since I used scaffolding, I must install the templates and ensure the GSP templates are modified. Once scaffolding templates are installed, you will see the folders /src/templates/scaffolding/**. The files I modified in this sample application are create.gsp, edit.gsp, list.gsp & show.gsp.
install-templates
...</pre>
<div class="nav"><span class="menuButton">Next Quote</span> <span class="menuButton">Admin</span> <span class="menuButton">Login</span> <span class="menuButton"> (Logout)</span></div>
<pre>...
Step 7: Customize the auth.gsp to Add ExtJs Login Form
By now the integration is completed and if you used the test data in Step 10, you will be able to see the default login page. In order to customize it, copy the ExtJS source into /web-app and in this sample application I am using ExtJS 4.0.2. Start the customization by modifying the auth.gsp as shown below. Do take note on the naming of the fields as it is the convention used in Spring Security.
Login<script type="text/javascript" src="${resource(dir:'ext-4.0.2',file:'ext-all.js')}"></script><script type="text/javascript">// <![CDATA[
var defLoginUrl = '${postUrl}';
var homeUrl = '${createLink(uri: "/quote/random")}';
// ]]></script>
<script type="text/javascript">// <![CDATA[
Ext.onReady(function(){
Ext.QuickTips.init();
var loginForm = Ext.create('Ext.form.Panel',{
url: defLoginUrl,
title: 'Login',
renderTo: 'login',
frame: true,
cls: 'my-form-class',
width: 350,
items: [{
xtype: 'textfield',
fieldLabel: 'Login',
name: 'j_username'
},{
xtype: 'textfield',
inputType: 'password',
fieldLabel: 'Password',
name: 'j_password'
}, {
xtype: 'checkbox',
fieldLabel: 'Remember Me?',
name: '_spring_security_remember_me',
checked: false
}],
buttons: [{
id: 'lf.btn.login',
text: 'Login',
handler: function() {
fnLoginForm(loginForm);
}
},{
id: 'lf.btn.reset',
text: 'Reset',
handler: function() {
fnResetForm(loginForm);
}
}]
});
});
function fnLoginForm(theForm)
{
theForm.getForm().submit({
success: function(form, action) {
Ext.Msg.alert('Success', 'Login Successful!', function(btn, text) {
if (btn == 'ok') {
window.location = homeUrl; //optionally this can be part of the data return by the server.
}
});
},
failure: function(form, action) {
Ext.Msg.alert('Warning', action.result.error);
}
});
} //end fnLoginForm
function fnResetForm(theForm)
{
theForm.getForm().reset();
} //end fnResetForm
// ]]></script>
Step 8: Configure Authentication Success and Failure Handler
The ExtJS login form will perform HTTP post to submit the login data however it expects JSON data as response. The default method in the LoginController will not be able to handle this, thus you must define your own handler. Add your handler into Config.groovy as shown below. The handlers are implemented in LoginController in the next step.
grails.plugins.springsecurity.successHandler.defaultTargetUrl = '/login/authSucccessExtJs'
grails.plugins.springsecurity.successHandler.alwaysUseDefault = true
grails.plugins.springsecurity.failureHandler.defaultFailureUrl = '/login/authFailExtJs?login_error=1'
Step 9: Implement the Handler in LoginController
After adding the handler, I suggest you remove or comment out the other methods to ensure they are not accessible.
import grails.converters.JSON
import javax.servlet.http.HttpServletResponse
import org.codehaus.groovy.grails.plugins.springsecurity.SpringSecurityUtils
import org.springframework.security.authentication.AccountExpiredException
import org.springframework.security.authentication.CredentialsExpiredException
import org.springframework.security.authentication.DisabledException
import org.springframework.security.authentication.LockedException
import org.springframework.security.core.context.SecurityContextHolder as SCH
import org.springframework.security.web.WebAttributes
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter
class LoginController {
/**
* Dependency injection for the authenticationTrustResolver.
*/
def authenticationTrustResolver
/**
* Dependency injection for the springSecurityService.
*/
def springSecurityService
...
/**
* The ExtJS Authentication success handler
*/
def authSucccessExtJs = {
render([success: true, username: springSecurityService.authentication.name] as JSON)
}
/**
* The ExtJS Authentication failure handler
*/
def authFailExtJs = {
def username = session[UsernamePasswordAuthenticationFilter.SPRING_SECURITY_LAST_USERNAME_KEY]
String msg = ''
def exception = session[WebAttributes.AUTHENTICATION_EXCEPTION]
if (exception) {
if (exception instanceof AccountExpiredException) {
msg = SpringSecurityUtils.securityConfig.errors.login.expired
}
else if (exception instanceof CredentialsExpiredException) {
msg = SpringSecurityUtils.securityConfig.errors.login.passwordExpired
}
else if (exception instanceof DisabledException) {
msg = SpringSecurityUtils.securityConfig.errors.login.disabled
}
else if (exception instanceof LockedException) {
msg = SpringSecurityUtils.securityConfig.errors.login.locked
}
else {
msg = SpringSecurityUtils.securityConfig.errors.login.fail
}
}
render([success: false, error: msg] as JSON)
}
}
Step 10: Setup Test Data
So, you have integrated your Grails application, customized the login form using ExtJS. Now, setup the test data in BootStrap.groovy and launch the application (http://localhost:8080/grails-qotd-extjs).
import org.xaab.qotd.*
class BootStrap {
def springSecurityService
def init = { servletContext ->
new Quote(author: "Unknown", content: "If you think sunshine brings you happiness, then you haven’t danced in the rain.").save()
new Quote(author: "Lao Tzu", content: "An ant on the move does more than a dozing ox.").save()
new Quote(author: "Tony Robbins", content: "It not knowing what to do, it’s doing what you know.").save()
new Quote(author: "Bruce Lee", content: "Simplicity is the key to brilliance.").save()
...
def userRole = Role.findByAuthority('ROLE_USER') ?: new Role(authority: 'ROLE_USER').save(failOnError: true)
def adminRole = Role.findByAuthority('ROLE_ADMIN') ?: new Role(authority: 'ROLE_ADMIN').save(failOnError: true)
def adminUser = User.findByUsername('admin') ?: new User(
username: 'admin',
password: springSecurityService.encodePassword('admin'),
enabled: true).save(failOnError: true)
if (!adminUser.authorities.contains(adminRole)) {
UserRole.create(adminUser, adminRole)
}
}
def destroy = {
}
}
Hope this tutorial helps and happy coding!
download source
References: