Mostrando entradas con la etiqueta spring security. Mostrar todas las entradas
Mostrando entradas con la etiqueta spring security. Mostrar todas las entradas

viernes, 18 de febrero de 2022

Autenticación con Spring Boot y JWT

Hoy vamos a ver como realizar autenticación de una API REST con JWT y Spring Boot. Explicaremos cómo generar el token y como usar ese mismo token para acceder a recursos de la misma API. Paso a paso veremos los componentes claves para realizar el ejemplo completo.

Para el ejemplo utilizaremos las siguientes librerías:

  • Spring Boot: 2.6.3
    • spring-boot-starter-security
    • spring-boot-starter-web
  • java-jwt: 3.18.3

El primer paso será crear la configuración de seguridad que nos permita indicar la configuración de nuestra API, en base a los siguientes componentes:

  1. @EnableWebSecurity: Permite habilitar la seguridad en nuestra API.
  2. WebSecurityConfigurerAdapter: Nos permite sobrescribir el comportamiento por defecto de la seguridad proporcionada por Spring. 
  3. CORS: Habilitamos la configuración por defecto y que podemos sobrescribir a través del bean CorsFilter.
  4. SessionCreationPolicy: Define la API como Stateless evitando la creación de HTTPSession y deshabilitamos el uso de cookies.
  5. CSRF: Al configurar la API como stateless no necesitamos el uso de cookies. 
  6. Configuramos la respuesta de la API en caso de error en la autenticación
  7. Declaramos como públicas los métodos que permiten el login y la obtención de tokens JWT e implementamos la seguridad para el resto de métodos. 
  8. Añadimos el filtro que compruebe y valide si la petición tiene un token JWT.
@EnableWebSecurity // 1
public class SecurityConfig extends WebSecurityConfigurerAdapter { // 2

	@Autowired
	private JwtTokenFilter jwtTokenFilter;

	@Override
	protected void configure(HttpSecurity http) throws Exception {
		// Enable CORS and disable CSRF. 3 and 5
		http = http.cors().and().csrf().disable();

		// Set session management to stateless. 4
		http = http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and();

		// Set unauthorized requests exception handler. 6
		http = http.exceptionHandling().authenticationEntryPoint((request, response, ex) -> {
			response.sendError(HttpServletResponse.SC_UNAUTHORIZED, ex.getMessage());
		}).and();

		// Set permissions on endpoints. 7
		http.authorizeRequests()
				// Our public endpoints
				.antMatchers("/login", "/refresh-token").permitAll()
				// Our private endpoints
				.anyRequest().authenticated();

		// Add JWT token filter. 8
		http.addFilterBefore(jwtTokenFilter, UsernamePasswordAuthenticationFilter.class);
	}

	// Used by spring security if CORS is enabled. 3
	@Bean
	public CorsFilter corsFilter() {
		UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
		CorsConfiguration config = new CorsConfiguration();
		config.setAllowCredentials(true);
		config.setAllowedOrigins(Arrays.asList("*"));
		config.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE"));
		config.setAllowedHeaders(Arrays.asList("authorization",  "Authorization", 
			"content-type","Content-Type", "Access-Control-Allow-Origin", 
			"Access-Control-Allow-Headers", "Access-Control-Allow-Methods"));
		source.registerCorsConfiguration("/**", config);
		return new CorsFilter(source);
	}
}

El siguiente paso será crear el filtro JWT que hemos visto en el paso anterior. El filtro permitirá que las llamadas que se realicen a nuestra API que no tengan la cabecera de autenticación con el token JWT sean rechazadas y devuelvan error 401. En el cual se realizarán los siguientes pasos:
  • Obtener el token de la request y validarlo con la utilidad JwtTokenUtil.
  • Obtener los datos del usuario a través provisto por el propio Token.
  • Asociar los datos del usuario al contexto de Spring Security
@Component
public class JwtTokenFilter extends OncePerRequestFilter {
	@Autowired
	private JwtTokenUtil jwtTokenUtil;

