Autenticación en aplicaciones web y móviles con Spring Security

Autenticación en aplicaciones web y móviles con Spring Security

Ya hemos tratado anteriormente Spring Security como framework de autenticación en aplicaciones web en Java. En esta ocasión, queremos mostrar un ejemplo de configuración del sistema de autenticación para una aplicación con dos puntos de entrada para cada uno de sus sistemas cliente:

  • web, cliente que accede al sistema a través de la web
  • móvil, cliente que accede al sistema a través del API proporcionada

Además, y para ejemplificar un sistema real, accederemos a la aplicación con distintos roles:

  • Usuario cliente normal
  • Administrador, usuario encargado de gestionar diferentes aspectos del sistema

Autenticación API

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<security:http pattern="/api/**" realm="Protected API" use-expressions="true" create-session="stateless" entry-point-ref="unauthorizedEntryPoint" authentication-manager-ref="authenticationManager">
	<security:custom-filter ref="restAuthenticationFilter" position="FORM_LOGIN_FILTER" />
	<security:intercept-url pattern="/api/*/password" access="permitAll" />
	<security:intercept-url pattern="/api/*/*" access="hasRole('ADMIN')" />
</security:http>
<bean id="unauthorizedEntryPoint" class="com.cleventy.myapp.commons.controller.security.UnauthorizedEntryPoint" />
<bean id="userDetailService" class="com.cleventy.myapp.commons.controller.security.MyAppUserDetailsService" />
<bean id="tokenManager" class="com.cleventy.myapp.commons.controller.security.TokenManagerImpl">
	<property name="userDetailsService" ref="myappUserDetailsService" />
</bean>
<bean id="tokenAuthenticationService" class="com.cleventy.myapp.commons.controller.security.TokenAuthenticationServiceDefault" 
	c:authenticationManager-ref="authenticationManager" c:tokenManager-ref="tokenManager" />
<bean id="restAuthenticationFilter" class="com.cleventy.myapp.commons.controller.security.TokenAuthenticationFilter"
	c:authenticationService-ref="tokenAuthenticationService" c:loginLink="/login" c:logoutLink="/logout" />

