Versiones comparadas

Clave

  • Se ha añadido esta línea.
  • Se ha eliminado esta línea.
  • El formato se ha cambiado.

...

Bloque de código
languagesql
titleQuery para crear tabla "CAS_TOKEN_SESSION"
collapsetrue
  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" ;

...

Tendremos que añadir estas dependencias al pom.xml:

Bloque de código
languagexml
themeEclipse
titleDependencias pom.xml
collapsetrue
        <!-- 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>

...

En 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
titleapplication.properties
collapsetrue
login.config.serviceurl=httphttps://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
es.um.jwt.expires=15
# Para suplantacion, todavia no esta lista
#esTiempo de vida del token (en minutos)
es.um.jwt.expiresImpersonationexpires=1015
server.servlet.session.persistent=false
# Entorno - Se comprobara si esta variable es "local" o "desarrollo"
es.um.p15s.environment=local

...

Bloque de código
languagejava
themeEclipse
titleClase inicial - NombreProyectoApplication.java
linenumberstrue
collapsetrue
package 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();
	}
}

...

Nuestra clase SecurityConfig 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
languagejava
themeEclipse
titleConfiguración de seguridad - SecurityConfig.java
linenumberstrue
collapsetrue
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;
    }
 
}

Constantes

Se utilizan una serie de constantes que nos definimos en un archivo Constans.java situado en el paquete raíz, el mismo que la clase que inicia la aplicación, y contendrá, al menos, lo siguiente (podemos añadir constantes que nos hagan falta para otras cosas del proyecto):

Bloque de código
languagejava
package es.um.atica.----;


public final class Constants {

	private Constants() {}

	public static final String IS_ADMIN = "isAdmin";

	public static final String USER_PARAM = "user";

	public static final String SLASH = "/";

	public static final String DEFAULT_CALLBACK = "/";

	public static final String LOCAL_ENV = "local";

	public static final String DEV_ENV = "desarrollo";

	public static final String TEST_ENV = "test";

	public static final String APIUM_DEV_ENV = "DESA";

	public static final String APIUM_TEST_ENV = "TEST";

	public static final String PUBLIC_PREFIX = "/public";

	public static final String PRIVATE_PREFIX = "/private";

	public static final String ROLE_ADMIN = "ROLE_ADMIN";

	public static final String ROLE_ANONYMOUS = "ROLE_ANONYMOUS";

	public static final String ROLE_USER = "ROLE_USER";

	public static final String AUTHORIZATION_HEADER = "Authorization";

	public static final String CLAIMS_PARAM = "claims";

	public static final String ROLENAME_PARAM = "roleName";

	public static final String IMPERSONATOR_PARAM = "impersonator";

	public static final String SESSION_TOKEN_HEADER = "refresh-token";

	public static final String EMAIL_PARAM = "email";

	public static final String USER_AGENT_HEADER = "User-Agent";

	public static final String LANGUAGE_PARAM = "inLanguage";

	public static final String REFERER_HEADER = "referer";

	public static final String CAS_ATTR_LDAP_GROUPS = "ldapGroups";

	public static final String CAS_ATTR_PLACE = "place";

	public static final String CAS_ATTR_GENDER = "gender";

}

Resto de clases


Bloque de código
languagejava

Resto de clases

El resto de clases se pueden descargar aquí: Clases cliente CAS.rar. Se incluyen El resto de clases se pueden descargar 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í:

...

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.

Cómo comprobar el token

Recibiremos el token por parte del front-end como un String, en el header Authorization, que tendrá la siguiente forma:

Bloque de código
languagejava
themeEclipse
# Bearer token
"Bearer asdffsdfasdfasdfasdfasdfasdfasdf"

En nuestros métodos REST privados, por lo tanto, tendremos que incluir el header Authorization (constante AUTHORIZATION_HEADER) como requerido, Para recibir el token en nuestro método REST, lo haremos en el header Authorization de la petición, y lo especificaremos en nuestros métodos Java con @RequestHeader, y podemos comprobarlo de la siguiente manera:

Bloque de código
languagejava
themeEclipse
linenumberstrue
collapsetrue
import static es.um.atica.----.Constants.AUTHORIZATION_HEADER;

@GetMapping("/titulaciones")
public ResponseEntity<List<TitulacionDTO>> titulaciones(@RequestHeader(value="Authorization"AUTHORIZATION_HEADER, required = true) String tokenCodificado) {
	// Comprobamos el token
	try {
		jwtTokenUtil.getClaim( tokenCodificado );
	} catch( final ServicioException e ) {
		log.error( e.getLocalizedMessage() );
		return new ResponseEntity<>( HttpStatus.FORBIDDEN );
	}
}

En el método getClaim se comprueba si el token es correcto, y si lo es devuelve los datos que contiene. Este método se incluye en la clase JwtTokenUtil que viene en el cliente CAS, y si lo hacemos con el Portal de Servicios podemos incluirla así:

...

Bloque de código
languagejava
themeEclipse
linenumberstrue
collapsetrue
/**
 * ServicioNotFoundException
 */
public class ServicioException extends RuntimeException {
 
	private static final long serialVersionUID = 1L;
 
	private static final HttpStatus status = HttpStatus.BAD_REQUEST;
 
	protected ServicioException( String msg ) {
		super( msg );
	}
 
	public HttpStatus getStatus() {
		return ServicioException.status;
	}
 
}

...

Bloque de código
languagejava
themeEclipse
linenumberstrue
collapsetrue
/**
 * ServicioNotFoundException
 */
public class TokenExpiredException extends ServicioException {
 
	private static final long serialVersionUID = 1L;
 
	private static final HttpStatus status = HttpStatus.UNAUTHORIZED;
 
	public TokenExpiredException() {
		super( "Token_expired" );
	}
 
	@Override
	public HttpStatus getStatus() {
		return TokenExpiredException.status;
	}
 
}

...

Bloque de código
languagejava
themeEclipse
linenumberstrue
collapsetrue
/**
 * ServicioNotFoundException
 */
public class UnauthorizedException extends ServicioException {
 
	private static final long serialVersionUID = 1L;
 
	private static final HttpStatus status = HttpStatus.UNAUTHORIZED;
 
	public UnauthorizedException() {
		super( "No tiene acceso para solicitar este servicio " );
	}
 
	@Override
	public HttpStatus getStatus() {
		return UnauthorizedException.status;
	}
 
}

...