Grails Spring Security Core Guide | WebDesigner.la

Before the Grails Spring Security Core plugin matured, wiring authentication and fine-grained authorization into a Groovy on Grails enterprise web application meant grappling with hundreds of lines of complex XML in applicationContext-security.xml. The introduction of spring-security-core fundamentally transformed security architecture across JVM applications, marrying the battle-tested power of Spring Security's servlet filters with the elegance of Groovy GORM domain classes and AST transformations.

Grails Spring Security Core Architecture Diagram
End-to-End Grails Spring Security Core Architecture: Request interception via FilterChainProxy, DaoAuthenticationProvider validation, GORM domain resolution, and controller annotation enforcement.

The Shift from Spring XML to Groovy Convention

In traditional Spring MVC architectures, securing HTTP endpoints required explicit declaration of security filter chains, custom authentication entry points, voting-based access decision managers, and verbose user details services. For teams building high-throughput portals, recruitment directories, and e-commerce checkouts on the JVM, this manual plumbing introduced unnecessary friction and subtle configuration vulnerabilities.

The Grails Spring Security Core plugin bridged this divide by adopting convention-over-configuration principles. Instead of maintaining disconnected XML beans, developers could generate authentic GORM domain entities representing users, authorities, and permissions, while configuring security behavior directly within grails-app/conf/Config.groovy.

Bootstrap & Domain Modeling: The s2-quickstart Command

The standard entry point for integrating the plugin into a Grails application begins with the s2-quickstart script. Executing this command creates the canonical domain triad that underpins user identity and role assignment:

grails s2-quickstart com.webdesigner.security User Role

This automated generator produces three foundational classes within grails-app/domain/:

1. The User Entity (Identity & Credentials)

The User class captures principal credentials, account validity flags, and credential expiration timestamps. Crucially, password hashing is handled transparently during GORM lifecycle events:

package com.webdesigner.security

class User implements Serializable {
    private static final long serialVersionUID = 1

    transient springSecurityService

    String username
    String password
    String email
    boolean enabled = true
    boolean accountExpired = false
    boolean accountLocked = false
    boolean passwordExpired = false

    static constraints = {
        username blank: false, unique: true
        password blank: false
        email email: true, blank: false, unique: true
    }

    static mapping = {
        table 'sec_user'
        password column: '`password`'
    }

    Set<Role> getAuthorities() {
        UserRole.findAllByUser(this)*.role as Set<Role>
    }

    def beforeInsert() {
        encodePassword()
    }

    def beforeUpdate() {
        if (isDirty('password')) {
            encodePassword()
        }
    }

    protected void encodePassword() {
        password = springSecurityService?.passwordEncoder ? 
            springSecurityService.encodePassword(password) : password
    }
}

2. The Role Entity (Granted Authorities)

Roles represent system-level entitlements adhering to the standard ROLE_ prefix convention expected by Spring's RoleVoter:

package com.webdesigner.security

class Role implements Serializable {
    private static final long serialVersionUID = 1

    String authority

    static mapping = {
        table 'sec_role'
        cache true
    }

    static constraints = {
        authority blank: false, unique: true
    }
}

3. The UserRole Join Class (Association Optimization)

Rather than relying on Hibernate's unmanaged hasMany join tables, the plugin implements a dedicated mapping class with composite identity constraints. This architecture eliminates cartesian product fetch traps and delivers deterministic cascade deletion:

package com.webdesigner.security

import org.apache.commons.lang.builder.HashCodeBuilder

class UserRole implements Serializable {
    private static final long serialVersionUID = 1

    User user
    Role role

    static mapping = {
        table 'sec_user_role'
        id composite: ['user', 'role']
        version false
    }

    static UserRole create(User user, Role role, boolean flush = false) {
        def instance = new UserRole(user: user, role: role)
        instance.save(flush: flush, insert: true)
        instance
    }

    static boolean remove(User u, Role r, boolean flush = false) {
        if (u == null || r == null) return false
        UserRole.where { user == u && role == r }.deleteAll()
        if (flush) { UserRole.withSession { it.flush() } }
        true
    }

    @Override
    boolean equals(other) {
        if (!(other instanceof UserRole)) return false
        other.user?.id == user?.id && other.role?.id == role?.id
    }

    @Override
    int hashCode() {
        def builder = new HashCodeBuilder()
        if (user) builder.append(user.id)
        if (role) builder.append(role.id)
        builder.toHashCode()
    }
}

