Autenticación RESTful vía Spring

262

Problema:
Tenemos una API RESTful basada en Spring MVC que contiene información confidencial. La API debe estar protegida, sin embargo, no es deseable enviar las credenciales del usuario (combinación de usuario / paso) con cada solicitud. Según las pautas REST (y los requisitos comerciales internos), el servidor debe permanecer sin estado. La API será consumida por otro servidor en un enfoque de estilo 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 el token Remember-Me de Spring Security .

  • El cliente realiza solicitudes posteriores a varias URL (protegidas), agregando el token obtenido previamente como un parámetro de consulta (o, de manera menos deseable, un encabezado de solicitud HTTP).

  • No se puede esperar que el cliente almacene cookies.

  • Como ya usamos Spring, la solución debería hacer uso de Spring Security.

Hemos estado golpeando nuestras cabezas contra la pared tratando de hacer que esto funcione, así que espero que alguien ya haya resuelto este problema.

Dado el escenario anterior, ¿cómo podría resolver esta necesidad particular?

Chris Cashwell
fuente
49
Hola Chris, no estoy seguro de que pasar ese token en el parámetro de consulta sea la mejor idea. Eso aparecerá en los registros, independientemente de HTTPS o HTTP. Los encabezados son probablemente más seguros. Solo para tu información. Gran pregunta sin embargo. +1
jmort253
1
¿Qué entiendes de apátridas? Su requisito de token choca con mi comprensión de los apátridas. La respuesta de autenticación Http me parece la única implementación sin estado.
Markus Malkusch
9
@MarkusMalkusch sin estado se refiere al conocimiento del servidor de comunicaciones previas con un cliente determinado. HTTP no tiene estado por definición, y las cookies de sesión lo hacen con estado. La vida útil (y la fuente, para el caso) del token son irrelevantes; al servidor solo le importa que sea válido y que pueda vincularse a un usuario (NO a una sesión). Pasar una ficha de identificación, por lo tanto, no interfiere con la capacidad de estado.
Chris Cashwell
1
@ChrisCashwell ¿Cómo se asegura de que el token no sea falsificado / generado por el cliente? ¿Utiliza una clave privada en el lado del servidor para cifrar el token, proporcionarlo al cliente y luego usar la misma clave para descifrarlo en futuras solicitudes? Obviamente, Base64 o alguna otra ofuscación no sería suficiente. ¿Puedes elaborar técnicas para la "validación" de estos tokens?
Craig Otis
66
Aunque esto está anticuado y no he tocado ni actualizado el código en más de 2 años, he creado un Gist para ampliar aún más estos conceptos. gist.github.com/ccashwell/dfc05dd8bd1a75d189d1
Chris Cashwell

Respuestas:

190

Logramos que esto funcione exactamente como se describe en el OP, y esperamos que alguien más pueda hacer uso de la solución. Esto es lo que hicimos:

Configure el contexto de seguridad de esta manera:

<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 puede ver, hemos creado una costumbre AuthenticationEntryPoint, que básicamente solo devuelve un 401 Unauthorizedsi nuestra solicitud no fue autenticada en la cadena de filtros 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." );
    }
}

AuthenticationTokenProcessingFilter :

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, TokenUtilscontiene un código privado (y muy específico para cada caso) y no se puede compartir fácilmente. Aquí está su interfaz:

public interface TokenUtils {
    String getToken(UserDetails userDetails);
    String getToken(UserDetails userDetails, Long expiration);
    boolean validate(String token);
    UserDetails getUserFromToken(String token);
}

Eso debería llevarte a un buen comienzo. Feliz codificación :)

Chris Cashwell
fuente
¿Es necesario autenticar el token cuando el token se envía con la solicitud? ¿Qué tal obtener la información del nombre de usuario directamente y establecerla en el contexto / solicitud actual?
Fisher
1
@Spring No los guardo en ningún lado ... la idea general del token es que debe pasarse con cada solicitud, y se puede deconstruir (parcialmente) para determinar su validez (de ahí el validate(...)método). Esto es importante porque quiero que el servidor permanezca sin estado. Me imagino que podría usar este enfoque sin necesidad de usar Spring.
Chris Cashwell
1
Si el cliente es un navegador, ¿cómo se puede almacenar el token? ¿o tiene que rehacer la autenticación para cada solicitud?
principiante_
2
Grandes consejos. @ChrisCashwell: la parte que no puedo encontrar es ¿dónde validan las credenciales de usuario y envían un token? Supongo que debería estar en algún lugar en el impl del punto final / autenticar estoy en lo cierto? Si no, ¿cuál es el objetivo de / autenticar?
Yonatan Maman
3
¿Qué hay dentro del AuthenticationManager?
MoienGK
25

Puede considerar la autenticación de acceso implícito . Esencialmente, el protocolo es el siguiente:

  1. La solicitud se realiza desde el cliente.
  2. El servidor responde con una cadena única nonce
  3. El cliente proporciona un nombre de usuario y contraseña (y algunos otros valores) md5 hash con el nonce; este hash se conoce como HA1
  4. El servidor puede verificar la identidad del cliente y entregar los materiales solicitados.
  5. La comunicación con el nonce puede continuar hasta que el servidor suministre un nuevo nonce (se utiliza un contador para eliminar los ataques de repetición)

Toda esta comunicación se realiza a través de encabezados, que, como señala jmort253, generalmente es más seguro que comunicar material sensible en los parámetros de la url.

La autenticación de acceso implícito es compatible con Spring Security . Tenga en cuenta que, aunque los documentos dicen que debe tener acceso a la contraseña de texto sin formato de su cliente, puede autenticarse correctamente si tiene el hash HA1 para su cliente.

Tim Pote
fuente
1
Si bien este es un enfoque posible, los varios viajes de ida y vuelta que se deben realizar para recuperar un token lo hacen un poco indeseable.
Chris Cashwell
Si su cliente sigue la especificación de autenticación HTTP, esos viajes de ida y vuelta ocurren solo en la primera llamada y cuando sucede 5..
Markus Malkusch
5

Con respecto a los tokens que transportan información, JSON Web Tokens ( http://jwt.io ) es una tecnología brillante. El concepto principal es incorporar elementos de información (reclamos) en el token y luego firmar todo el token para que el extremo de validación pueda verificar que los reclamos son realmente confiables.

Uso esta implementación de Java: https://bitbucket.org/b_c/jose4j/wiki/Home

También hay un módulo Spring (spring-security-jwt), pero no he investigado qué admite.

Leif John
fuente