Autenticação RESTful via Spring

262

Problema:
Temos uma API RESTful baseada no Spring MVC que contém informações confidenciais. A API deve ser protegida, no entanto, o envio das credenciais do usuário (combinação usuário / senha) com cada solicitação não é desejável. De acordo com as diretrizes REST (e requisitos internos de negócios), o servidor deve permanecer sem estado. A API será consumida por outro servidor em uma abordagem no estilo de mashup.

Requisitos:

  • O cliente faz uma solicitação para .../authenticate(URL não protegido) com credenciais; O servidor retorna um token seguro que contém informações suficientes para o servidor validar solicitações futuras e permanecer sem estado. Provavelmente, isso consistiria nas mesmas informações que o Remember-Me Token da Spring Security .

  • O cliente faz solicitações subsequentes para várias URLs (protegidas), acrescentando o token obtido anteriormente como um parâmetro de consulta (ou, menos desejável, um cabeçalho de solicitação HTTP).

  • Não se espera que o cliente armazene cookies.

  • Como já usamos o Spring, a solução deve usar o Spring Security.

Temos batido nossas cabeças contra a parede tentando fazer isso funcionar, por isso espero que alguém por aí já tenha resolvido esse problema.

Dado o cenário acima, como você pode resolver essa necessidade específica?

Chris Cashwell
fonte
49
Oi Chris, não tenho certeza de que passar esse token no parâmetro de consulta é a melhor ideia. Isso aparecerá nos logs, independentemente de HTTPS ou HTTP. Os cabeçalhos são provavelmente mais seguros. Apenas para sua informação. Ótima pergunta embora. +1
jmort253
1
Qual é a sua compreensão de apátridas? Seu requisito de token colide com meu entendimento de apátrida. A resposta da autenticação HTTP parece-me a única implementação sem estado.
Markus Malkusch
9
O @MarkusMalkusch sem estado refere-se ao conhecimento do servidor sobre comunicações anteriores com um determinado cliente. O HTTP é sem estado por definição, e os cookies de sessão o tornam com estado. A vida útil (e a origem, nesse caso) do token são irrelevantes; o servidor só se preocupa com a validade e pode ser vinculado a um usuário (NÃO a uma sessão). Passar um token de identificação, portanto, não interfere com o estado.
Chris Cashwell
1
@ChrisCashwell Como você garante que o token não esteja sendo falsificado / gerado pelo cliente? Você usa uma chave privada no lado do servidor para criptografar o token, fornecê-lo ao cliente e, em seguida, usa a mesma chave para descriptografá-lo durante solicitações futuras? Obviamente, Base64 ou alguma outra ofuscação não seria suficiente. Você pode elaborar técnicas para a "validação" desses tokens?
Craig Otis
6
Embora isso seja datado e não toquei ou atualizei o código há mais de 2 anos, criei um Gist para expandir ainda mais esses conceitos. gist.github.com/ccashwell/dfc05dd8bd1a75d189d1
Chris Cashwell

Respostas:

190

Conseguimos fazer isso funcionar exatamente como descrito no OP, e esperamos que alguém possa fazer uso da solução. Aqui está o que fizemos:

Configure o contexto de segurança da seguinte maneira:

<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 você pode ver, criamos um personalizado AuthenticationEntryPoint, que basicamente apenas retorna a 401 Unauthorizedse a solicitação não foi autenticada na cadeia de filtros por nossa 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, TokenUtilscontém algum código privado (e muito específico para cada caso) e não pode ser facilmente compartilhado. Aqui está sua interface:

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

Isso deve levá-lo a um bom começo. Feliz codificação. :)

Chris Cashwell
fonte
É necessário autenticar o token quando o token estiver sendo enviado com a solicitação. Que tal obter as informações do nome de usuário diretamente e definir no contexto / solicitação atual?
Fisher
1
@ Spring Eu não os guardo em nenhum lugar ... a idéia do token é que ele precisa ser passado a cada solicitação e pode ser desconstruído (parcialmente) para determinar sua validade (daí o validate(...)método). Isso é importante porque quero que o servidor permaneça sem estado. Eu imaginaria que você poderia usar essa abordagem sem precisar usar o Spring.
Chris Cashwell
1
Se o cliente for um navegador, como o token pode ser armazenado? ou você precisa refazer a autenticação para cada solicitação?
Iniciantes_13 /
2
ótimas dicas. @ ChrisCashwell - a parte que não consigo encontrar é onde você valida as credenciais do usuário e envia de volta um token? Eu acho que deveria estar em algum lugar no impl do ponto final / authenticate. Estou certo ? Se não, qual é o objetivo / autenticar?
Yonatan Maman
3
o que há dentro do AuthenticationManager?
MoienGK 25/09
25

Você pode considerar a autenticação de acesso Digest . Essencialmente, o protocolo é o seguinte:

  1. A solicitação é feita do cliente
  2. O servidor responde com uma string nonce exclusiva
  3. O cliente fornece um nome de usuário e senha (e alguns outros valores) MD5 hash com o nonce; esse hash é conhecido como HA1
  4. O servidor pode verificar a identidade do cliente e fornecer os materiais solicitados
  5. A comunicação com o nonce pode continuar até que o servidor forneça um novo nonce (um contador é usado para eliminar ataques de reprodução)

Toda essa comunicação é feita por meio de cabeçalhos, que, como jmort253 aponta, geralmente são mais seguros do que a comunicação de material sensível nos parâmetros da URL.

A autenticação Digest Access é suportada pelo Spring Security . Observe que, embora os documentos digam que você deve ter acesso à senha de texto sem formatação do seu cliente, você poderá se autenticar com êxito se tiver o hash HA1 para o seu cliente.

Tim Pote
fonte
1
Embora essa seja uma abordagem possível, as várias viagens de ida e volta que devem ser feitas para recuperar um token o tornam um pouco indesejável.
Chris Cashwell
Se o seu cliente seguir a especificação de autenticação HTTP, essas viagens de ida e volta ocorrerão somente na primeira chamada e quando 5. acontecer.
Markus Malkusch
5

Em relação aos tokens que transportam informações, o JSON Web Tokens ( http://jwt.io ) é uma tecnologia brilhante. O conceito principal é incorporar elementos de informação (declarações) no token e, em seguida, assinar todo o token para que o fim da validação possa verificar se as declarações são realmente confiáveis.

Eu uso esta implementação Java: https://bitbucket.org/b_c/jose4j/wiki/Home

Há também um módulo Spring (spring-security-jwt), mas não examinei o que ele suporta.

Leif John
fonte