	@Override
	protected void doFilterInternal(final HttpServletRequest request, final HttpServletResponse response,
			final FilterChain chain) throws ServletException, IOException {
		// Get authorization header and validate
		final String header = request.getHeader(HttpHeaders.AUTHORIZATION);
		if (ObjectUtils.isEmpty(header) || !header.startsWith("Bearer ")) {
			chain.doFilter(request, response);
			return;
		}
		// Get jwt token and validate
		final String token = header.split(" ")[1].trim();
		if (!jwtTokenUtil.validate(token)) {
			chain.doFilter(request, response);
			return;
		}
		// Obtains UserDetails from the token itself
		UserDetails userDetails = jwtTokenUtil.getUserDetails(token);
		// Associated UserDetails to Security Context
		UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null,
				userDetails.getAuthorities());
		authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
		SecurityContextHolder.getContext().setAuthentication(authentication);
		// Continue with the filter chain
		chain.doFilter(request, response);
	}
}

Como hemos indicado vamos a utilizar la librería java-jwt, aunque hay muchas otras disponibles y todas funcionan de manera similar debido a que siguen la especificación RFC 7519. 

Recordemos que JWT es un token dividido en tres partes: Cabecera, Cuerpo y Firma. Y que la tercera es la encriptación de las dos anteriores. Para realizar esta encriptación utiliza un algoritmo que le indiquemos y encripta el contenido del token con ayuda de una clave secreta. Esta clave secreta estará almacenada en nuestro ejemplo en el fichero jwt-secret.

Además otra de las características del propio token es que nos permite almacenar información en forma de Claims. Las cuales puede contener información de distintos ambitos pero principalmente asociada al usuario. En nuestro caso crearemos un token con las siguientes caracteristicas:
  • issuer: Claim por defecto asociado al identificador de nuestra aplicación
  • subject: Claim por defecto asociado al nombre de usuario
  • id: Identificador del usuario
  • roles: Listado de roles asociados al usuario
  • IssuedAt: Fecha en la cual se ha generado el token. 
  • ExpireAt: Fecha a partir de la cual el token no será válido.
public class JwtTokenUtil {
	private Algorithm algorithmHS;
	private JWTVerifier verifier;
	@Value("${jwt.api.issuer}")
	private String loginApiIssuer;
	@Value("${jwt.api.jwtExpiration}")
	private int jwtExpiration;

	public String generateAccessToken(final Long userId, final String userName, final List<String> roles) {
		return JWT.create().withIssuer(loginApiIssuer).withSubject(userName).withIssuedAt(getIssuedAt())
				.withClaim("id", userId).withClaim("roles", roles).withExpiresAt(getExpireAt()).sign(getAlgorithmHS());
	}
	private Algorithm getAlgorithmHS() {
		if (algorithmHS == null) {
			try {
				File resource = new ClassPathResource("jwt-secret").getFile();
				String text = new String(Files.readAllBytes(resource.toPath()), Charset.defaultCharset());
				algorithmHS = Algorithm.HMAC512(text);
			} catch (IOException e) {
				// Invalid signature/claims
				log.error("Error getting expiration date: " + e.getMessage(), e);
			}
		}
		return algorithmHS;
	}
	public DecodedJWT getDecodedJWT(final String token) {
		DecodedJWT jwt = null;
		try {
			jwt = getVerifier().verify(token);
		} catch (JWTVerificationException e) {
			// Invalid signature/claims
			log.error("Error validating token JWT: " + e.getMessage(), e);
		}
		return jwt;
	}
	public UserDetails getUserDetails(final String token) {
		DecodedJWT decoded = getDecodedJWT(token);
		return JWTUserDetailsImpl.build(decoded.getClaim("id").asLong(), decoded.getSubject(),
				decoded.getClaim("roles").asList(String.class));
	}
	private JWTVerifier getVerifier() {
		if (verifier == null) {
			verifier = JWT.require(getAlgorithmHS()).withIssuer(loginApiIssuer).build();
		}
		return verifier;
	}
	public boolean validate(final String token) {
		return getDecodedJWT(token) != null;
	}
        //more utilities
}

Ya solo nos queda el método de login con el cual podremos crear el token a través de los datos identificativos del usuario. Para obtener y almacenar los datos de usuario, Spring provée de un par de interfaces, como son UserDetailsService y UserDetails. La información a obtener asociada al usuario, permitirá la autenticación y autorización del mismo. UserDetailsService obtendrá los datos del usuario puede ser una BBDD u otra fuente. 

@RestController
public class LoginController {
	@Autowired
	MockDetailsService mockDetailsService;
	@Autowired
	JwtTokenUtil jwtTokenUtil;

