In this tutorial, we will show you how to implementing custom user details for Grails 4 and Spring Security web applications. As a default, the Grails 4 Spring Security generated user domain or entity has only username and role fields that can get or show in the SecurityTagLib. So, we will add additional fields to display user info or details in the SecurityTagLib.
This tutorial divided into several steps:
- Step #1: Create a New Grails 4 Application
- Step #2: Add Spring Security Plugin
- Step #3: Generate User and Role Domain
- Step #4: Create a Custom User Details
- Step #5: Add a Secure Page
- Step #6: Run and Test Grails 4 Spring Security Application
The following tools, frameworks, and libraries are required for this tutorial:
- Grails 4 (the latest stable version)
- Spring Security Plugin
- Terminal or CMD
- Text Editor or IDE (We are using VSCode)
Let's get started with the main steps!
Step #1: Create a New Grails 4 Application
Before creating a new Grails 4 application, make sure that you have installed the latest Grails 4 version in your machine and can run the Grails 4 interactive console in the terminal or CMD. In the Mac terminal, we will use SDKman to install the latest Grails 4. To see the available Grails version in the SDKman, type this command.
sdk list grails
As you can see, there is some version of Grails 4 but we will install and use the stable version of Grails 4 by type this command.
sdk i grails 4.0.3
sdk u grails 4.0.3
Next, create a new Grails 4 application by type this command.
grails create-app securepage
Go to the newly created Grail 4 application. Then open the Grails project with your IDE or Text Editor.
cd securepage
code .
To working with Grails interactive console, type this command.
grails
If you see this error.
Error Error initializing classpath: Timeout of 120000 reached waiting for exclusive access to file: /Users/didin/.gradle/wrapper/dists/gradle-5.1.1-bin/90y9l8txxfw1s2o6ctiqeruwn/gradle-5.1.1-bin.zip (Use --stacktrace to see the full trace)
Just, remove .grails folder in the home directory then start again Grails interactive console.
rm -rf ~/.gradle
grails
For sanitation, start this Grails 4 application by type this command inside Grails interactive console.
run-app
Open your browser then go to "localhost:8080" and you should see this Grails page.
To stop or shutdown the running Grails server, type this command.
stop-app
Step #2: Add Spring Security Plugin
To add the Grails Spring Security Plugin, open and edit "build.gradle" in the root of the project folder. Then add this line to the dependencies.
dependencies {
...
compile 'org.grails.plugins:spring-security-core:4.0.0'
...
}
Compile this project to download and install the Spring Security dependency.
compile
Step #3: Generate User and Role Domain
Still, on Grails interactive console, type this command to generate the User and Role domain class.
s2-quickstart com.djamware.securepage User Role
That command generates the User.groovy, Role.groovy, and UserRole.groovy domain class. Also, an additional configuration file "grails-app/conf/application.groovy". The generated User.groovy look like this.
package com.djamware.securepage
import groovy.transform.EqualsAndHashCode
import groovy.transform.ToString
import grails.compiler.GrailsCompileStatic
@GrailsCompileStatic
@EqualsAndHashCode(includes='username')
@ToString(includes='username', includeNames=true, includePackage=false)
class User implements Serializable {
private static final long serialVersionUID = 1
String username
String password
boolean enabled = true
boolean accountExpired
boolean accountLocked
boolean passwordExpired
Set<Role> getAuthorities() {
(UserRole.findAllByUser(this) as List<UserRole>)*.role as Set<Role>
}
static constraints = {
password nullable: false, blank: false, password: true
username nullable: false, blank: false, unique: true
}
static mapping = {
password column: '`password`'
}
}
The generated Role.groovy looks like this.
package com.djamware.securepage
import groovy.transform.EqualsAndHashCode
import groovy.transform.ToString
import grails.compiler.GrailsCompileStatic
@GrailsCompileStatic
@EqualsAndHashCode(includes='authority')
@ToString(includes='authority', includeNames=true, includePackage=false)
class Role implements Serializable {
private static final long serialVersionUID = 1
String authority
static constraints = {
authority nullable: false, blank: false, unique: true
}
static mapping = {
cache true
}
}
The generated UserRole.groovy looks like this.
package com.djamware.securepage
import grails.gorm.DetachedCriteria
import groovy.transform.ToString
import org.codehaus.groovy.util.HashCodeHelper
import grails.compiler.GrailsCompileStatic
@GrailsCompileStatic
@ToString(cache=true, includeNames=true, includePackage=false)
class UserRole implements Serializable {
private static final long serialVersionUID = 1
User user
Role role
@Override
boolean equals(other) {
if (other instanceof UserRole) {
other.userId == user?.id && other.roleId == role?.id
}
}
@Override
int hashCode() {
int hashCode = HashCodeHelper.initHash()
if (user) {
hashCode = HashCodeHelper.updateHash(hashCode, user.id)
}
if (role) {
hashCode = HashCodeHelper.updateHash(hashCode, role.id)
}
hashCode
}
static UserRole get(long userId, long roleId) {
criteriaFor(userId, roleId).get()
}
static boolean exists(long userId, long roleId) {
criteriaFor(userId, roleId).count()
}
private static DetachedCriteria criteriaFor(long userId, long roleId) {
UserRole.where {
user == User.load(userId) &&
role == Role.load(roleId)
}
}
static UserRole create(User user, Role role, boolean flush = false) {
def instance = new UserRole(user: user, role: role)
instance.save(flush: flush)
instance
}
static boolean remove(User u, Role r) {
if (u != null && r != null) {
UserRole.where { user == u && role == r }.deleteAll()
}
}
static int removeAll(User u) {
u == null ? 0 : UserRole.where { user == u }.deleteAll() as int
}
static int removeAll(Role r) {
r == null ? 0 : UserRole.where { role == r }.deleteAll() as int
}
static constraints = {
user nullable: false
role nullable: false, validator: { Role r, UserRole ur ->
if (ur.user?.id) {
if (UserRole.exists(ur.user.id, r.id)) {
return ['userRole.exists']
}
}
}
}
static mapping = {
id composite: ['user', 'role']
version false
}
}
You don't have to worry about login page view, it automatically handles by the Spring Security.
Step #4: Create a Custom User Details
Now, we will add additional fields to the User domain class. Open and edit "grails-app/domain/com/djamware/securepage/User.groovy" then add this fields after another fields.
String fullname
String address
Also, add these constraints.
static constraints = {
...
fullname nullable: false, blank: false
address nullable: false, blank: false
}
Next, create a Groovy file "src/main/groovy/com/djamware/securepage/CustomUserDetails.groovy". On this file, add these package and imports.
package com.djamware.securepage
import grails.plugin.springsecurity.userdetails.GrailsUser
import org.springframework.security.core.GrantedAuthority
import org.springframework.security.core.userdetails.User
Then add the CustomUserDetails class that adds full name and address fields.
class CustomUserDetails extends GrailsUser {
final String fullname
final String address
CustomUserDetails(String username, String password, boolean enabled,
boolean accountNonExpired, boolean credentialsNonExpired,
boolean accountNonLocked,
Collection<GrantedAuthority> authorities,
long id, String fullname, String address) {
super(username, password, enabled, accountNonExpired,
credentialsNonExpired, accountNonLocked, authorities, id)
this.fullname = fullname
this.address= address
}
}
Next, generate a CustomUserDetailsService by type this command in Grails interactive console.
create-service com.djamware.securepage.CustomUserDetails
Open and edit "grails-app/services/com/djamware/securepage/CustomUserDetailsService.groovy" then add or replace these imports.
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
Remove the @Trannsactional annotation and add implements GrailsUserDetailsService.
class CustomUserDetailsService implements GrailsUserDetailsService {
...
}
Replace the class body with this.
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
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, user.address)
}
Inject that service to the "grails-app/conf/spring/resources.groovy".
import com.djamware.securepage.UserPasswordEncoderListener
import com.djamware.securepage.CustomUserDetailsService
// Place your Spring DSL code here
beans = {
userPasswordEncoderListener(UserPasswordEncoderListener)
userDetailsService(CustomUserDetailsService)
}
Step #5: Add a Secure Page
We will create a single secure page by creating a controller first. Type this command to create a controller from Grails interactive console.
create-controller com.djamware.securepage.Dashboard
Open and edit "grails-app/controllers/com/djamware/securepage/Dashboard.groovy" then add this import.
import grails.plugin.springsecurity.annotation.Secured
Add @Secured annotation before the class name.
@Secured('ROLE_ADMIN')
class DashboardController {
...
}
Next, create a new GSP file "grails-app/views/dashboard/index.gsp" then replace all GSP tags with this.
<!doctype html>
<html>
<head>
<meta name="layout" content="main"/>
<title>Secure Dashboard</title>
</head>
<body>
<div id="content" role="main">
<section class="row colset-2-its">
<sec:ifLoggedIn>
<h1>Welcome to the secure dashboard <sec:loggedInUserInfo field='fullname'/>!</h1>
<p><sec:loggedInUserInfo field='address'/></p>
<h2><g:link controller="logout">Logout</g:link></h2>
</sec:ifLoggedIn>
</section>
</div>
</body>
</html>
As a default logout method only working with the POST method only. To make it available using the GET method, open and edit "grails-app/conf/application.groovy" then add this line.
grails.plugin.springsecurity.logout.postOnly = false
Step #6: Run and Test Grails 4 Spring Security Application
For testing purposes, we will create a dummy user and role by adding this import to "grails-app/init/securepage/Bootstrap.groovy".
import com.djamware.securepage.*
Add these lines to the init method to create a new role, user, and userRole.
def init = { servletContext ->
def adminRole
Role.withTransaction { rl ->
adminRole = new Role(authority: 'ROLE_ADMIN').save(flush: true)
}
def testUser
User.withTransaction { us ->
testUser = new User(username: 'jacksparrow', password: 'password1', fullname: 'Jack Sparrow', address: '1600 Amphitheatre Parkway, Mountain View, California, United States').save(flush: true)
}
UserRole.create testUser, adminRole
UserRole.withTransaction { urole ->
UserRole.withSession {
it.flush()
it.clear()
}
}
}
Next, run again this Grails 4 application from the Grails interactive console.
run-app
And here's the Grails 4 Spring Security application look like.
That it's, the Grails 4 and Spring Security Custom User Details Example. You can get the full source code in our GitHub.
That just the basic. If you need more deep learning about Groovy and Grails you can take the following cheap course:
- Mastering Grails. A Comprehensive Grails Course.
- Groovy Scripting for Developers / Testers
- Introduction to JVM Languages Clojure, Kotlin, and Groovy
Thanks!