diff --git a/docs/src/docs/asciidoc/guides/grails3.adoc b/docs/src/docs/asciidoc/guides/grails3.adoc new file mode 100644 index 00000000..1485dc56 --- /dev/null +++ b/docs/src/docs/asciidoc/guides/grails3.adoc @@ -0,0 +1,130 @@ += Spring Session - Grails +Eric Helgeson +:toc: + +This guide describes how to use Spring Session to transparently leverage Redis to back a web application's `HttpSession` when using Grails 3.1 + +NOTE: Grails 3.1 is based off spring boot 1.3 so much of the advanced configuration and options can be found in the boot docs as well. + +NOTE: The completed guide can be found in the <>. + +== Updating Dependencies +Before you use Spring Session, you must ensure to update your dependencies. +We assume you are working with a working Grails 3.1 web profile. +Add the following dependencies: + +.build.gradle +[source,groovy] +[subs="verbatim,attributes"] +---- +dependencies { + compile 'org.springframework.boot:spring-boot-starter-redis' + compile 'org.springframework.session:spring-session:{spring-session-version}' +} +---- + +ifeval::["{version-snapshot}" == "true"] +Since We are using a SNAPSHOT version, we need to ensure to add the Spring Snapshot Maven Repository. +Ensure you have the following in your pom.xml: + +.build.gradle +[source,groovy] +---- +repositories { + maven { + url 'https://repo.spring.io/libs-snapshot' + } +} +---- +endif::[] + +ifeval::["{version-milestone}" == "true"] +Since We are using a Milestone version, we need to ensure to add the Spring Milestone Maven Repository. +Ensure you have the following in your pom.xml: + +.build.gradle +[source,groovy] +---- +repositories { + maven { + url 'https://repo.spring.io/libs-milestone' + } +} +---- +endif::[] + +[[grails3-redis-configuration]] +== Configuring the Redis Connection + +Spring Boot automatically creates a `RedisConnectionFactory` that connects Spring Session to a Redis Server on localhost on port 6379 (default port). +In a production environment you need to ensure to update your configuration to point to your Redis server. +For example, you can include the following in your *application.yml* + +.grails-app/conf/application.yml +[source,yml] +---- +--- +spring: + redis: + host: localhost + password: secret + port: 6397 +---- + +For more information, refer to http://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/#boot-features-connecting-to-redis[Connecting to Redis] portion of the Spring Boot documentation. + +[[grails3-sample]] +== Grails 3 Sample Application + +The Grails 3 Sample Application demonstrates how to use Spring Session to transparently leverage Redis to back a web application's `HttpSession` when using Grails. + +[[grails3-running]] +=== Running the Grails 3 Sample Application + +You can run the sample by obtaining the {download-url}[source code] and invoking the following command: + +[NOTE] +==== +For the sample to work, you must http://redis.io/download[install Redis 2.8+] on localhost and run it with the default port (6379). +Alternatively, you can update the `JedisConnectionFactory` to point to a Redis server. +==== + +---- +$ ./gradlew :samples:grails3:bootRun +---- + +You should now be able to access the application at http://localhost:8080/test/index + +[[grails3-explore]] +=== Exploring the security Sample Application + +Try using the application. Enter the following to log in: + +* **Username** _user_ +* **Password** _password_ + +Now click the **Login** button. +You should now see a message indicating your are logged in with the user entered previously. +The user's information is stored in Redis rather than Tomcat's `HttpSession` implementation. + +[[grails3-how]] +=== How does it work? + +Instead of using Tomcat's `HttpSession`, we are actually persisting the values in Redis. +Spring Session replaces the `HttpSession` with an implementation that is backed by Redis. +When Spring Security's `SecurityContextPersistenceFilter` saves the `SecurityContext` to the `HttpSession` it is then persisted into Redis. + +When a new `HttpSession` is created, Spring Session creates a cookie named SESSION in your browser that contains the id of your session. +Go ahead and view the cookies (click for help with https://developer.chrome.com/devtools/docs/resources#cookies[Chrome] or https://getfirebug.com/wiki/index.php/Cookies_Panel#Cookies_List[Firefox]). + +If you like, you can easily remove the session using redis-cli. For example, on a Linux based system you can type: + + $ redis-cli keys '*' | xargs redis-cli del + +TIP: The Redis documentation has instructions for http://redis.io/topics/quickstart[installing redis-cli]. + +Alternatively, you can also delete the explicit key. Enter the following into your terminal ensuring to replace `7e8383a4-082c-4ffe-a4bc-c40fd3363c5e` with the value of your SESSION cookie: + + $ redis-cli del spring:session:sessions:7e8383a4-082c-4ffe-a4bc-c40fd3363c5e + +Now visit the application at http://localhost:8080/test/index and observe that we are no longer authenticated. diff --git a/samples/grails3/build.gradle b/samples/grails3/build.gradle new file mode 100644 index 00000000..0326f939 --- /dev/null +++ b/samples/grails3/build.gradle @@ -0,0 +1,77 @@ +buildscript { + ext { + grailsVersion = project.grailsVersion + } + repositories { + mavenLocal() + maven { url "https://repo.grails.org/grails/core" } + } + dependencies { + classpath "org.grails:grails-gradle-plugin:$grailsVersion" + classpath "com.bertramlabs.plugins:asset-pipeline-gradle:2.5.0" + classpath "org.grails.plugins:hibernate4:5.0.2" + } +} + +version "0.1" +group "grails3.redis.session" + +apply plugin:"eclipse" +apply plugin:"idea" +apply plugin:"war" +apply plugin:"org.grails.grails-web" +apply plugin:"org.grails.grails-gsp" +apply plugin:"asset-pipeline" + +ext { + grailsVersion = project.grailsVersion + gradleWrapperVersion = project.gradleWrapperVersion +} + +repositories { + mavenLocal() + maven { url "https://repo.grails.org/grails/core" } +} + +dependencyManagement { + imports { + mavenBom "org.grails:grails-bom:$grailsVersion" + } + applyMavenExclusions false +} + +dependencies { + compile "org.springframework.boot:spring-boot-starter-logging" + compile "org.springframework.boot:spring-boot-autoconfigure" + compile "org.grails:grails-core" + compile "org.springframework.boot:spring-boot-starter-actuator" + compile "org.springframework.boot:spring-boot-starter-tomcat" + compile "org.grails:grails-dependencies" + compile "org.grails:grails-web-boot" + compile "org.grails.plugins:cache" + compile "org.grails.plugins:scaffolding" + compile "org.grails.plugins:hibernate4" + compile "org.hibernate:hibernate-ehcache" + console "org.grails:grails-console" + profile "org.grails.profiles:web:3.1.4" + runtime "org.grails.plugins:asset-pipeline" + runtime "com.h2database:h2" + testCompile "org.grails:grails-plugin-testing" + testCompile "org.grails.plugins:geb" + testRuntime "org.seleniumhq.selenium:selenium-htmlunit-driver:2.47.1" + testRuntime "net.sourceforge.htmlunit:htmlunit:2.18" + + compile "org.springframework.boot:spring-boot-starter-redis" + compile 'org.springframework.session:spring-session:1.1.1.RELEASE' + + compile 'org.grails.plugins:spring-security-core:3.0.4' +} + +task wrapper(type: Wrapper) { + gradleVersion = gradleWrapperVersion +} + +assets { + minifyJs = true + minifyCss = true +} diff --git a/samples/grails3/gradle.properties b/samples/grails3/gradle.properties new file mode 100644 index 00000000..b6709f38 --- /dev/null +++ b/samples/grails3/gradle.properties @@ -0,0 +1,2 @@ +grailsVersion=3.1.4 +gradleWrapperVersion=2.9 diff --git a/samples/grails3/gradle/wrapper/gradle-wrapper.jar b/samples/grails3/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 00000000..94114481 Binary files /dev/null and b/samples/grails3/gradle/wrapper/gradle-wrapper.jar differ diff --git a/samples/grails3/gradle/wrapper/gradle-wrapper.properties b/samples/grails3/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 00000000..3869368d --- /dev/null +++ b/samples/grails3/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Fri Nov 27 23:09:32 CET 2015 +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-2.9-bin.zip diff --git a/samples/grails3/gradlew b/samples/grails3/gradlew new file mode 100755 index 00000000..9d82f789 --- /dev/null +++ b/samples/grails3/gradlew @@ -0,0 +1,160 @@ +#!/usr/bin/env bash + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS="" + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn ( ) { + echo "$*" +} + +die ( ) { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; +esac + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin, switch paths to Windows format before running java +if $cygwin ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules +function splitJvmOpts() { + JVM_OPTS=("$@") +} +eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS +JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" + +exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" diff --git a/samples/grails3/gradlew.bat b/samples/grails3/gradlew.bat new file mode 100644 index 00000000..aec99730 --- /dev/null +++ b/samples/grails3/gradlew.bat @@ -0,0 +1,90 @@ +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS= + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windowz variants + +if not "%OS%" == "Windows_NT" goto win9xME_args +if "%@eval[2+2]" == "4" goto 4NT_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* +goto execute + +:4NT_args +@rem Get arguments from the 4NT Shell from JP Software +set CMD_LINE_ARGS=%$ + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/samples/grails3/grails-app/conf/application.groovy b/samples/grails3/grails-app/conf/application.groovy new file mode 100644 index 00000000..36901ec7 --- /dev/null +++ b/samples/grails3/grails-app/conf/application.groovy @@ -0,0 +1,28 @@ + + +// Added by the Spring Security Core plugin: +grails.plugin.springsecurity.userLookup.userDomainClassName = 'grails3.redis.session.User' +grails.plugin.springsecurity.userLookup.authorityJoinClassName = 'grails3.redis.session.UserRole' +grails.plugin.springsecurity.authority.className = 'grails3.redis.session.Role' +grails.plugin.springsecurity.controllerAnnotations.staticRules = [ + [pattern: '/', access: ['permitAll']], + [pattern: '/error', access: ['permitAll']], + [pattern: '/index', access: ['permitAll']], + [pattern: '/index.gsp', access: ['permitAll']], + [pattern: '/shutdown', access: ['permitAll']], + [pattern: '/assets/**', access: ['permitAll']], + [pattern: '/**/js/**', access: ['permitAll']], + [pattern: '/**/css/**', access: ['permitAll']], + [pattern: '/**/images/**', access: ['permitAll']], + [pattern: '/**/favicon.ico', access: ['permitAll']] +] + +grails.plugin.springsecurity.filterChain.chainMap = [ + [pattern: '/assets/**', filters: 'none'], + [pattern: '/**/js/**', filters: 'none'], + [pattern: '/**/css/**', filters: 'none'], + [pattern: '/**/images/**', filters: 'none'], + [pattern: '/**/favicon.ico', filters: 'none'], + [pattern: '/**', filters: 'JOINED_FILTERS'] +] + diff --git a/samples/grails3/grails-app/conf/application.yml b/samples/grails3/grails-app/conf/application.yml new file mode 100644 index 00000000..43c77d29 --- /dev/null +++ b/samples/grails3/grails-app/conf/application.yml @@ -0,0 +1,122 @@ +--- +hibernate: + cache: + queries: false + use_second_level_cache: true + use_query_cache: false + region.factory_class: 'org.hibernate.cache.ehcache.EhCacheRegionFactory' + +dataSource: + pooled: true + jmxExport: true + driverClassName: org.h2.Driver + username: sa + password: + +environments: + development: + dataSource: + dbCreate: create-drop + url: jdbc:h2:mem:devDb;MVCC=TRUE;LOCK_TIMEOUT=10000;DB_CLOSE_ON_EXIT=FALSE + test: + dataSource: + dbCreate: update + url: jdbc:h2:mem:testDb;MVCC=TRUE;LOCK_TIMEOUT=10000;DB_CLOSE_ON_EXIT=FALSE + production: + dataSource: + dbCreate: update + url: jdbc:h2:./prodDb;MVCC=TRUE;LOCK_TIMEOUT=10000;DB_CLOSE_ON_EXIT=FALSE + properties: + jmxEnabled: true + initialSize: 5 + maxActive: 50 + minIdle: 5 + maxIdle: 25 + maxWait: 10000 + maxAge: 600000 + timeBetweenEvictionRunsMillis: 5000 + minEvictableIdleTimeMillis: 60000 + validationQuery: SELECT 1 + validationQueryTimeout: 3 + validationInterval: 15000 + testOnBorrow: true + testWhileIdle: true + testOnReturn: false + jdbcInterceptors: ConnectionState + defaultTransactionIsolation: 2 # TRANSACTION_READ_COMMITTED + +--- +--- +grails: + profile: web + codegen: + defaultPackage: grails3.redis.session + spring: + transactionManagement: + proxies: false +info: + app: + name: '@info.app.name@' + version: '@info.app.version@' + grailsVersion: '@info.app.grailsVersion@' +spring: + + groovy: + template: + check-template-location: false + +--- +grails: + mime: + disable: + accept: + header: + userAgents: + - Gecko + - WebKit + - Presto + - Trident + types: + all: '*/*' + atom: application/atom+xml + css: text/css + csv: text/csv + form: application/x-www-form-urlencoded + html: + - text/html + - application/xhtml+xml + js: text/javascript + json: + - application/json + - text/json + multipartForm: multipart/form-data + pdf: application/pdf + rss: application/rss+xml + text: text/plain + hal: + - application/hal+json + - application/hal+xml + xml: + - text/xml + - application/xml + urlmapping: + cache: + maxsize: 1000 + controllers: + defaultScope: singleton + converters: + encoding: UTF-8 + views: + default: + codec: html + gsp: + encoding: UTF-8 + htmlcodec: xml + codecs: + expression: html + scriptlets: html + taglib: none + staticparts: none +endpoints: + jmx: + unique-names: true diff --git a/samples/grails3/grails-app/conf/logback.groovy b/samples/grails3/grails-app/conf/logback.groovy new file mode 100644 index 00000000..2f7c41c1 --- /dev/null +++ b/samples/grails3/grails-app/conf/logback.groovy @@ -0,0 +1,23 @@ +import grails.util.BuildSettings +import grails.util.Environment + +// See http://logback.qos.ch/manual/groovy.html for details on configuration +appender('STDOUT', ConsoleAppender) { + encoder(PatternLayoutEncoder) { + pattern = "%level %logger - %msg%n" + } +} + +root(ERROR, ['STDOUT']) + +def targetDir = BuildSettings.TARGET_DIR +if (Environment.isDevelopmentMode() && targetDir) { + appender("FULL_STACKTRACE", FileAppender) { + file = "${targetDir}/stacktrace.log" + append = true + encoder(PatternLayoutEncoder) { + pattern = "%level %logger - %msg%n" + } + } + logger("StackTrace", ERROR, ['FULL_STACKTRACE'], false) +} diff --git a/samples/grails3/grails-app/conf/spring/resources.groovy b/samples/grails3/grails-app/conf/spring/resources.groovy new file mode 100644 index 00000000..fa950068 --- /dev/null +++ b/samples/grails3/grails-app/conf/spring/resources.groovy @@ -0,0 +1,3 @@ +// Place your Spring DSL code here +beans = { +} diff --git a/samples/grails3/grails-app/controllers/grails3/redis/session/TestController.groovy b/samples/grails3/grails-app/controllers/grails3/redis/session/TestController.groovy new file mode 100644 index 00000000..60e878d3 --- /dev/null +++ b/samples/grails3/grails-app/controllers/grails3/redis/session/TestController.groovy @@ -0,0 +1,8 @@ +package grails3.redis.session + +import grails.plugin.springsecurity.annotation.Secured + +class TestController { + @Secured('ROLE_ADMIN') + def index() { } // Renders `test/index.gsp` +} diff --git a/samples/grails3/grails-app/controllers/grails3/redis/session/UrlMappings.groovy b/samples/grails3/grails-app/controllers/grails3/redis/session/UrlMappings.groovy new file mode 100644 index 00000000..436d40d5 --- /dev/null +++ b/samples/grails3/grails-app/controllers/grails3/redis/session/UrlMappings.groovy @@ -0,0 +1,16 @@ +package grails3.redis.session + +class UrlMappings { + + static mappings = { + "/$controller/$action?/$id?(.$format)?"{ + constraints { + // apply constraints here + } + } + + "/"(view:"/index") + "500"(view:'/error') + "404"(view:'/notFound') + } +} diff --git a/samples/grails3/grails-app/domain/grails3/redis/session/Role.groovy b/samples/grails3/grails-app/domain/grails3/redis/session/Role.groovy new file mode 100644 index 00000000..4705064c --- /dev/null +++ b/samples/grails3/grails-app/domain/grails3/redis/session/Role.groovy @@ -0,0 +1,26 @@ +package grails3.redis.session + +import groovy.transform.EqualsAndHashCode +import groovy.transform.ToString + +@EqualsAndHashCode(includes='authority') +@ToString(includes='authority', includeNames=true, includePackage=false) +class Role implements Serializable { + + private static final long serialVersionUID = 1 + + String authority + + Role(String authority) { + this() + this.authority = authority + } + + static constraints = { + authority blank: false, unique: true + } + + static mapping = { + cache true + } +} diff --git a/samples/grails3/grails-app/domain/grails3/redis/session/User.groovy b/samples/grails3/grails-app/domain/grails3/redis/session/User.groovy new file mode 100644 index 00000000..078e249f --- /dev/null +++ b/samples/grails3/grails-app/domain/grails3/redis/session/User.groovy @@ -0,0 +1,55 @@ +package grails3.redis.session + +import groovy.transform.EqualsAndHashCode +import groovy.transform.ToString + +@EqualsAndHashCode(includes='username') +@ToString(includes='username', includeNames=true, includePackage=false) +class User implements Serializable { + + private static final long serialVersionUID = 1 + + transient springSecurityService + + String username + String password + boolean enabled = true + boolean accountExpired + boolean accountLocked + boolean passwordExpired + + User(String username, String password) { + this() + this.username = username + this.password = password + } + + Set getAuthorities() { + UserRole.findAllByUser(this)*.role + } + + def beforeInsert() { + encodePassword() + } + + def beforeUpdate() { + if (isDirty('password')) { + encodePassword() + } + } + + protected void encodePassword() { + password = springSecurityService?.passwordEncoder ? springSecurityService.encodePassword(password) : password + } + + static transients = ['springSecurityService'] + + static constraints = { + password blank: false, password: true + username blank: false, unique: true + } + + static mapping = { + password column: '`password`' + } +} diff --git a/samples/grails3/grails-app/domain/grails3/redis/session/UserRole.groovy b/samples/grails3/grails-app/domain/grails3/redis/session/UserRole.groovy new file mode 100644 index 00000000..3ba030fa --- /dev/null +++ b/samples/grails3/grails-app/domain/grails3/redis/session/UserRole.groovy @@ -0,0 +1,103 @@ +package grails3.redis.session + +import grails.gorm.DetachedCriteria +import groovy.transform.ToString + +import org.apache.commons.lang.builder.HashCodeBuilder + +@ToString(cache=true, includeNames=true, includePackage=false) +class UserRole implements Serializable { + + private static final long serialVersionUID = 1 + + User user + Role role + + UserRole(User u, Role r) { + this() + user = u + role = r + } + + @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() + } + + 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, insert: true) + instance + } + + static boolean remove(User u, Role r, boolean flush = false) { + if (u == null || r == null) return false + + int rowCount = UserRole.where { user == u && role == r }.deleteAll() + + if (flush) { UserRole.withSession { it.flush() } } + + rowCount + } + + static void removeAll(User u, boolean flush = false) { + if (u == null) return + + UserRole.where { user == u }.deleteAll() + + if (flush) { UserRole.withSession { it.flush() } } + } + + static void removeAll(Role r, boolean flush = false) { + if (r == null) return + + UserRole.where { role == r }.deleteAll() + + if (flush) { UserRole.withSession { it.flush() } } + } + + static constraints = { + role validator: { Role r, UserRole ur -> + if (ur.user == null || ur.user.id == null) return + boolean existing = false + UserRole.withNewSession { + existing = UserRole.exists(ur.user.id, r.id) + } + if (existing) { + return 'userRole.exists' + } + } + } + + static mapping = { + id composite: ['user', 'role'] + version false + } +} diff --git a/samples/grails3/grails-app/i18n/messages.properties b/samples/grails3/grails-app/i18n/messages.properties new file mode 100644 index 00000000..b0451362 --- /dev/null +++ b/samples/grails3/grails-app/i18n/messages.properties @@ -0,0 +1,56 @@ +default.doesnt.match.message=Property [{0}] of class [{1}] with value [{2}] does not match the required pattern [{3}] +default.invalid.url.message=Property [{0}] of class [{1}] with value [{2}] is not a valid URL +default.invalid.creditCard.message=Property [{0}] of class [{1}] with value [{2}] is not a valid credit card number +default.invalid.email.message=Property [{0}] of class [{1}] with value [{2}] is not a valid e-mail address +default.invalid.range.message=Property [{0}] of class [{1}] with value [{2}] does not fall within the valid range from [{3}] to [{4}] +default.invalid.size.message=Property [{0}] of class [{1}] with value [{2}] does not fall within the valid size range from [{3}] to [{4}] +default.invalid.max.message=Property [{0}] of class [{1}] with value [{2}] exceeds maximum value [{3}] +default.invalid.min.message=Property [{0}] of class [{1}] with value [{2}] is less than minimum value [{3}] +default.invalid.max.size.message=Property [{0}] of class [{1}] with value [{2}] exceeds the maximum size of [{3}] +default.invalid.min.size.message=Property [{0}] of class [{1}] with value [{2}] is less than the minimum size of [{3}] +default.invalid.validator.message=Property [{0}] of class [{1}] with value [{2}] does not pass custom validation +default.not.inlist.message=Property [{0}] of class [{1}] with value [{2}] is not contained within the list [{3}] +default.blank.message=Property [{0}] of class [{1}] cannot be blank +default.not.equal.message=Property [{0}] of class [{1}] with value [{2}] cannot equal [{3}] +default.null.message=Property [{0}] of class [{1}] cannot be null +default.not.unique.message=Property [{0}] of class [{1}] with value [{2}] must be unique + +default.paginate.prev=Previous +default.paginate.next=Next +default.boolean.true=True +default.boolean.false=False +default.date.format=yyyy-MM-dd HH:mm:ss z +default.number.format=0 + +default.created.message={0} {1} created +default.updated.message={0} {1} updated +default.deleted.message={0} {1} deleted +default.not.deleted.message={0} {1} could not be deleted +default.not.found.message={0} not found with id {1} +default.optimistic.locking.failure=Another user has updated this {0} while you were editing + +default.home.label=Home +default.list.label={0} List +default.add.label=Add {0} +default.new.label=New {0} +default.create.label=Create {0} +default.show.label=Show {0} +default.edit.label=Edit {0} + +default.button.create.label=Create +default.button.edit.label=Edit +default.button.update.label=Update +default.button.delete.label=Delete +default.button.delete.confirm.message=Are you sure? + +# Data binding errors. Use "typeMismatch.$className.$propertyName to customize (eg typeMismatch.Book.author) +typeMismatch.java.net.URL=Property {0} must be a valid URL +typeMismatch.java.net.URI=Property {0} must be a valid URI +typeMismatch.java.util.Date=Property {0} must be a valid Date +typeMismatch.java.lang.Double=Property {0} must be a valid number +typeMismatch.java.lang.Integer=Property {0} must be a valid number +typeMismatch.java.lang.Long=Property {0} must be a valid number +typeMismatch.java.lang.Short=Property {0} must be a valid number +typeMismatch.java.math.BigDecimal=Property {0} must be a valid number +typeMismatch.java.math.BigInteger=Property {0} must be a valid number +typeMismatch=Property {0} is type-mismatched diff --git a/samples/grails3/grails-app/init/BootStrap.groovy b/samples/grails3/grails-app/init/BootStrap.groovy new file mode 100644 index 00000000..1bf1ef43 --- /dev/null +++ b/samples/grails3/grails-app/init/BootStrap.groovy @@ -0,0 +1,24 @@ +import grails3.redis.session.* + +class BootStrap { + + def init = { servletContext -> + def adminRole = new Role('ROLE_ADMIN').save() + def userRole = new Role('ROLE_USER').save() + + def testUser = new User('user', 'password').save() + + UserRole.create testUser, adminRole + + UserRole.withSession { + it.flush() + it.clear() + } + + assert User.count() == 1 + assert Role.count() == 2 + assert UserRole.count() == 1 + } + def destroy = { + } +} diff --git a/samples/grails3/grails-app/init/grails3/redis/session/Application.groovy b/samples/grails3/grails-app/init/grails3/redis/session/Application.groovy new file mode 100644 index 00000000..b83e7ea0 --- /dev/null +++ b/samples/grails3/grails-app/init/grails3/redis/session/Application.groovy @@ -0,0 +1,10 @@ +package grails3.redis.session + +import grails.boot.GrailsApp +import grails.boot.config.GrailsAutoConfiguration + +class Application extends GrailsAutoConfiguration { + static void main(String[] args) { + GrailsApp.run(Application, args) + } +} \ No newline at end of file diff --git a/samples/grails3/grails-app/views/error.gsp b/samples/grails3/grails-app/views/error.gsp new file mode 100644 index 00000000..9a3bb8aa --- /dev/null +++ b/samples/grails3/grails-app/views/error.gsp @@ -0,0 +1,31 @@ + + + + <g:if env="development">Grails Runtime Exception</g:if><g:else>Error</g:else> + + + + + + + + + + + + +
    +
  • An error has occurred
  • +
  • Exception: ${exception}
  • +
  • Message: ${message}
  • +
  • Path: ${path}
  • +