	private ResponseEntity<LoginResponse> getLoginResponseFromUserDetails(final JWTUserDetailsImpl userDetails) {
		List<String> roles = userDetails.getAuthorities().stream()
                        .map(GrantedAuthority::getAuthority)
			.collect(Collectors.toList());
		String token = jwtTokenUtil.generateAccessToken(userDetails.getId(), 
                        userDetails.getUsername(), roles);
		return new ResponseEntity<>(new LoginResponse(token), HttpStatus.OK);
	}
	@PostMapping("/login")
	public ResponseEntity<LoginResponse> login(@RequestBody final LoginRequest request) {
		JWTUserDetailsImpl userDetails = (JWTUserDetailsImpl) mockDetailsService
				.loadUserByUsername(request.getUserName());
		return getLoginResponseFromUserDetails(userDetails);
	}
	@PostMapping("/refresh-token")
	public ResponseEntity<LoginResponse> refreshToken(@RequestBody final String token) {
		JWTUserDetailsImpl userDetails = (JWTUserDetailsImpl) jwtTokenUtil.getUserDetails(token);
		return getLoginResponseFromUserDetails(userDetails);
	}

}

Como ultima aclaración, indicar que en el filtro no necesitaremos volver a recurrir a la fuente de datos para obtener los datos del usuario. Puesto que como ya hemos indicado el token JWT ya contiene la información del usuario y es más eficiente obtener los datos del usuario del propio JWT.

Como siempre, podréis consultar el código aqui. 

jueves, 2 de marzo de 2017

Spring Security: Redirección tras login

En un post anterior ya hemos explicado como realizar el login con usuario/contraseña y con redes sociales. Pero en ese ejemplo siempre volvíamos a una página fija y eso no siempre le da la mejor experiencia al usuario. Por eso en este post vamos a tratar como realizar la redirección tras intentar logarse tanto si va bien, como si no. 

El primer paso es modificar la configuración de Spring Security para indicarle que serán clases nuestras las que se encargen de la redirección

@Bean
public SavedRequestAwareAuthenticationSuccessHandler successHandler() {
   SavedRequestAwareAuthenticationSuccessHandler successHandler = new SavedRequestAwareAuthenticationSuccessHandler();
   successHandler.setUseReferer( true );
   return successHandler;
}
@Bean
public SimpleUrlAuthenticationFailureHandler failureHandler() {
   return new CustomAuthenticationFailureHandler();
}
@Override
protected void configure( final HttpSecurity http ) throws Exception {
// @formatter:off
// configures form login
http.formLogin().loginPage( "/login").permitAll().successHandler( successHandler() ).failureHandler( failureHandler() )
// more configuration
...
// @formatter:on
}

Para el caso del login, si lo único que queremos hacer es que redirija a la página hacia la cual se redirigía el usuario al saltar el login, tendremos que poner la propiedad 'UseReferer' a 'true'. Si queremos alguna acción más, deberemos crear una clase que implemente 'SavedRequestAwareAuthenticationSuccessHandler'. Ejemplo:

@Component
public class CustomAuthenticationSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler {
 @Override
 public void onAuthenticationSuccess( final HttpServletRequest request, final HttpServletResponse response, final Authentication authentication ) throws IOException, ServletException {
  CustomUserDetails user = (com.example.app.CustomUserDetails) authentication.getPrincipal();
  request.getSession().setAttribute( SESSION.USER_LANGUAGE_ID, user.getLanguageId() );
  super.onAuthenticationSuccess( request, response, authentication );
 }
}

Para el caso de logout es muy similar, pero la redirección no podremos hacerla automática sino que tendremos que implementar una clase de la API de Spring que realice dicha lógica. En nuestro ejemplo le añadiremos la coletilla de que ha ocurrido error durante el login, para que lo muestre en la pantalla desde la cual ha intentado logarse.


@Component
public class CustomAuthenticationFailureHandler extends SimpleUrlAuthenticationFailureHandler {
 @Override
 public void onAuthenticationFailure( final HttpServletRequest request, final HttpServletResponse response, final AuthenticationException exception ) throws IOException, ServletException {
  String referer =  request.getHeader( "referer" );
  String redirectUrl = StringUtils.substringBeforeLast( referer, "?" ) + "?login_error=1";
  redirectUrl = response.encodeRedirectURL( redirectUrl );
  response.sendRedirect( redirectUrl );
 }
}