The Ingress Pipeline: FilterChainProxy Sequence

When an incoming HTTP request reaches the servlet container (e.g. Apache Tomcat or Jetty), it is intercepted by Spring's DelegatingFilterProxy, which forwards the request directly into the plugin's internal FilterChainProxy:

  1. SecurityContextPersistenceFilter: Restores the existing SecurityContext from the HTTP session (or establishes a fresh context for anonymous requests).
  2. LogoutFilter: Monitors requests destined for /j_spring_security_logout, terminates the active session, invalidates cookies, and redirects to the landing view.
  3. UsernamePasswordAuthenticationFilter: Intercepts POST submissions to /j_spring_security_check, constructs an unauthenticated UsernamePasswordAuthenticationToken, and delegates credentials to the configured AuthenticationManager.
  4. ExceptionTranslationFilter: Catches downstream AuthenticationException and AccessDeniedException instances, redirecting unauthenticated users to the login form or returning 403 Forbidden headers for unprivileged requests.
  5. FilterSecurityInterceptor: Evaluates whether the currently authenticated principal possesses sufficient authorities to invoke the requested controller action or static URL mapping.

Access Control Paradigms: Annotations vs. Intercept URL Maps

The plugin supports three distinct patterns for declaring access control rules. In enterprise production environments, selecting the appropriate pattern is essential for long-term maintainability.

Approach A: Controller & Action Annotations (@Secured)

By specifying grails.plugin.springsecurity.securityConfigType = "Annotation" in Config.groovy, access control rules reside directly alongside controller implementation code. This approach provides immediate clarity to developers inspecting the endpoint:

package com.webdesigner.portal

import grails.plugin.springsecurity.annotation.Secured

@Secured(['ROLE_USER', 'ROLE_ADMIN'])
class ProjectController {

    def springSecurityService

    def index() {
        def currentUser = springSecurityService.currentUser
        [projects: Project.findAllByOwner(currentUser)]
    }

    @Secured(['ROLE_ADMIN'])
    def delete(Long id) {
        def project = Project.get(id)
        project?.delete(flush: true)
        redirect action: 'index'
    }

    @Secured(['permitAll'])
    def preview(Long id) {
        [project: Project.get(id)]
    }
}

Approach B: Centralized interceptUrlMap

For large-scale architectures requiring strict oversight by security leads, defining a centralized pattern map in Config.groovy guarantees that entire URL subtrees remain protected without relying on individual controller annotations:

grails.plugin.springsecurity.securityConfigType = "InterceptUrlMap"
grails.plugin.springsecurity.interceptUrlMap = [
    '/admin/**':        ['ROLE_ADMIN'],
    '/api/v1/secure/**':['ROLE_CLIENT', 'ROLE_ADMIN'],
    '/portfolio/**':    ['permitAll'],
    '/assets/**':       ['permitAll'],
    '/**':              ['isAuthenticated()']
]

Programmatic Context & User Resolution

In service classes and background workers, accessing the active user principal is achieved through dependency injection of springSecurityService:

package com.webdesigner.services

class AuditLogService {
    def springSecurityService

    void recordEvent(String actionName, String details) {
        def principal = springSecurityService.principal
        String username = springSecurityService.isLoggedIn() ? principal.username : 'anonymous'

        new AuditLog(
            username: username,
            action: actionName,
            details: details,
            timestamp: new Date()
        ).save(flush: true)
    }
}

Integration with grails-datasources-plugin

In multi-tenant or clustered deployments utilizing the Grails DataSources plugin, security tables (sec_user, sec_role, sec_user_role) can be isolated into an administrative master database while tenant-specific transactional data resides across distributed read replicas. Configuring datasource routing on the User and Role domains ensures security credentials remain segregated and encrypted at rest.

Key Architectural Takeaways

  • Deterministic Join Classes: Always prefer a composite-key UserRole implementation over unmanaged Hibernate collections to avoid N+1 query overhead during authentication.
  • Bcrypt Password Hashing: Configure grails.plugin.springsecurity.password.algorithm = 'bcrypt' with an appropriate log rounds work factor (typically 12) to resist offline dictionary attacks.
  • Stateless API Endpoints: For modern single-page applications or mobile backends, combine spring-security-core with stateless token authentication or OAuth2 providers, bypassing HTTP session creation while preserving Spring's declarative access rules.