1. Información general
En este tutorial, veremos el Servicio de autenticación central de Apereo (CAS) y veremos cómo un servicio Spring Boot puede usarlo para la autenticación. CAS es una solución empresarial de inicio de sesión único (SSO) que también es de código abierto.
¿Qué es SSO? Cuando inicia sesión en YouTube, Gmail y Maps con las mismas credenciales, eso es inicio de sesión único. Vamos a demostrar esto configurando un servidor CAS y una aplicación Spring Boot. La aplicación Spring Boot utilizará CAS para la autenticación.2. Configuración del servidor CAS
2.1. Instalación y dependencias de CAS
El servidor utiliza el estilo War Overlay de Maven (Gradle) para facilitar la configuración y la implementación:
git clone //github.com/apereo/cas-overlay-template.git cas-server
Este comando clonará cas-overlay-template en el directorio cas-server .
Algunos de los aspectos que cubriremos incluyen el registro del servicio JSON y la conexión a la base de datos JDBC. Entonces, agregaremos sus módulos a la sección de dependencias del archivo build.gradle :
compile "org.apereo.cas:cas-server-support-json-service-registry:${casServerVersion}" compile "org.apereo.cas:cas-server-support-jdbc:${casServerVersion}"
Asegurémonos de verificar la última versión de casServer.
2.2. Configuración del servidor CAS
Antes de que podamos iniciar el servidor CAS, necesitamos agregar algunas configuraciones básicas. Comencemos creando una carpeta cas-server / src / main / resources y en esta carpeta. Esto será seguido por la creación de application.properties en la carpeta, también:
server.port=8443 spring.main.allow-bean-definition-overriding=true server.ssl.key-store=classpath:/etc/cas/thekeystore server.ssl.key-store-password=changeit
Procedamos con la creación del archivo de almacén de claves al que se hace referencia en la configuración anterior. Primero, necesitamos crear las carpetas / etc / cas y / etc / cas / config en cas-server / src / main / resources .
Luego, necesitamos cambiar el directorio a cas-server / src / main / resources / etc / cas y ejecutar el comando para generar el almacén de claves :
keytool -genkey -keyalg RSA -alias thekeystore -keystore thekeystore -storepass changeit -validity 360 -keysize 2048
Para que no tengamos un error de protocolo de enlace SSL, debemos usar localhost como el valor del nombre y apellido. Deberíamos usar lo mismo para el nombre de la organización y la unidad también. Además, necesitamos importar el almacén de claves al JDK / JRE que usaremos para ejecutar nuestra aplicación cliente:
keytool -importkeystore -srckeystore thekeystore -destkeystore $JAVA11_HOME/jre/lib/security/cacerts
La contraseña para el almacén de claves de origen y destino es changeit . En sistemas Unix, es posible que tengamos que ejecutar este comando con privilegios de administrador ( sudo ). Después de la importación, debemos reiniciar todas las instancias de Java que se están ejecutando o reiniciar el sistema.
Estamos usando JDK11 porque es requerido por CAS versión 6.1.x. Además, definimos la variable de entorno $ JAVA11_HOME que apunta a su directorio de inicio. Ahora podemos iniciar el servidor CAS:
./gradlew run -Dorg.gradle.java.home=$JAVA11_HOME
Cuando se inicie la aplicación, veremos "READY" impreso en la terminal y el servidor estará disponible en // localhost: 8443 .
2.3. Configuración de usuario del servidor CAS
No podemos iniciar sesión todavía porque no hemos configurado ningún usuario. CAS tiene diferentes métodos para administrar la configuración, incluido el modo independiente. Creemos una carpeta de configuración cas-server / src / main / resources / etc / cas / config en la que crearemos un archivo de propiedades cas.properties . Ahora, podemos definir un usuario estático en el archivo de propiedades:
cas.authn.accept.users=casuser::Mellon
Tenemos que comunicar la ubicación de la carpeta de configuración al servidor CAS para que la configuración surta efecto. La actualización de Let tasks.gradle por lo que puede pasar a la ubicación como un argumento JVM desde la línea de comandos:
task run(group: "build", description: "Run the CAS web application in embedded container mode") { dependsOn 'build' doLast { def casRunArgs = new ArrayList(Arrays.asList( "-server -noverify -Xmx2048M -XX:+TieredCompilation -XX:TieredStopAtLevel=1".split(" "))) if (project.hasProperty('args')) { casRunArgs.addAll(project.args.split('\\s+')) } javaexec { main = "-jar" jvmArgs = casRunArgs args = ["build/libs/${casWebApplicationBinaryName}"] logger.info "Started ${commandLine}" } } }
Luego guardamos el archivo y ejecutamos:
./gradlew run -Dorg.gradle.java.home=$JAVA11_HOME -Pargs="-Dcas.standalone.configurationDirectory=/cas-server/src/main/resources/etc/cas/config"
Tenga en cuenta que el valor de cas.standalone.configurationDirectory es una ruta absoluta . Ahora podemos ir a // localhost: 8443 e iniciar sesión con el nombre de usuario casuser y la contraseña Mellon .
3. Configuración del cliente CAS
Usaremos Spring Initializr para generar una aplicación cliente Spring Boot. Tendrá dependencias Web , Security , Freemarker y DevTools . Además, también agregaremos la dependencia para el módulo Spring Security CAS a su pom.xml :
org.springframework.security spring-security-cas 5.3.0.RELEASE
Finalmente, agreguemos las siguientes propiedades de Spring Boot para configurar la aplicación:
server.port=8900 spring.freemarker.suffix=.ftl
4. Registro del servicio del servidor CAS
Las aplicaciones de los clientes deben registrarse con el servidor CAS antes de cualquier autenticación . El servidor CAS admite el uso de registros de clientes YAML, JSON, MongoDB y LDAP.
En este tutorial, usaremos el método JSON Service Registry. Creemos otra carpeta cas-server / src / main / resources / etc / cas / services . Es esta carpeta la que albergará los archivos JSON del registro de servicios.
Crearemos un archivo JSON que contiene la definición de nuestra aplicación cliente. El nombre del archivo, casSecuredApp-8900.json, sigue el patrón s erviceName-Id.json :
{ "@class" : "org.apereo.cas.services.RegexRegisteredService", "serviceId" : "//localhost:8900/login/cas", "name" : "casSecuredApp", "id" : 8900, "logoutType" : "BACK_CHANNEL", "logoutUrl" : "//localhost:8900/exit/cas" }
El atributo serviceId define un patrón de URL regex para la aplicación cliente. El patrón debe coincidir con la URL de la aplicación cliente.
El atributo id debe ser único. En otras palabras, no debería haber dos o más servicios con la misma identificación registrados en el mismo servidor CAS. Tener una identificación duplicada dará lugar a conflictos y la anulación de configuraciones.
También configuramos el tipo de cierre de sesión para que sea BACK_CHANNEL y la URL para que sea // localhost: 8900 / exit / cas para que podamos hacer un cierre de sesión único más tarde. Antes de que el servidor CAS pueda hacer uso de nuestro archivo de configuración JSON, tenemos que habilitar el registro JSON en nuestras cas.properties :cas.serviceRegistry.initFromJson=true cas.serviceRegistry.json.location=classpath:/etc/cas/services
5. Configuración de inicio de sesión único del cliente CAS
El siguiente paso para nosotros es configurar Spring Security para que funcione con el servidor CAS. También deberíamos comprobar el flujo completo de interacciones, llamado secuencia CAS.
Let's add the following bean configurations to the CasSecuredApplication class of our Spring Boot app:
@Bean public CasAuthenticationFilter casAuthenticationFilter( AuthenticationManager authenticationManager, ServiceProperties serviceProperties) throws Exception { CasAuthenticationFilter filter = new CasAuthenticationFilter(); filter.setAuthenticationManager(authenticationManager); filter.setServiceProperties(serviceProperties); return filter; } @Bean public ServiceProperties serviceProperties() { logger.info("service properties"); ServiceProperties serviceProperties = new ServiceProperties(); serviceProperties.setService("//cas-client:8900/login/cas"); serviceProperties.setSendRenew(false); return serviceProperties; } @Bean public TicketValidator ticketValidator() { return new Cas30ServiceTicketValidator("//localhost:8443"); } @Bean public CasAuthenticationProvider casAuthenticationProvider( TicketValidator ticketValidator, ServiceProperties serviceProperties) { CasAuthenticationProvider provider = new CasAuthenticationProvider(); provider.setServiceProperties(serviceProperties); provider.setTicketValidator(ticketValidator); provider.setUserDetailsService( s -> new User("[email protected]", "Mellon", true, true, true, true, AuthorityUtils.createAuthorityList("ROLE_ADMIN"))); provider.setKey("CAS_PROVIDER_LOCALHOST_8900"); return provider; }
The ServiceProperties bean has the same URL as the serviceId in casSecuredApp-8900.json. This is important because it identifies this client to the CAS server.
The sendRenew property of ServiceProperties is set to false. This means a user only needs to present login credentials to the server once.
The AuthenticationEntryPoint bean will handle authentication exceptions. Thus, it'll redirect the user to the login URL of the CAS server for authentication.
In summary, the authentication flow goes:
- A user attempts to access a secure page, which triggers an authentication exception
- The exception triggers AuthenticationEntryPoint. In response, the AuthenticationEntryPoint will take the user to the CAS server login page – //localhost:8443/login
- On successful authentication, the server redirects back to the client with a ticket
- CasAuthenticationFilter will pick up the redirect and call CasAuthenticationProvider
- CasAuthenticationProvider will use TicketValidator to confirm the presented ticket on CAS server
- If the ticket is valid, the user will get a redirection to the requested secure URL
Finally, let's configure HttpSecurity to secure some routes in WebSecurityConfig. In the process, we'll also add the authentication entry point for exception handling:
@Override protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests().antMatchers( "/secured", "/login") .authenticated() .and().exceptionHandling() .authenticationEntryPoint(authenticationEntryPoint()); }
6. CAS Client Single Logout Configuration
So far, we've dealt with single sign-on; let's now consider CAS single logout (SLO).
Applications that use CAS for managing user authentication can log out a user from two places:
- The client application can logout a user from itself locally – this will not affect the user's login status in other applications using the same CAS server
- The client application can also log out the user from the CAS server – this will cause the user to be logged out from all other client apps connected to the same CAS server.
We'll first put in place logout on the client application and then extend it to single logout on the CAS server.
In order to make obvious what goes on behind the scene, we'll create a logout() method to handle the local logout. On success, it'll redirect us to a page with a link for single logout:
@GetMapping("/logout") public String logout( HttpServletRequest request, HttpServletResponse response, SecurityContextLogoutHandler logoutHandler) { Authentication auth = SecurityContextHolder .getContext().getAuthentication(); logoutHandler.logout(request, response, auth ); new CookieClearingLogoutHandler( AbstractRememberMeServices.SPRING_SECURITY_REMEMBER_ME_COOKIE_KEY) .logout(request, response, auth); return "auth/logout"; }
In the single logout process, the CAS server will first expire the user's ticket and then send an async request to all registered client apps. Each client app that receives this signal will perform a local logout. Thereby accomplishing the goal of logout once, it will cause a log out everywhere.
Having said that, let's add some bean configurations to our client app. Specifically, in the CasSecuredApplicaiton:
@Bean public SecurityContextLogoutHandler securityContextLogoutHandler() { return new SecurityContextLogoutHandler(); } @Bean public LogoutFilter logoutFilter() { LogoutFilter logoutFilter = new LogoutFilter("//localhost:8443/logout", securityContextLogoutHandler()); logoutFilter.setFilterProcessesUrl("/logout/cas"); return logoutFilter; } @Bean public SingleSignOutFilter singleSignOutFilter() { SingleSignOutFilter singleSignOutFilter = new SingleSignOutFilter(); singleSignOutFilter.setCasServerUrlPrefix("//localhost:8443"); singleSignOutFilter.setLogoutCallbackPath("/exit/cas"); singleSignOutFilter.setIgnoreInitConfiguration(true); return singleSignOutFilter; }
The logoutFilter will intercept requests to /logout/cas and redirect the application to the CAS server. The SingleSignOutFilter will intercept requests coming from the CAS server and perform the local logout.
7. Connecting the CAS Server to a Database
We can configure the CAS server to read credentials from a MySQL database. We'll use the test database of a MySQL server that's running in a local machine. Let's update cas-server/src/main/resources/etc/cas/config/cas.properties:
cas.authn.accept.users= cas.authn.jdbc.query[0].sql=SELECT * FROM users WHERE email = ? cas.authn.jdbc.query[0].url= jdbc:mysql://127.0.0.1:3306/test? useUnicode=true&useJDBCCompliantTimezoneShift=true&useLegacyDatetimeCode=false&serverTimezone=UTC cas.authn.jdbc.query[0].dialect=org.hibernate.dialect.MySQLDialect cas.authn.jdbc.query[0].user=root cas.authn.jdbc.query[0].password=root cas.authn.jdbc.query[0].ddlAuto=none cas.authn.jdbc.query[0].driverClass=com.mysql.cj.jdbc.Driver cas.authn.jdbc.query[0].fieldPassword=password cas.authn.jdbc.query[0].passwordEncoder.type=NONE
We set the cas.authn.accept.users to blank. This will deactivate the use of static user repositories by the CAS server.
According to the SQL above, users' credentials are stored in the users table. The email column is what represents the users' principal (username).
Please make sure to check the list of supported databases, available drivers and dialects. We also set the password encoder type to NONE. Other encryption mechanisms and their peculiar properties are also available.
Note that the principal in the database of the CAS server must be the same as that of the client application.
Let's update CasAuthenticationProvider to have the same username as the CAS server:
@Bean public CasAuthenticationProvider casAuthenticationProvider() { CasAuthenticationProvider provider = new CasAuthenticationProvider(); provider.setServiceProperties(serviceProperties()); provider.setTicketValidator(ticketValidator()); provider.setUserDetailsService( s -> new User("[email protected]", "Mellon", true, true, true, true, AuthorityUtils.createAuthorityList("ROLE_ADMIN"))); provider.setKey("CAS_PROVIDER_LOCALHOST_8900"); return provider; }
CasAuthenticationProvider no utiliza la contraseña para la autenticación. No obstante, su nombre de usuario debe coincidir con el del servidor CAS para que la autenticación sea exitosa. El servidor CAS requiere que un servidor MySQL se ejecute en localhost en el puerto 3306 . El nombre de usuario y la contraseña deben ser root .
Reinicie el servidor CAS y la aplicación Spring Boot una vez más. Luego, use las nuevas credenciales para la autenticación.
8. Conclusión
Hemos visto cómo utilizar CAS SSO con Spring Security y muchos de los archivos de configuración involucrados. Hay muchos otros aspectos de CAS SSO que se pueden configurar. Desde temas y tipos de protocolos hasta políticas de autenticación.
Estos y otros están en los documentos. El código fuente para el servidor CAS y la aplicación Spring Boot está disponible en GitHub.