domingo, 19 de febrero de 2017

Spring Social: SignIn y SignUp


En este post intentaremos explicar de la forma más breve y clara como utilizar Spring Social en tu aplicación. Y como permitir logarse a los usuarios de una red social (en este caso Facebook), e incluso registrarlos en la aplicación, si es la primera vez que acceden. 

El post se centrará exclusivamente en el funcionamiento de Spring Social (pudiendose explicar alguna otra sección si alguien lo pide). El código lo podéis encontrar aquí: Github. 
  • 1º paso: Obtener un ID para tu aplicación
Debemos acceder a Facebook Developers y crear una aplicación asociada a nuestro usuario. A esta aplicación le tenemos que dar permiso para autenticación con Facebook, y entre los datos a solicitar debemos pedirle el 'email'. 

El identificador de facebook y su clave secreta se deben añadir al fichero application.properties (el fichero de configuración de Spring Boot). 
  • 2º paso: Indicar a Spring Security que vamos a hacer uso de Spring Social
Utilizaremos Spring Security para que maneje la autenticación de usuarios, y a que páginas se puede o no acceder. Con la siguiente configuración, permitiremos que los usuarios de redes sociales se integren dentro del ciclo de autenticación.

protected void configure( final HttpSecurity http ) throws Exception {
http.authorizeRequests()
//... mas configuraciones aqui
.and().apply( new SpringSocialConfigurer().postLoginUrl( "/" ).alwaysUsePostLoginUrl( true ) );
}
  • 3º paso: Configurar Spring Social
Debemos crear una clase que implemente 'SocialConfigurer'. Esta clase contendrá la anotación '@EnableSocial' que permitirá añadir esta configuración a la propia de Spring MVC. Además tendrá tres métodos esenciales. 
- addConnectionFactories: No necesitaremos configurarlo al estar utilizando Spring Boot y haber configurado anteriormente el fichero 'application.properties' . 
- getUserIdSource: Donde indicaremos que campo usaremos como identificador del usuario. La implementación por defecto que se suele hacer, nos permite indicar que será el 'username'. Esto nos dará algún quebradero de cabeza mas adelante, puesto que Facebook no cuenta con un username como tal. 
- getUsersConnectionRepository: Donde indicaremos que la información de las conexiones será persistida en BBDD y que en caso de que el usuario no se haya logado nunca se redirija a un clase concreta (CustomConnectionSignUp) para registrarlo en la aplicación. 

@Configuration
@EnableSocial
public class SocialConfig implements SocialConfigurer {
 @Autowired
 private DataSource dataSource;
 @Autowired
 private UserDao dao;
 @Override
 public void addConnectionFactories( final ConnectionFactoryConfigurer connectionFactoryConfigurer, final Environment environment ) {
 }
 @Override
 public UserIdSource getUserIdSource() {
  return new AuthenticationNameUserIdSource();
 }
 @Override
 public UsersConnectionRepository getUsersConnectionRepository( final ConnectionFactoryLocator connectionFactoryLocator ) {
  JdbcUsersConnectionRepository repository = new JdbcUsersConnectionRepository( dataSource, connectionFactoryLocator, Encryptors.noOpText() );
  repository.setConnectionSignUp( new CustomConnectionSignUp( dao ) );
  return repository;
 }
}

La tabla donde se persitirán los datos es 'userconnection' y almacenará la información de conexión de los usuarios a través de sus redes sociales. Si encuentra al usuario en esta tabla, procederá a logarlo en la aplicación y en caso contrario procederá a registrarlo y posteriormente logarlo. El esquema de la misma se encuentra también en el código de Github. 
  • 4º Paso: Registrar usuario de red social automáticamente
Ya hemos configurado la clase que permite el registro, ahora nos queda implementarla. Básicamente nuestra labora será obtener los datos de Facebook, crear un registro del usuario de la aplicación con ellos y devolver el identificador de dicho usuario. Como ya indicamos anteriormente, un usuario de Facebook no tiene 'username' como tal, pero podemos utilizar su correo electrónico como identificador. 