Consideramos que todas las llamadas al API irán al endpoint /api/vX/*. Es decir, las URLs a las que responde el API tendrán el formato definido por el prefijo api, seguido de la versión del API a la que se referencia y por último el nombre de la acción a ejecutar.
Además, vemos que el API tiene lo que podemos considerar tres tipos de autenticación:

  • anónima para las peticiones que no requieran autenticación. Es el caso de la petición que solicita recuperar la contraseña.
  • BasicAuth para la petición de login. BasicAuth envía el usuario y la contraseña separados por dos puntos (: ) y codificado en base64. En este caso, si el login es satisfactorio, la misma petición devuelve en su respuesta un token de autenticación para futuras peticiones
  • TokenAuth, utilizada por el resto de peticiones del API, donde se comprueba que el token enviado es válido
  • Autenticación WEB

    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
    26
    27
    28
    29
    30
    31
    32
    33
    34
    
    <bean id="myappUserDetailsService" class="com.cleventy.myapp.commons.controller.security.MyAppUserDetailsService" />
    <bean id="myappUserAuthenticationProvider" class="com.cleventy.myapp.commons.controller.security.MyAppAuthenticationProvider">
    	<property name="userDetailsService" ref="myappUserDetailsService" />
    </bean>
    <bean id="authenticationManager" class="org.springframework.security.authentication.ProviderManager">
    	<property name="providers">
    		<list>
    			<ref bean="myappUserAuthenticationProvider" />
    		</list>
    	</property>
    </bean>
    <bean id="myappAuthenticationProcessingFilter" class="com.cleventy.myapp.commons.controller.security.MyAppUsernamePasswordAuthenticationFilter">
    	<property name="authenticationManager" ref="authenticationManager" />
    	<property name="filterProcessesUrl" value="/j_spring_security_check" />
    	<property name="authenticationSuccessHandler">
    		<bean class="com.cleventy.myapp.commons.controller.security.MyAppSavedRequestAwareAuthenticationSuccessHandler">
    			<property name="defaultTargetUrl" value="/" />
    		</bean>
    	</property>
    	<property name="authenticationFailureHandler">
    		<bean class="org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler">
    			<property name="defaultFailureUrl" value="/loginerror" />
    		</bean>
    	</property>
    </bean>
    <bean id="loginUserUrlAuthenticationEntryPoint" class="org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint">
    	<property name="loginFormUrl" value="/login" />
    </bean>
    <security:http auto-config="false" use-expressions="true" authentication-manager-ref="authenticationManager" entry-point-ref="loginUserUrlAuthenticationEntryPoint">
    	<security:custom-filter ref="myappAuthenticationProcessingFilter" position="FORM_LOGIN_FILTER" />
    	<security:intercept-url pattern="/admin/**" access="hasRole('ADMIN')" />
    	<security:intercept-url pattern="/user/**" access="hasRole('USER')" />
    	<security:logout invalidate-session="true" delete-cookies="JSESSIONID" logout-success-url="/" logout-url="/j_spring_security_logout" />
    </security:http>

    Aquí vemos como los usuarios podrán acceder a las páginas bajo /user, los administradores a las páginas bajo /admin y el resto de usuarios sin autenticar únicamente a las páginas de login, recordar contraseña, y otro tipo de información anónima.

    Configuración clave

    Además de la configuración de Spring Security será necesario definir una serie de clases que asuman el comportamiento referenciado en dicha configuración. De esta manera, queremos mostrar los siguientes snippets.

    MyAppUserDetailsService

    Comprueba la existencia de un usuario, su estado y roles.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    
    public class MyAppUserDetailsService implements UserDetailsService {
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
    	User user = this.adminService.findUserByLogin(username);
    	if ((user != null) && (user.getState().equals(User.REGISTERED_USER_STATE_ACTIVE))) {
    		return new MyAppUserDetails(user.getId(), user.getLogin(), user.getPassword(), user.getLanguage()==null?null:user.getLanguage().getCode(), true, true, true, true, getUserAuthorities(user.getRol()));
    	}
    	throw new UsernameNotFoundException("User does not exist");
    }
    }

    UnauthorizedEntryPoint

    Rechazo de una petición con unas credenciales (token o usuario y contraseña) incorrectas.

    1
    2
    3
    4
    5
    6
    7
    
    public final class UnauthorizedEntryPoint implements AuthenticationEntryPoint {
    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException {
    	logger.debug(" *** UnauthorizedEntryPoint.commence: " + request.getRequestURI());
    	response.sendError(HttpServletResponse.SC_UNAUTHORIZED);
    }
    }

    MyAppAuthenticationProvider

    Proveedor de autenticación para administración.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    
    public class MyAppAuthenticationProvider implements AuthenticationProvider {
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
    	UserDetails userDetails = this.userDetailsService.loadUserByUsername(authentication.getName().toLowerCase());
    	if (userDetails != null
    			&& new BasicPasswordEncryptor().checkPassword(authentication.getCredentials().toString(), userDetails.getPassword())
    			&& userDetails.getAuthorities().contains(new SimpleGrantedAuthority(User.USER_ROL_ADMIN)) ) {
    		return new MyAppUsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
    	}
    	throw new BadCredentialsException("Bad credentials");
    }
    public boolean supports(Class<?> authentication) {
    	return UsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication);
    }
    }

    TokenAuthenticationFilter

    Filtro que comprueba la autenticación por token de las peticiones al API.

    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
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    
    public final class TokenAuthenticationFilter extends GenericFilterBean {
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
    	HttpServletRequest httpRequest = (HttpServletRequest) request;
    	HttpServletResponse httpResponse = (HttpServletResponse) response;
    	if (currentLink(httpRequest).endsWith(this.loginLink)) {
    		checkLogin(httpRequest, httpResponse);
    	}
    	else {
    		boolean authenticated = checkToken(httpRequest, httpResponse);
    		if (canRequestProcessingContinue(httpRequest)) {
    			if (authenticated) {
    				 if (currentLink(httpRequest).endsWith(this.logoutLink)) {
    						logout(httpRequest);
    				 }
    			}
    		}
    	}
    	if (canRequestProcessingContinue(httpRequest)) {
    		chain.doFilter(request, response);
    	}
    }
    private void checkLogin(HttpServletRequest httpRequest, HttpServletResponse httpResponse) throws IOException {
    	String authorization = httpRequest.getHeader("Authorization");
    	if (authorization != null) {
    		checkBasicAuthorization(authorization, httpRequest, httpResponse);
    	}
    }
    private void checkBasicAuthorization(String authorization, HttpServletRequest httpRequest, HttpServletResponse httpResponse) throws IOException {
    	StringTokenizer tokenizer = new StringTokenizer(authorization);
    	if (tokenizer.countTokens() < 2 || !tokenizer.nextToken().equalsIgnoreCase("Basic")) {
    		httpResponse.sendError(HttpServletResponse.SC_UNAUTHORIZED);
    		doNotContinueWithRequestProcessing(httpRequest);
    		return;
    	}
    	String base64 = tokenizer.nextToken();
    	String loginPassword = new String(Base64.decode(base64.getBytes()));
    	tokenizer = new StringTokenizer(loginPassword, ":");
    	if (tokenizer.countTokens()!=2) {
    		httpResponse.sendError(HttpServletResponse.SC_UNAUTHORIZED);
    		doNotContinueWithRequestProcessing(httpRequest);
    		return;
    	}
    	checkUsernameAndPassword(tokenizer.nextToken(), tokenizer.nextToken(), httpRequest, httpResponse);
    }
    private void checkUsernameAndPassword(String username, String password, HttpServletRequest httpRequest, HttpServletResponse httpResponse) throws IOException {
    	TokenInfo tokenInfo = this.authenticationService.authenticate(username, password);
    	if (tokenInfo != null) {
    		httpResponse.setHeader(HEADER_TOKEN, tokenInfo.getToken());
    	} else {
    		httpResponse.sendError(HttpServletResponse.SC_UNAUTHORIZED);
    		doNotContinueWithRequestProcessing(httpRequest);
    	}
    }
    /** Returns true, if request contains valid authentication token. */
    private boolean checkToken(HttpServletRequest httpRequest, HttpServletResponse httpResponse) throws IOException {
    	String token = httpRequest.getHeader(HEADER_TOKEN);
    	if (token == null) {
    		return false;
    	}
    	if (this.authenticationService.checkToken(token)) {
    		this.logger.debug(" *** " + HEADER_TOKEN + " valid for " + ((UsernamePasswordAuthenticationToken)SecurityContextHolder.getContext().getAuthentication()).getName() +"; token="+token);
    		return true;
    	}
    	this.logger.debug(" *** Invalid " + HEADER_TOKEN + ' ' + token);
    	httpResponse.sendError(HttpServletResponse.SC_UNAUTHORIZED);
    	doNotContinueWithRequestProcessing(httpRequest);
    	return false;
    }
    private void logout(HttpServletRequest httpRequest) {
    	String token = httpRequest.getHeader(HEADER_TOKEN);
    	// we go here only authenticated, token must not be null
    	this.authenticationService.logout(token);
    	doNotContinueWithRequestProcessing(httpRequest);
    }
    private static String currentLink(HttpServletRequest httpRequest) {
    	if (httpRequest.getPathInfo() == null) {
    		return httpRequest.getServletPath();
    	}
    	return httpRequest.getServletPath() + httpRequest.getPathInfo();
    }
    /**
     * This is set in cases when we don't want to continue down the filter chain. This occurs
     * for any {@link HttpServletResponse#SC_UNAUTHORIZED} and also for login or logout.
     */
    private static void doNotContinueWithRequestProcessing(HttpServletRequest httpRequest) {
    	httpRequest.setAttribute(REQUEST_ATTR_DO_NOT_CONTINUE, "");
    }
    private static boolean canRequestProcessingContinue(HttpServletRequest httpRequest) {
    	return httpRequest.getAttribute(REQUEST_ATTR_DO_NOT_CONTINUE) == null;
    }
    }

    TokenManager

    Proporciona una interfaz para la creación de tokens a partir de los detalles de un usuario y viceversa.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    
    public interface TokenManager {
    /**
     * Creates a new token for the user and returns its {@link TokenInfo}.
     * It may add it to the token list or replace the previous one for the user. Never returns {@code null}.
     */
    TokenInfo createNewToken(UserDetails userDetails) throws GenericErrorException;
    /** Returns user details for a token. */
    UserDetails getUserDetails(String token);
    }

    TokenAuthenticationServiceDefault

    Gestiona la autenticación del API.

    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
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    
    public class TokenAuthenticationServiceDefault implements TokenAuthenticationService {
    @Override
    public TokenInfo authenticate(String login, String password) {
    	logger.debug(" *** AuthenticationServiceImpl.authenticate");
    	// Here principal=username, credentials=password
    	Authentication authentication = new UsernamePasswordAuthenticationToken(login, password);
    	try {
    		authentication = authenticationManager.authenticate(authentication);
    		// Here principal=UserDetails (UserContext in our case), credentials=null (security reasons)
    		SecurityContextHolder.getContext().setAuthentication(authentication);
     
    		if (authentication.getPrincipal() != null) {
    			UserDetails userContext = (UserDetails) authentication.getPrincipal();
    			TokenInfo newToken = null;
    			try {
    				newToken = tokenManager.createNewToken(userContext);
    			} catch (GenericErrorException e) {
    			}
    			if (newToken == null) {
    				return null;
    			}
    			return newToken;
    		}
    	} catch (AuthenticationException e) {
    		logger.debug(" *** AuthenticationServiceImpl.authenticate - FAILED: " + e.toString());
    	}
    	return null;
    }
    @Override
    public boolean checkToken(String token) {
    	logger.debug(" *** AuthenticationServiceImpl.checkToken");
    	UserDetails userDetails = tokenManager.getUserDetails(token);
    	if (userDetails == null) {
    		return false;
    	}
    	UsernamePasswordAuthenticationToken securityToken = new UsernamePasswordAuthenticationToken(
    		userDetails, null, userDetails.getAuthorities());
    	SecurityContextHolder.getContext().setAuthentication(securityToken);
     
    	return true;
    }
     
    @Override
    public void logout(String token) {
    	SecurityContextHolder.clearContext();
    }
    }

    Para más información o detalle podéis consultarnos a nosotros o la página en la que se ha basado el tutorial.
    Fuente de la Imagen: aquí

    Publicado en marzo 2, 2016

    ,

    3 comentarios en Autenticación en aplicaciones web y móviles con Spring Security
    1. Marco Antonio Carrasco dice:

      Buen dia muy bueno el tutorail de Autenticación en aplicaciones web y móviles con Spring Security sin embargo quiseira saber si existe una manera de descargar el codigo fuente saludos

    2. dfernandez dice:

      Buen día, Marco Antonio.
      nos alegramos de que te haya resultado de utilidad.
      Lamentablemente los únicos fragmentos de código públicos en este momento son los mostrados en el propio artículo. No obstante puedes ver otros ejemplos en el enlace que se indica a pie de página.
      Un saludo.

    Deja un comentario

    Tu dirección de correo electrónico no será publicada. Los campos necesarios están marcados *

    « »