| Bloque de código |
|---|
| language | sql |
|---|
| title | Query para crear tabla "CAS_TOKEN_SESSION" |
|---|
| collapse | true |
|---|
| CREATE TABLE "PORTALFUNDEWEB"."CAS_TOKEN_SESSION"
( "UUID" VARCHAR2(40 BYTE) NOT NULL ENABLE,
"LOGIN" VARCHAR2(60 BYTE) NOT NULL ENABLE,
"START_TIME" TIMESTAMP (6) NOT NULL ENABLE,
"LAST_REFRESH" TIMESTAMP (6) NOT NULL ENABLE,
"USER_AGENT" VARCHAR2(256 BYTE),
"PLACE" VARCHAR2(256 BYTE),
"LDAP_GROUPS" VARCHAR2(256 BYTE),
CONSTRAINT "CAS_TOKEN_SESSION_PK" PRIMARY KEY ("UUID")
USING INDEX PCTFREE 10 INITRANS 2 MAXTRANS 255 COMPUTE STATISTICS
STORAGE(INITIAL 65536 NEXT 1048576 MINEXTENTS 1 MAXEXTENTS 2147483645
PCTINCREASE 0 FREELISTS 1 FREELIST GROUPS 1
BUFFER_POOL DEFAULT FLASH_CACHE DEFAULT CELL_FLASH_CACHE DEFAULT)
TABLESPACE "PORTALFUNDEWEB" ENABLE
) SEGMENT CREATION IMMEDIATE
PCTFREE 10 PCTUSED 40 INITRANS 1 MAXTRANS 255
NOCOMPRESS LOGGING
STORAGE(INITIAL 65536 NEXT 1048576 MINEXTENTS 1 MAXEXTENTS 2147483645
PCTINCREASE 0 FREELISTS 1 FREELIST GROUPS 1
BUFFER_POOL DEFAULT FLASH_CACHE DEFAULT CELL_FLASH_CACHE DEFAULT)
TABLESPACE "PORTALFUNDEWEB" ; |
Después, seguimos el diagrama que se muestra en esta página de la wiki de FundeWeb 2.0. Se muestra tanto el esquema como el script para crear las tablas. Se tienen tres tablas, Usuarios, Roles y Objetivos, con tablas intermedias entre ellas. Lo más habitual es que se utilicen los Usuarios y los Roles, los Objetivos son para dar permisos más detallados, para ciertas partes de la aplicación. Vamos a ver las entidades correspondientes a estas tablas. En nuestro proyecto tendremos que incluir las entidades correspondientes e implementar UserDetails (en nuestro ejemplo son las clases de es.um.atica.proyecto.entities y la clase ServiceUserDetails de es.um.atica.logincas.model, las podemos descargar en el apartado Resto de clases). Además, si nuestro frontend necesita consultar datos de usuario, debemos implementar los métodos correspondientes en la clase UserEndpoints (en nuestro ejemplo sólo hay un /whoami básico). Cosas a añadir en el proyectoDependencias en el pom.xmlTendremos que añadir estas dependencias al pom.xml: | Bloque de código |
|---|
| language | xml |
|---|
| theme | Eclipse |
|---|
| title | Dependencias pom.xml |
|---|
| collapse | true |
|---|
| <!-- CAS -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-cas</artifactId>
</dependency>
<!-- JWT -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency> |
Parámetros en application.propertiesEn nuestro application.properties, además de tener definido el datasource de la aplicación, tenemos que añadir las siguientes propiedades: | Bloque de código |
|---|
| title | application.properties |
|---|
| collapse | true |
|---|
| login.config.serviceurl=https://atica-67-165.atica.um.es:8080/api/entrada
login.config.sso.loginurl=https://sso.um.es/cas/login
login.config.sso.logouturl=https://sso.um.es/cas/logout
login.config.sso.serverurl=https://sso.um.es/cas/
es.um.jwt.secret=---
security.enable-csrf=false
es.um.token.session.expiration=1000
# Tiempo de vida del token (en minutos)
es.um.jwt.expires=15
server.servlet.session.persistent=false
# Entorno - Se comprobara si esta variable es "local" o "desarrollo"
es.um.p15s.environment=local |
La variable login.config.serviceurl es a la que tendremos que acceder para redirigir al cas. Se utiliza un secret para codificar el token, en la variable es.um.jwt.secret. En cada entorno se sustituirá por el secret correspondiente. Si queremos probar el login con el portal, como será este el que nos devuelva el token cifrado, tendremos que poner el mismo secret que se utilice en el entorno correspondiente con el que hagamos las pruebas. Configuración en la clase inicialEn la calse que inicia nuestra aplicación, que se llamará NombreProyectoApplication.java, tendremos que añadir varias cosas, dejándola como en este ejemplo, pero con el método main que nos venía: | Bloque de código |
|---|
| language | java |
|---|
| theme | Eclipse |
|---|
| title | Clase inicial - NombreProyectoApplication.java |
|---|
| linenumbers | true |
|---|
| collapse | true |
|---|
| package es.um.atica.----;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSessionEvent;
import org.jasig.cas.client.session.SingleSignOutFilter;
import org.jasig.cas.client.session.SingleSignOutHttpSessionListener;
import org.jasig.cas.client.validation.Cas30ServiceTicketValidator;
import org.jasig.cas.client.validation.TicketValidator;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Primary;
import org.springframework.context.event.EventListener;
import org.springframework.security.cas.ServiceProperties;
import org.springframework.security.cas.authentication.CasAuthenticationProvider;
import org.springframework.security.cas.web.CasAuthenticationEntryPoint;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.security.web.authentication.logout.LogoutFilter;
import org.springframework.security.web.authentication.logout.SecurityContextLogoutHandler;
import org.springframework.web.client.RestTemplate;
import es.um.atica.----.cas.services.UsuarioService;
@SpringBootApplication
public class LogincasApplication {
@Value( "${login.config.serviceurl}" )
private String serviceurl;
@Value( "${login.config.sso.loginurl}" )
private String ssoLoginUrl;
@Value( "${login.config.sso.logouturl}" )
private String ssoLogoutUrl;
@Value( "${login.config.sso.serverurl}" )
private String ssoServerUrl;
public static void main( String[] args ) {
SpringApplication.run( LogincasApplication.class, args );
}
@Bean
public RestTemplate restTemplate() {
return new RestTemplate();
}
@Bean
public ServiceProperties serviceProperties() {
final ServiceProperties serviceProperties = new ServiceProperties();
serviceProperties.setSendRenew( false );
serviceProperties.setService( serviceurl );
serviceProperties.setAuthenticateAllArtifacts( true );
return serviceProperties;
}
@Bean
@Primary
public AuthenticationEntryPoint authenticationEntryPoint( ServiceProperties sP ) {
final CasAuthenticationEntryPoint entryPoint = new CasAuthenticationEntryPoint() {
@Override
protected String createServiceUrl( final HttpServletRequest request, final HttpServletResponse response ) {
String serviceUrl = serviceProperties().getService();
final String callback = request.getParameter( "callback" );
if ( callback != null ) {
serviceUrl += "/" + callback;
}
return serviceUrl;
}
};
entryPoint.setLoginUrl( ssoLoginUrl );
entryPoint.setServiceProperties( sP );
return entryPoint;
}
@Bean
public TicketValidator ticketValidator() {
return new Cas30ServiceTicketValidator( ssoServerUrl );
}
@Bean
public UserDetailsService generateUserDetailsService() {
return new UsuarioService();
}
@Bean
public CasAuthenticationProvider casAuthenticationProvider() {
final CasAuthenticationProvider provider = new CasAuthenticationProvider();
provider.setServiceProperties( serviceProperties() );
provider.setTicketValidator( ticketValidator() );
provider.setUserDetailsService( generateUserDetailsService() );
provider.setKey( "CAS_PROVIDER_P15S" );
return provider;
}
@Bean
public SecurityContextLogoutHandler securityContextLogoutHandler() {
return new SecurityContextLogoutHandler();
}
@Bean
public LogoutFilter logoutFilter() {
final LogoutFilter logoutFilter = new LogoutFilter( ssoLogoutUrl, securityContextLogoutHandler() );
logoutFilter.setFilterProcessesUrl( "/logout/cas" );
return logoutFilter;
}
@Bean
public SingleSignOutFilter singleSignOutFilter() {
final SingleSignOutFilter singleSignOutFilter = new SingleSignOutFilter();
// singleSignOutFilter.setCasServerUrlPrefix( ssoServerUrl );
singleSignOutFilter.setIgnoreInitConfiguration( true );
return singleSignOutFilter;
}
@EventListener
public SingleSignOutHttpSessionListener singleSignOutHttpSessionListener( HttpSessionEvent event ) {
return new SingleSignOutHttpSessionListener();
}
} |
Configuración de seguridadNuestra clase de configuración de seguridad (la que tenemos con la anotación @EnableWebSecurity, se llame ConfiguraciónSeguridad, SecurityConfig, o como queramos) quedará así (no hay que duplicar esta clase, debemos tener una en el proyecto, si tenemos otras cosas metidas en nuestro SecurityConfig, como ModelMapper por ejemplo, se debe añadir lo que contiene esta): | Bloque de código |
|---|
| language | java |
|---|
| theme | Eclipse |
|---|
| title | Configuración de seguridad - SecurityConfig.java |
|---|
| linenumbers | true |
|---|
| collapse | true |
|---|
| package es.um.atica.----.cas.config;
import static es.um.atica.----.Constants.SESSION_TOKEN_HEADER;
import static es.um.atica.----.Constants.LOCAL_ENV;
import java.util.Arrays;
import javax.servlet.http.HttpServletRequest;
import org.jasig.cas.client.session.SingleSignOutFilter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.security.authentication.AuthenticationDetailsSource;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.ProviderManager;
import org.springframework.security.cas.ServiceProperties;
import org.springframework.security.cas.authentication.CasAuthenticationProvider;
import org.springframework.security.cas.web.CasAuthenticationFilter;
import org.springframework.security.cas.web.authentication.ServiceAuthenticationDetails;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.security.web.authentication.logout.LogoutFilter;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import es.um.atica.----.cas.handlers.CustomizedLogoutHandler;
import es.um.atica.----.cas.handlers.CustomizedUrlAuthenticationSuccessHandler;
@EnableWebSecurity
@EnableGlobalMethodSecurity( securedEnabled = true )
public class ConfiguracionSeguridad extends WebSecurityConfigurerAdapter {
private static final org.apache.logging.log4j.Logger log = org.apache.logging.log4j.LogManager
.getLogger( ConfiguracionSeguridad.class );
private final AuthenticationProvider authenticationProvider;
private final LogoutFilter logoutFilter;
private final SingleSignOutFilter singleSignOutFilter;
private final AuthenticationEntryPoint authenticationEntryPoint;
private final ServiceProperties serviceProperties;
@Value( "${es.um.p15s.environment}" )
private String environment;
@Autowired
public ConfiguracionSeguridad ( AuthenticationEntryPoint aep, LogoutFilter lF, SingleSignOutFilter ssF,
CasAuthenticationProvider casAuthenticationProvider, ServiceProperties sp ) {
authenticationProvider = casAuthenticationProvider;
logoutFilter = lF;
singleSignOutFilter = ssF;
authenticationEntryPoint = aep;
serviceProperties = sp;
}
// Autenticacion básica con usuario autogenerado
@Override
protected void configure( HttpSecurity http ) throws Exception {
http.cors().and().csrf().disable().addFilter( casAuthenticationFilter( serviceProperties ) ).authorizeRequests()
.regexMatchers( "^/entrada(\\/)?(\\?.+)?$" ).authenticated().and().authorizeRequests()
.regexMatchers( "/public/*", "/private/*" ).permitAll().and().httpBasic()
.authenticationEntryPoint( authenticationEntryPoint ).and().logout()
.logoutSuccessHandler( customizedLogoutHandler() ).deleteCookies( SESSION_TOKEN_HEADER ).and()
.addFilterBefore( singleSignOutFilter, CasAuthenticationFilter.class )
.addFilterBefore( logoutFilter, LogoutFilter.class );
}
@Bean
CorsConfigurationSource corsConfigurationSource() {
final CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowedOriginPatterns( Arrays.asList( "*" ) );
configuration.setAllowedMethods( Arrays.asList( "*" ) );
configuration.setAllowedHeaders( Arrays.asList( "*" ) );
configuration.setAllowCredentials( true );
final UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration( "/**", configuration );
return source;
}
@Bean
public CustomizedLogoutHandler customizedLogoutHandler() {
return new CustomizedLogoutHandler();
}
@Override
protected void configure( AuthenticationManagerBuilder auth ) throws Exception {
auth.authenticationProvider( authenticationProvider );
}
@Override
protected AuthenticationManager authenticationManager() throws Exception {
return new ProviderManager( Arrays.asList( authenticationProvider ) );
}
@Bean
AuthenticationDetailsSource<HttpServletRequest, ServiceAuthenticationDetails> dynamicServiceResolver() {
return ( HttpServletRequest context ) -> {
String urlProtocol = "http:";
if ( !LOCAL_ENV.equals( environment ) ) {
urlProtocol = "https:";
}
final String url = context.getRequestURL().toString().replace( "http:", urlProtocol );
log.error( "-- ORIGINAL Dynamic URL: " + context.getRequestURL().toString() + " ---" );
return new ServiceAuthenticationDetails() {
private static final long serialVersionUID = 1L;
@Override
public String getServiceUrl() {
log.error( "-- Dynamic URL: " + url + " ---" );
return url;
}
};
};
}
@Bean
public CasAuthenticationFilter casAuthenticationFilter( ServiceProperties sP ) throws Exception {
final CasAuthenticationFilter filter = new CasAuthenticationFilter();
filter.setServiceProperties( sP );
filter.setAuthenticationManager( authenticationManager() );
filter.setFilterProcessesUrl( "/entrada/*" );
filter.setAuthenticationDetailsSource( dynamicServiceResolver() );
filter.setAuthenticationSuccessHandler( new CustomizedUrlAuthenticationSuccessHandler() );
return filter;
}
} |
Descargar clases necesariasPodemos descargar estas clases, y el resto de clases necesarias aquí: Clases cliente CAS.rar. Se incluyen las entidades correspondientes a base de datos, y sus correspondientes repositorios. La estructura de paquetes del proyecto, incluyendo el cliente cas, quedaría así: | Paquete | Clases |
|---|
| es.um.atica.proyecto | Constants.java, NombreProyectoApplication.java, SecurityConfig.java | | es.um.atica.proyecto.cas.entities | Objetivos.java, Roles.java, RolesObjetivos, RolesUsuario.java, TokenSession.java, Usuarios.java | | es.um.atica.proyecto.cas.exceptions | ServicioException.java, ServicioNotFoundException.java, TokenExpiredException.java, UnauthorizedException.java, UsuarioNotFoundException.java | | es.um.atica.proyecto.cas.handlers | CustomizedLogoutHandler.java, CustomizedUrlAuthenticationSuccessHandler.java | | es.um.atica.proyecto.cas.model | ServiceUserDetails.java | | es.um.atica.proyecto.cas.repositories | ObjetivosRepository.java, RolesRepository.java, RolesObjetivosRepository, RolesUsuarioRepository.java, TokenSessionRepository.java, UsuariosRepository.java | | es.um.atica.proyecto.cas.rest | SecuredEndpoint.java, SessionManagementendPoint.java, UserEndpoint.java | | es.um.atica.proyecto.cas.services | UsuarioService.java | | es.um.atica.proyecto.cas.util | JwtTokenUtil.java, SessionTokenUtil.java, Util.java |
Podemos descargarlas y meterlas en nuestro proyecto, cambiando los nombres de los paquetes para que coincidan con los de nuestro proyecto, así como los datos de las tablas de base de datos en las entidades. |