public class CustomConnectionSignUp implements ConnectionSignUp {
 private final UserDao userDao;
 public CustomConnectionSignUp( final UserDao usersDao ) {
  userDao = usersDao;
 }
 @Override
 public String execute( final Connection<?> connection ) {
  Facebook facebook = (Facebook) connection.getApi();
  String[] fields = { "email", "name", "first_name", "last_name" };
  User userProfile = facebook.fetchObject( "me", User.class, fields );
  // seteamos a nulo para poder insertar AI MYSQL
  userProfile.setId( null );
  // generamos password
  userProfile.setPassword( createEncryptPassword() );
  // indicamos como username el correo
  userProfile.setUsername( userProfile.getEmail() );
  userProfile = userDao.save( userProfile );
  return userProfile.getEmail();
 }
 private String createEncryptPassword() {
  return new BCryptPasswordEncoder().encode( RandomStringUtils.randomAlphanumeric( 17 ) );
 }
}
  • 5º Paso: Identificar al usuario de la red social en la aplicación y logarlo
Deberemos crear una clase que implemente 'SocialUserDetailsService' y que permita logar al usuario proveniente de la red social. El parámetro 'userId' que llega al método 'loadUserByUserId', será el valor devuelto por la clase 'CustomConnectionSignUp'. Y el objeto UserDetails devuelto, será el asociado a la sesión del usuario a través de Spring Security.

El código de Github puede ser algo diferente, pero es porque para realizar esta parte nos hemos apoyado en 'UserDetailsService'. Y de paso nos sirve su implementación para realizar el acceso del usuario a través de usuario y contraseña.

@Service
public class CustomSocialUsersDetailService implements SocialUserDetailsService {
 @Autowired
 private UserDao userDao;
 public UserDetails loadUserByUsername( final String username ) throws UsernameNotFoundException {
  User user = userDao.findByUsernameOrEmail( username, username );
  if( user == null ) {
   throw new UsernameNotFoundException( "No user present with username/email: " + username );
  } else {
   List<String> userRoles = new ArrayList<>();
   userRoles.add( "USER" );
   return new CustomUserDetails( user, userRoles );
  }
 }
 @Override
 public SocialUserDetails loadUserByUserId( final String userId ) throws UsernameNotFoundException, DataAccessException {
  UserDetails userDetails = loadUserByUsername( userId );
  return new SocialUser( userDetails.getUsername(), userDetails.getPassword(), userDetails.getAuthorities() );
 }
}
  • 6º Paso: Habilitar enlace para logarse a través de Facebook
Spring social pone a nuestro alcance diferentes método de conexión con las redes sociales y que son manejados de manera automática por el. Para nuestro caso, y para poder autenticar al usuario en nuestro sistema a través de la red social disponemos de 'auth'. Si creamos un enlace o un formulario que apunte a la dirección '/auth/{providerId}', donde '{providerId}' es el identificador de nuestra red social devidamente configurada, podremos autenticarnos a través de ella. Hay que tener en cuenta que no vale cualquier red social.


<form action="${pageContext.request.contextPath}/auth/facebook" method="POST">
 <input type="hidden" name="scope" value="email, public_profile" />
 <p><button class="btn btn-default col-md-12 col-sm-12 col-xs-12 btnSignFacebook" type="submit"><span class="fb-icon"></span><spring:message code="b.signFacebook" /></button></button></p>
 <input type="hidden" name="${_csrf.parameterName}" value="${_csrf.token}" />
</form>

sábado, 8 de octubre de 2016

Como realizar la Auditoria de BBDD con Spring Data y Spring Boot


Una tarea recurrente en el mundo del Backend a nivel de BBDD, es que las tablas tengan campos de auditoria (quien ha creado o editado el registro, y cuando). Y otra tarea recuerrente es obtener los datos del usuario para hacer registrar sus datos en los insert o updates. 

Gracias a Spring Data podremos hacerlo con poco más que una anotación. Y a continuación te indicaré como realizarlo con Spring Boot. 

Supongamos que cada tabla tiene los capos 'CREATE_USER', 'MODIFY_USER', 'CREATE_DATE' y 'MODIFY_DATE' (que nos permiten lo anteriormente comentado). Estos campos los crearemos en una clase de la cual extenderán el resto de clases JPA (con idea de reducir código). Y tendran unas etiquetas específicas asociadas '@CreatedBy', '@LastModifiedBy', '@CreatedDate' y '@LastModifiedDate'.Esta será la clase:

