In this Grails 4 tutorial, we will show you how to build a secure Grails 4 application using Spring Security Core Plugin. We will add the login and register function to the Grails 4 application. The purpose of using the Spring Security plugin has simplified the integration of Spring Security Java (we have written this tutorial). The usage of this Grails 4 Spring Security plugin similar to Grails 2 or 3, but there’s a lot of updates on the Spring Security code and its dependencies to match the compatibilities.
The flow of this tutorial is very simple. We have a secure Product list that only accessible to the authorized user with ROLE_USER and Product CRUD for the user with ROLE_ADMIN. Any access to this Product resource will be redirected to the Login Page as default (if no authorized user). In the login page, there will be a link to the registration page that will register a new user.
The following tools, frameworks, libraries, and dependencies are required for this tutorial:
Before starting the main steps, make sure you have downloaded and installed the latest Grails 4. In Mac, we are using the SDKMan. For that, type this command in the Terminal to install SDKMan.
curl -s https://get.sdkman.io | bash
Follow all instructions that showed up during installation. Next, open the new Terminal window or tab then type this command.
source "$HOME/.sdkman/bin/sdkman-init.sh"
Now, you can install Grails 4 using SDKMan.
sdk install grails 4.0.1
Set that new Grails 4 as default. To check the Grails version, type this command.
grails -version
| Grails Version: 4.0.1
| JVM Version: 1.8.0_92
Same as previous Grails version, to create a new Grails application, simply type this command.
grails create-app com.djamware.gadgethouse
That command will create a new Grails 4 application with the name “gadgethouse” with the package name “com.djamware”. Next, go to the newly created project folder then enter the Grails 4 interactive console.
cd ./gadgethouse
grails
In the Grails interactive console, type this command to run this Grails application for the first time.
grails> run-app
Here’s the new Grails 4 look like.
Now, we will install and configure Grails Spring Security Core Plugin. For the database, we keep H2 in-memory database (You can change to other relational database configuration). To install the Grails Spring Security Core Plugin, open and edit build.gradle
then add this dependency in dependencies array.
compile "org.grails.plugins:spring-security-core:4.0.0.RC2"
Next, stop the running Grails application using this command in the Grails interactive console.
stop-app
Compile the Grails application, to install the Spring Security Core plugin.
compile
We will use the Grails s2-quickstart command to create User and Role domain class for authentication. Type this command in Grails interactive console.
s2-quickstart com.djamware User Role
That command will create User and Role domain class with the package name “com.djamware”. Next, open and edit grails-app/domain/com/djamware/Role.groovy
to add default field after the bracket closing when this domain calls.
String toString() {
authority
}
Next, create a domain class using regular Grails command for Product.
create-domain-class com.djamware.Product
Next, we need to create an additional field in the user domain class. For that, open and edit grails-app/domain/com/djamware/User.groovy
then add a fullName field after the password field.
String fullname
Also, add a constraint for that field.
static constraints = {
password nullable: false, blank: false, password: true
username nullable: false, blank: false, unique: true
fullname nullable: false, blank: false
}
Next, open and edit grails-app/domain/com/djamware/Product.groovy
then replace that domain class with these Groovy codes.
package com.djamware
class Product {
String prodCode
String prodName
String prodModel
String prodDesc
String prodImageUrl
String prodPrice
static constraints = {
prodCode nullable: false, blank: false
prodName nullable: false, blank: false
prodModel nullable: false, blank: false
prodDesc nullable: false, blank: false
prodImageUrl nullable: true
prodPrice nullable: false, blank: false
}
String toString() {
prodName
}
}
Because we have added a field in the previous User domain class, we need to create a custom UserDetails. Create a new Groovy file src/main/groovy/com/djamware/CustomUserDetails.groovy
then add these lines of Groovy codes that add full name field to the Grails UserDetails.
package com.djamware
import grails.plugin.springsecurity.userdetails.GrailsUser
import org.springframework.security.core.GrantedAuthority
class CustomUserDetails extends GrailsUser {
final String fullname
CustomUserDetails(String username, String password, boolean enabled,
boolean accountNonExpired, boolean credentialsNonExpired,
boolean accountNonLocked,
Collection<GrantedAuthority> authorities,
long id, String fullname) {
super(username, password, enabled, accountNonExpired,
credentialsNonExpired, accountNonLocked, authorities, id)
this.fullname = fullname
}
}
Next, type this command in the Grails interactive console to create a new Grails service.
grails> create-service com.djamware.CustomUserDetails
Open that file then replace all Groovy codes with these codes.
package com.djamware
import grails.plugin.springsecurity.SpringSecurityUtils
import grails.plugin.springsecurity.userdetails.GrailsUserDetailsService
import grails.plugin.springsecurity.userdetails.NoStackUsernameNotFoundException
import grails.gorm.transactions.Transactional
import org.springframework.security.core.authority.SimpleGrantedAuthority
import org.springframework.security.core.userdetails.UserDetails
import org.springframework.security.core.userdetails.UsernameNotFoundException
class CustomUserDetailsService implements GrailsUserDetailsService {
/**
* Some Spring Security classes (e.g. RoleHierarchyVoter) expect at least
* one role, so we give a user with no granted roles this one which gets
* past that restriction but doesn't grant anything.
*/
static final List NO_ROLES = [new SimpleGrantedAuthority(SpringSecurityUtils.NO_ROLE)]
UserDetails loadUserByUsername(String username, boolean loadRoles)
throws UsernameNotFoundException {
return loadUserByUsername(username)
}
@Transactional(readOnly=true, noRollbackFor=[IllegalArgumentException, UsernameNotFoundException])
UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = User.findByUsername(username)
if (!user) throw new NoStackUsernameNotFoundException()
def roles = user.authorities
// or if you are using role groups:
// def roles = user.authorities.collect { it.authorities }.flatten().unique()
def authorities = roles.collect {
new SimpleGrantedAuthority(it.authority)
}
return new CustomUserDetails(user.username, user.password, user.enabled,
!user.accountExpired, !user.passwordExpired,
!user.accountLocked, authorities ?: NO_ROLES, user.id,
user.fullname)
}
}
Next, register that new CustomUserDetailsService in the grails-app/conf/spring/resources.groovy
.
import com.djamware.UserPasswordEncoderListener
import com.djamware.CustomUserDetailsService
// Place your Spring DSL code here
beans = {
userPasswordEncoderListener(UserPasswordEncoderListener)
userDetailsService(CustomUserDetailsService)
}
We will customize the login page to make UI better and add a link to the Register page. For that, create a login
folder under views then create an auth.gsp
file in that folder. Open and edit grails-app/views/login/auth.gsp
then add these lines of GSP HTML tags.
<html>
<head>
<meta name="layout" content="${gspLayout ?: 'main'}"/>
<title><g:message code='springSecurity.login.title'/></title>
</head>
<body>
<div class="row">
<div class="col-sm-9 col-md-7 col-lg-5 mx-auto">
<div class="card card-signin my-5">
<div class="card-body">
<h5 class="card-title text-center">Please Login</h5>
<g:if test='${flash.message}'>
<div class="alert alert-danger" role="alert">${flash.message}</div>
</g:if>
<form class="form-signin" action="${postUrl ?: '/login/authenticate'}" method="POST" id="loginForm" autocomplete="off">
<div class="form-group">
<label for="username">Username</label>
<input type="text" class="form-control" name="${usernameParameter ?: 'username'}" id="username" autocapitalize="none"/>
</div>
<div class="form-group">
<label for="password">Password</label>
<input type="password" class="form-control" name="${passwordParameter ?: 'password'}" id="password"/>
<i id="passwordToggler" title="toggle password display" onclick="passwordDisplayToggle()">👁</i>
</div>
<div class="form-group form-check">
<label class="form-check-label">
<input type="checkbox" class="form-check-input" name="${rememberMeParameter ?: 'remember-me'}" id="remember_me" <g:if test='${hasCookie}'>checked="checked"</g:if>/> Remember me
</label>
</div>
<button id="submit" class="btn btn-lg btn-primary btn-block text-uppercase" type="submit">Sign in</button>
<hr class="my-4">
<p>Don't have an account? <g:link controller="register">Register</g:link></p>
</form>
</div>
</div>
</div>
</div>
<script type="text/javascript">
document.addEventListener("DOMContentLoaded", function(event) {
document.forms['loginForm'].elements['username'].focus();
});
function passwordDisplayToggle() {
var toggleEl = document.getElementById("passwordToggler");
var eyeIcon = '\u{1F441}';
var xIcon = '\u{2715}';
var passEl = document.getElementById("password");
if (passEl.type === "password") {
toggleEl.innerHTML = xIcon;
passEl.type = "text";
} else {
toggleEl.innerHTML = eyeIcon;
passEl.type = "password";
}
}
</script>
</body>
</html>
Next, we will make this login page as default or homepage when the application opens in the browser. For that, open and edit grails-app/controllers/UrlMappings.groovy
then replace this line.
"/"(view: "index")
With this line.
"/"(controller:'login', action:'auth')
Now, we have to implement POST logout to the Navbar. This logout button active when the user logged in successfully along with user info. For that, modify grails-app/views/layout/main.gsp
then replace all GSP HTML tags with these.
<!doctype html>
<html lang="en" class="no-js">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
<meta http-equiv="X-UA-Compatible" content="IE=edge"/>
<title>
<g:layoutTitle default="Grails"/>
</title>
<meta name="viewport" content="width=device-width, initial-scale=1"/>
<asset:link rel="icon" href="favicon.ico" type="image/x-ico"/>
<asset:stylesheet src="application.css"/>
<g:layoutHead/>
</head>
<body>
<nav class="navbar navbar-expand-lg navbar-dark navbar-static-top" role="navigation">
<a class="navbar-brand" href="/#"><asset:image src="grails.svg" alt="Grails Logo"/></a>
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarContent" aria-controls="navbarContent" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" aria-expanded="false" style="height: 0.8px;" id="navbarContent">
<ul class="nav navbar-nav ml-auto">
<g:pageProperty name="page.nav"/>
<sec:ifLoggedIn>
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" href="#" id="navbardrop" data-toggle="dropdown">
<sec:loggedInUserInfo field='fullname'/>
</a>
<div class="dropdown-menu navbar-dark">
<g:form controller="logout">
<g:submitButton class="dropdown-item navbar-dark color-light" name="Submit" value="Logout" style="color:gray" />
</g:form>
</div>
</li>
</sec:ifLoggedIn>
</ul>
</div>
</nav>
<div class="container">
<g:layoutBody/>
</div>
<div class="footer row" role="contentinfo">
<div class="col">
<a href="http://guides.grails.org" target="_blank">
<asset:image src="advancedgrails.svg" alt="Grails Guides" class="float-left"/>
</a>
<strong class="centered"><a href="http://guides.grails.org" target="_blank">Grails Guides</a></strong>
<p>Building your first Grails app? Looking to add security, or create a Single-Page-App? Check out the <a href="http://guides.grails.org" target="_blank">Grails Guides</a> for step-by-step tutorials.</p>
</div>
<div class="col">
<a href="http://docs.grails.org" target="_blank">
<asset:image src="documentation.svg" alt="Grails Documentation" class="float-left"/>
</a>
<strong class="centered"><a href="http://docs.grails.org" target="_blank">Documentation</a></strong>
<p>Ready to dig in? You can find in-depth documentation for all the features of Grails in the <a href="http://docs.grails.org" target="_blank">User Guide</a>.</p>
</div>
<div class="col">
<a href="https://grails-slack.cfapps.io" target="_blank">
<asset:image src="slack.svg" alt="Grails Slack" class="float-left"/>
</a>
<strong class="centered"><a href="https://grails-slack.cfapps.io" target="_blank">Join the Community</a></strong>
<p>Get feedback and share your experience with other Grails developers in the community <a href="https://grails-slack.cfapps.io" target="_blank">Slack channel</a>.</p>
</div>
</div>
<div id="spinner" class="spinner" style="display:none;">
<g:message code="spinner.alt" default="Loading…"/>
</div>
<asset:javascript src="application.js"/>
</body>
</html>
As you see, there are built in Grails Spring Security TagLib sec:ifLoggedIn and <sec:loggedInUserInfo field=‘fullname’/>. The <sec:loggedInUserInfo field=‘fullname’/> only working when you implementing CustomUserDetailsService.
Back to the Grails interactive console to create a controller for the Register page.
grails> create-controller com.djamware.Register
Open and edit that Groovy file then replace all Groovy codes with these codes that have 2 methods of Register landing page and register action.
package com.djamware
import grails.validation.ValidationException
import grails.gorm.transactions.Transactional
import grails.plugin.springsecurity.annotation.Secured
import com.djamware.User
import com.djamware.Role
import com.djamware.UserRole
@Transactional
@Secured('permitAll')
class RegisterController {
static allowedMethods = [register: "POST"]
def index() { }
def register() {
if(!params.password.equals(params.repassword)) {
flash.message = "Password and Re-Password not match"
redirect action: "index"
return
} else {
try {
def user = User.findByUsername(params.username)?: new User(username: params.username, password: params.password, fullname: params.fullname).save()
def role = Role.get(params.role.id)
if(user && role) {
UserRole.create user, role
UserRole.withSession {
it.flush()
it.clear()
}
flash.message = "You have registered successfully. Please login."
redirect controller: "login", action: "auth"
} else {
flash.message = "Register failed"
render view: "index"
return
}
} catch (ValidationException e) {
flash.message = "Register Failed"
redirect action: "index"
return
}
}
}
}
Next, add the index.gsp
inside grails-app/views/register/
. Open and edit that file then add these lines of GSP HTML tags.
<html>
<head>
<meta name="layout" content="${gspLayout ?: 'main'}"/>
<title>Register</title>
</head>
<body>
<div class="row">
<div class="col-sm-9 col-md-7 col-lg-5 mx-auto">
<div class="card card-signin my-5">
<div class="card-body">
<h5 class="card-title text-center">Register Here</h5>
<g:if test='${flash.message}'>
<div class="alert alert-danger" role="alert">${flash.message}</div>
</g:if>
<form class="form-signin" action="register" method="POST" id="loginForm" autocomplete="off">
<div class="form-group">
<label for="role">Role</label>
<g:select class="form-control" name="role.id"
from="${com.djamware.Role.list()}"
optionKey="id" />
</div>
<div class="form-group">
<label for="username">Username</label>
<input type="text" placeholder="Your username" class="form-control" name="username" id="username" autocapitalize="none"/>
</div>
<div class="form-group">
<label for="password">Password</label>
<input type="password" placeholder="Your password" class="form-control" name="password" id="password"/>
</div>
<div class="form-group">
<label for="password">Re-Enter Password</label>
<input type="password" placeholder="Re-enter password" class="form-control" name="repassword" id="repassword"/>
</div>
<div class="form-group">
<label for="username">Full Name</label>
<input type="text" placeholder="Your full name" class="form-control" name="fullname" id="fullname" autocapitalize="none"/>
</div>
<button id="submit" class="btn btn-lg btn-primary btn-block text-uppercase" type="submit">Register</button>
<hr class="my-4">
<p>Already have an account? <g:link controller="login" action="auth">Login</g:link></p>
</form>
</div>
</div>
</div>
</div>
<script type="text/javascript">
document.addEventListener("DOMContentLoaded", function(event) {
document.forms['loginForm'].elements['username'].focus();
});
</script>
</body>
</html>
Now, we will make Product CRUD scaffolding and make them secured and accessible to ROLE_USER and ROLE_ADMIN. To create CRUD scaffolding, simply run this command inside Grails interactive console.
grails>generate-all com.djamware.Product
That command will generate controller, service, and view for a Product domain class. Next, open and edit grails-app/controllers/ProductController.groovy
then add the Secure annotation like these.
package com.djamware
import grails.validation.ValidationException
import static org.springframework.http.HttpStatus.*
import grails.plugin.springsecurity.annotation.Secured
class ProductController {
ProductService productService
static allowedMethods = [save: "POST", update: "PUT", delete: "DELETE"]
@Secured(['ROLE_ADMIN', 'ROLE_USER'])
def index(Integer max) {
params.max = Math.min(max ?: 10, 100)
respond productService.list(params), model:[productCount: productService.count()]
}
@Secured(['ROLE_ADMIN', 'ROLE_USER'])
def show(Long id) {
respond productService.get(id)
}
@Secured('ROLE_ADMIN')
def create() {
respond new Product(params)
}
@Secured('ROLE_ADMIN')
def save(Product product) {
if (product == null) {
notFound()
return
}
try {
productService.save(product)
} catch (ValidationException e) {
respond product.errors, view:'create'
return
}
request.withFormat {
form multipartForm {
flash.message = message(code: 'default.created.message', args: [message(code: 'product.label', default: 'Product'), product.id])
redirect product
}
'*' { respond product, [status: CREATED] }
}
}
@Secured('ROLE_ADMIN')
def edit(Long id) {
respond productService.get(id)
}
@Secured('ROLE_ADMIN')
def update(Product product) {
if (product == null) {
notFound()
return
}
try {
productService.save(product)
} catch (ValidationException e) {
respond product.errors, view:'edit'
return
}
request.withFormat {
form multipartForm {
flash.message = message(code: 'default.updated.message', args: [message(code: 'product.label', default: 'Product'), product.id])
redirect product
}
'*'{ respond product, [status: OK] }
}
}
@Secured('ROLE_ADMIN')
def delete(Long id) {
if (id == null) {
notFound()
return
}
productService.delete(id)
request.withFormat {
form multipartForm {
flash.message = message(code: 'default.deleted.message', args: [message(code: 'product.label', default: 'Product'), id])
redirect action:"index", method:"GET"
}
'*'{ render status: NO_CONTENT }
}
}
protected void notFound() {
request.withFormat {
form multipartForm {
flash.message = message(code: 'default.not.found.message', args: [message(code: 'product.label', default: 'Product'), params.id])
redirect action: "index", method: "GET"
}
'*'{ render status: NOT_FOUND }
}
}
}
Next, we will make Product controller as a default landing page after succeful login. For that, open and edit grails-app/conf/application.groovy
then add this configuration.
grails.plugin.springsecurity.successHandler.defaultTargetUrl = '/product'
Now, we will run and test the Grails 4 Spring Security Core application. In the Grails interactive console run this command.
grails>run-app
Open the browser then go to http://localhost:8080
and you will see this page.
That it’s, the Grails 4 Tutorial: Spring Security Core Login Example. You can find the full working source code in our GitHub.
Thanks!
#Grails #spring-security #security #web-development #spring-boot