+
+
+ +
    +
  • An error has occurred
  • +
+
+ + diff --git a/samples/grails3/grails-app/views/index.gsp b/samples/grails3/grails-app/views/index.gsp new file mode 100644 index 00000000..cba0cf98 --- /dev/null +++ b/samples/grails3/grails-app/views/index.gsp @@ -0,0 +1,8 @@ + + + Index + + + Left blank, goto test + + diff --git a/samples/grails3/grails-app/views/notFound.gsp b/samples/grails3/grails-app/views/notFound.gsp new file mode 100644 index 00000000..4c873baa --- /dev/null +++ b/samples/grails3/grails-app/views/notFound.gsp @@ -0,0 +1,14 @@ + + + + Page Not Found + + + + + + + diff --git a/samples/grails3/grails-app/views/test/index.gsp b/samples/grails3/grails-app/views/test/index.gsp new file mode 100644 index 00000000..00b421bf --- /dev/null +++ b/samples/grails3/grails-app/views/test/index.gsp @@ -0,0 +1,16 @@ + + + Home Page + + +
+ +
+
+ ${session.id} +
+
+ +
+ + diff --git a/samples/grails3/src/integration-test/groovy/functionaltests/HomeSpec.groovy b/samples/grails3/src/integration-test/groovy/functionaltests/HomeSpec.groovy new file mode 100644 index 00000000..d96d20ca --- /dev/null +++ b/samples/grails3/src/integration-test/groovy/functionaltests/HomeSpec.groovy @@ -0,0 +1,98 @@ +/* + * Copyright 2014-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package sample + +import grails.test.mixin.integration.Integration +import grails.transaction.Transactional +import org.springframework.boot.test.IntegrationTest + +import spock.lang.* +import geb.spock.* + +import sample.pages.HomePage +import sample.pages.LoginPage +import sample.pages.IndexPage +import spock.lang.Stepwise +import pages.* + +/** + * Functional tests for grails 3 and spring-session + * + * @author Eric Helgeson + */ + +@Stepwise +@IntegrationTest("server.port:0") +@Integration(applicationClass=grails3.redis.session.Application) +class HomeSpec extends GebSpec { + + def setup() { + } + + def cleanup() { + } + + void 'Anonymous page not redirected to login'() { + when: 'The index page is visited' + go '/' + + then: 'Not redirected' + at IndexPage + } + + void 'Unauthenticated user sent to log in page'() { + when: 'The test page is visited' + go '/test/index' + if(title != 'Login') { + println driver.pageSource + } + + then: 'The password form is correct' + title == 'Login' + $('#password') + $('#username') + } + + void 'Log in views home page'() { + when: 'log in successfully' + to LoginPage + login() + + then: 'sent to original page' + at HomePage + + and: 'the username is displayed' + username == 'user' + + and: 'session id is not blank' + session != '' + + and: 'Spring Session Management is being used' + driver.manage().cookies.find { it.name == 'SESSION' } + + and: 'Standard Session is NOT being used' + !driver.manage().cookies.find { it.name == 'JSESSIONID' } + } + + def 'Log out success'() { + when: + logout() + + then: + at IndexPage + } +} diff --git a/samples/grails3/src/integration-test/groovy/functionaltests/pages/HomePage.groovy b/samples/grails3/src/integration-test/groovy/functionaltests/pages/HomePage.groovy new file mode 100644 index 00000000..ae10649c --- /dev/null +++ b/samples/grails3/src/integration-test/groovy/functionaltests/pages/HomePage.groovy @@ -0,0 +1,34 @@ +/* + * Copyright 2014-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package sample.pages + +import geb.* + +/** + * The home page + * + * @author Rob Winch + */ +class HomePage extends Page { + static url = '/test' + static at = { assert driver.title == 'Home Page'; true} + static content = { + username { $('#un').text() } + session { $('#session').text() } + logout(to:LoginPage) { $('input[type=submit]').click() } + } +} diff --git a/samples/grails3/src/integration-test/groovy/functionaltests/pages/IndexPage.groovy b/samples/grails3/src/integration-test/groovy/functionaltests/pages/IndexPage.groovy new file mode 100644 index 00000000..497b90da --- /dev/null +++ b/samples/grails3/src/integration-test/groovy/functionaltests/pages/IndexPage.groovy @@ -0,0 +1,30 @@ +/* + * Copyright 2014-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package sample.pages + +import geb.* + +/** + * The Index page + * + * @author Eric Helgeson + */ +class IndexPage extends Page { + static url = '/' + static at = { assert driver.title == 'Index'; true} + static content = { } +} diff --git a/samples/grails3/src/integration-test/groovy/functionaltests/pages/LoginPage.groovy b/samples/grails3/src/integration-test/groovy/functionaltests/pages/LoginPage.groovy new file mode 100644 index 00000000..46c7caeb --- /dev/null +++ b/samples/grails3/src/integration-test/groovy/functionaltests/pages/LoginPage.groovy @@ -0,0 +1,38 @@ +/* + * Copyright 2014-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package sample.pages + +import geb.* + +/** + * The Login Page + * + * @author Rob Winch + */ +class LoginPage extends Page { + static url = '/login' + static at = { assert driver.title == 'Login'; true} + static content = { + form { $('form') } + submit { $('input[type=submit]') } + login(required:false) { user='user', pass='password' -> + form.username = user + form.password = pass + submit.click() + } + } +} diff --git a/settings.gradle b/settings.gradle index 0d986c01..da43a162 100644 --- a/settings.gradle +++ b/settings.gradle @@ -20,6 +20,7 @@ include 'samples:security' include 'samples:users' include 'samples:websocket' include 'samples:mongo' +include 'samples:grails3' include 'spring-session' include 'spring-session-data-gemfire'