@MappedSuperclass
public abstract class GenericEntity<K extends Serializable> implements Serializable {
 protected Date createDate;
 protected Date modifyDate;
 protected Integer createUser;
 protected Integer modifyUser;

 @Temporal( TemporalType.DATE )
 @Column( name = "CREATE_DATE" )
 @CreatedDate
 public Date getCreateDate() {
  return createDate;
 }

 @Temporal( TemporalType.DATE )
 @Column( name = "MODIFY_DATE" )
 @LastModifiedDate
 public Date getModifyDate() {
  return modifyDate;
 }

 @Column( name = "CREATE_USER" )
 @CreatedBy
 public String getCreateUser() {
  return createUser;
 }

 @Column( name = "MODIFY_USER" )
 @LastModifiedBy
 public String getModifyUser() {
  return modifyUser;
 }
}

Para que funcione las anotaciones, en el caso de los campos fecha no debemos hacer nada más. Pero para poder obtener  la información del  usuario deberemos hacer uso de la interfaz AuditorAware<T> (Siendo T el tipo de valor a insertar en los campos de 'CREATE_USER' y 'MODIFY_USER'). La clase que lo implemente, deberá obtener los datos de Spring Security y devolver el dato que deseamos almacenar en BBDD (el nombre del usuario, el identificador del mismo, etc).

@Component
public class MyOwnAuditorAware implements AuditorAware<String> {
 @Override
 public String getCurrentAuditor() {
  Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
  if( authentication == null || !authentication.isAuthenticated() ) {
   return null;
  }
  return ((CustomUserDetails) authentication.getPrincipal()).getUsername();
 }
}

El último paso será configurar en Spring Boot para que haga uso de el, Esto lo podemos hacer de la siguiente forma

@EnableJpaAuditing( auditorAwareRef = "myOwnAuditorAware" )
public class WebMvcConfig extends WebMvcConfigurerAdapter {

Y en la propia clase, con la anotación 'AuditingEntityListener', 

sábado, 13 de agosto de 2016

Spring Boot Security: nivel básico

En este post, vamos a intentar explicar como configurar de forma básica Spring Security 4 con Spring Boot. Accediendo a los roles a través de JDBC y con los passwords encriptados. Se puede configurar en 4 sencillas clases y tenéis todo el código aquí (ExampleSpringSecurity) (en src/test/resources teneis incluidos los scripts BBDD).

1. Application: Esta clase tendrá la anotación 'SpringBootApplication'  y un método main que nos permitirá arrancar la aplicación.
2. MvcConfig: En esta clase tendremos la configuración para la navegación url-pantallas/vistas. Y también indicaremos a través de 'viewResolver' como resolver las  el viewResolver, que nos permitirá indicar la pantalla/vista.


@Override
public void addViewControllers(final ViewControllerRegistry registry) {
 registry.addViewController("/home").setViewName("home");
 registry.addViewController("/").setViewName("home");
 registry.addViewController("/hello").setViewName("hello");
 registry.addViewController("/login").setViewName("login");
 registry.addViewController("/403").setViewName("403");
}
@Bean
public InternalResourceViewResolver viewResolver() {
 InternalResourceViewResolver resolver = new InternalResourceViewResolver();
 resolver.setPrefix("/WEB-INF/jsp/");
 resolver.setSuffix(".jsp");
 return resolver;
}

En el código podemos ver que si indicamos una vista denominada 'hello', buscará y redireccionará a la pantalla /WEB-INF/jsp/hello.jsp'.

3. AuthenticationProviderConfig: En esta clase declararemos el 'dataSource' que nos permitirá conectarnos a la BBDD. Y lo mas importante el bean 'userDetailService', en el cual implementaremos como obtener el usuario y contraseña almacenados en BBDD y como obtener los roles asociados al usuario.

@Bean(name = "userDetailsService")
public UserDetailsService userDetailsService() {
 JdbcDaoImpl jdbcImpl = new JdbcDaoImpl();
 jdbcImpl.setDataSource(dataSource());
 String usersByUserNameQuery = "select username,password, enabled from users where username=?";
 String authoritiesByUsernameQuery = "select u.username, ur.authority from user_roles ur, users u where username=? and u.user_id = ur.user_id";
 jdbcImpl.setUsersByUsernameQuery(usersByUserNameQuery);
 jdbcImpl.setAuthoritiesByUsernameQuery(authoritiesByUsernameQuery);
 return jdbcImpl;
}

4. WebSecurityConfig: En esta clase realizaremos la configuración propia de Spring Security, en el aspecto de que indicamos como se va a realizar la obtención de los datos (a través de userDetailsService). Como vamos a realizar la encriptación de la contraseña (a través de una clase incluida dentro de Spring como es: BCryptPasswordEncoder). Y por último configurar todas las urls de acceso de la aplicación e indicar si son accesible por todos los usuarios, si solo son accesible por un determinado rol y configuraciones normales como cuales son las paginas de login y logout.

@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
 @Autowired
 UserDetailsService userDetailsService;
 @Bean(name = "passwordEncoder")
 public PasswordEncoder passwordencoder() {
  return new BCryptPasswordEncoder();
 }
 @Autowired
 public void configAuthentication(final AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService).passwordEncoder(passwordencoder());
 }
 @Override
 protected void configure(final HttpSecurity http) throws Exception {
http.authorizeRequests().antMatchers("/hello").access("hasRole('ROLE_ADMIN')").anyRequest()
    .permitAll()
    .and().formLogin().loginPage("/login").usernameParameter("username")
    .passwordParameter("password")
    .and().logout().logoutSuccessUrl("/login?logout")
    .and().exceptionHandling().accessDeniedPage("/403")
    .and().csrf();
 }
}

