**Problema Tenemos una API RESTful basada en Spring MVC que contiene información sensible. La API debe ser segura, sin embargo el envío de las credenciales del usuario (combo usuario/pass) con cada solicitud no es deseable. Según las directrices REST (y los requisitos empresariales internos), el servidor debe permanecer sin estado. La API será consumida por otro servidor en un enfoque de tipo mashup.
**Requisitos
El cliente realiza una solicitud a .../authenticate
(URL desprotegida) con credenciales; el servidor devuelve un token seguro que contiene suficiente información para que el servidor valide futuras solicitudes y permanezca sin estado. Esto probablemente consistiría en la misma información que Spring Security's Remember-Me Token.
El cliente realiza peticiones posteriores a varias URLs (protegidas), añadiendo el token obtenido previamente como un parámetro de consulta (o, menos deseable, una cabecera de petición HTTP).
No se puede esperar que el cliente almacene cookies.
Dado que ya utilizamos Spring, la solución debería hacer uso de Spring Security.
Nos hemos estado dando cabezazos contra la pared tratando de hacer que esto funcione, así que esperemos que alguien por ahí ya haya resuelto este problema.
Dado el escenario anterior, ¿cómo podría resolver esta necesidad en particular?
Nos las arreglamos para conseguir que esto funcione exactamente como se describe en el OP, y espero que alguien más puede hacer uso de la solución. Esto es lo que hicimos:
Configurar el contexto de seguridad así:
<security:http realm="Protected API" use-expressions="true" auto-config="false" create-session="stateless" entry-point-ref="CustomAuthenticationEntryPoint">
<security:custom-filter ref="authenticationTokenProcessingFilter" position="FORM_LOGIN_FILTER" />
<security:intercept-url pattern="/authenticate" access="permitAll"/>
<security:intercept-url pattern="/**" access="isAuthenticated()" />
</security:http>
<bean id="CustomAuthenticationEntryPoint"
class="com.demo.api.support.spring.CustomAuthenticationEntryPoint" />
<bean id="authenticationTokenProcessingFilter"
class="com.demo.api.support.spring.AuthenticationTokenProcessingFilter" >
<constructor-arg ref="authenticationManager" />
</bean>
Como puedes ver, hemos creado un AuthenticationEntryPoint
personalizado, que básicamente devuelve un 401 Unauthorized
si la petición no ha sido autenticada en la cadena de filtrado por nuestro AuthenticationTokenProcessingFilter
.
CustomAuthenticationEntryPoint:
public class CustomAuthenticationEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response,
AuthenticationException authException) throws IOException, ServletException {
response.sendError( HttpServletResponse.SC_UNAUTHORIZED, "Unauthorized: Authentication token was either missing or invalid." );
}
}
Filtro de procesamiento de token de autenticación:
public class AuthenticationTokenProcessingFilter extends GenericFilterBean {
@Autowired UserService userService;
@Autowired TokenUtils tokenUtils;
AuthenticationManager authManager;
public AuthenticationTokenProcessingFilter(AuthenticationManager authManager) {
this.authManager = authManager;
}
@Override
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {
@SuppressWarnings("unchecked")
Map<String, String[]> parms = request.getParameterMap();
if(parms.containsKey("token")) {
String token = parms.get("token")[0]; // grab the first "token" parameter
// validate the token
if (tokenUtils.validate(token)) {
// determine the user based on the (already validated) token
UserDetails userDetails = tokenUtils.getUserFromToken(token);
// build an Authentication object with the user's info
UsernamePasswordAuthenticationToken authentication =
new UsernamePasswordAuthenticationToken(userDetails.getUsername(), userDetails.getPassword());
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails((HttpServletRequest) request));
// set the authentication into the SecurityContext
SecurityContextHolder.getContext().setAuthentication(authManager.authenticate(authentication));
}
}
// continue thru the filter chain
chain.doFilter(request, response);
}
}
Obviamente, TokenUtils
contiene código privado (y muy específico para cada caso) y no puede compartirse fácilmente. Esta es su interfaz:
public interface TokenUtils {
String getToken(UserDetails userDetails);
String getToken(UserDetails userDetails, Long expiration);
boolean validate(String token);
UserDetails getUserFromToken(String token);
}
Con esto deberías empezar con buen pie. Feliz codificación. :)
Puedes considerar Digest Access Authentication. Básicamente, el protocolo es el siguiente:
Toda esta comunicación se realiza a través de cabeceras, lo cual, como señala jmort253, es generalmente más seguro que comunicar material sensible en los parámetros de la url.
La autenticación de acceso Digest está soportada por Spring Security. Tenga en cuenta que, aunque la documentación dice que debe tener acceso a la contraseña en texto plano de su cliente, puede autenticarse con éxito si tiene el hash HA1 de su cliente.
En cuanto a los tokens que contienen información, JSON Web Tokens (http://jwt.io) es una tecnología brillante. El concepto principal consiste en incorporar elementos de información (reivindicaciones) en el token y, a continuación, firmar todo el token para que el extremo de validación pueda verificar que las reivindicaciones son realmente fiables.
Yo utilizo esta implementación Java: https://bitbucket.org/b_c/jose4j/wiki/Home
También hay un módulo de Spring (spring-security-jwt), pero no he investigado lo que soporta.