Toda la configuración se puede crear en un único fichero sin problemas. Pero la idea de realizarlo así es aumentar la claridad en la configuración de la aplicación.

Este ejemplo lo he sacado de esta Web, donde además podemos ver un post algo más complejo con JPA Data.

viernes, 15 de julio de 2016

Como obtener datos del usuario tras la autenticación con Spring

En este post hablaremos de como podemos manejar la información del usuario después de que se haya autenticado a través de Spring Security y haciendo uso de uno de sus manejadores.

La idea nace de la necesidad de almacenar determinada información en la sesión del usuario pero haciendo uso de datos específicos que se obtienen una vez que se ha logado en la aplicación.

Se probaron diferentes soluciones como un HttpSessionListener o el uso de filtros, pero no nos valía al no tener unos datos u otros en cada una de las opciones.

La solución se ha implementado haciendo uso de ficheros XML y no anotaciones pero la lógica debe ser básicamente la misma. Para empezar comenzamos configurando el fichero  de configuración de Spring. Debemos indicarle al Filtro encargado de la autenticación en spring-security (en nuestro caso un CAS) que tras una autenticación correcta la lógica pasará por un manejador concreto (en este caso el que nosotros vamos a implementar):

1
2
3
4
5
<bean class="org.springframework.security.cas.web.CasAuthenticationFilter" id="casFilter">
<property name="authenticationManager" ref="authenticationManager"></property>
<property name="authenticationSuccessHandler" ref="successHandler"></property>
</bean>
<bean class="es.home.example.filter.UserDataSessionFilter" id="successHandler"></bean>

Posteriomente creamos la clase que herede de 'SavedRequestAwareAuthenticationSuccessHandler'. En esta clase deberemos implementar el método 'onAuthenticationSuccess' y dentro del mismo realizaremos la lógica que deseemos. Si al terminar el método queremos que siga el flujo normal pre-establecido deberemos hacer uso de la llmada al método de la clase que extiende.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public class UserDataSessionFilter extends SavedRequestAwareAuthenticationSuccessHandler {

private PersonaDAO personaDao;

private void initializeDAOs( final ApplicationContext ctx ) {
   if( personaDao == null ) {
      personaDao = (PersonaDAO) ctx.getBean( "PersonaDAO" );
   }
}

public void onAuthenticationSuccess( final HttpServletRequest request, final HttpServletResponse response, final Authentication authentication ) throws IOException, ServletException {
   //Obtenemos la session
   HttpSession session = request.getSession();
   //Obtenemos el application context
   ApplicationContext ctx = WebApplicationContextUtils.getWebApplicationContext( session.getServletContext() );
   //inicializamos los DAOs
   initializeDAOs( ctx );
   //Realizamos la logica&nbsp;

   //Almacenamos la informacion en la session
   session.setAttribute( SESSIONS_VARS.USER_INFORMATION, userInformation );
   //Seguimos con la logica pre establecida
   super.onAuthenticationSuccess( request, response, authentication );
}
}