问题: 我们有一个基于Spring mvc的RESTful API,其中包含敏感信息。API应该是安全的,但是在每个请求时发送用户的凭证(user/pass组合)是不可取的。根据REST准则(和内部业务需求),服务器必须保持无状态。该API将由另一个服务器以混搭方式使用。

要求:

Client makes a request to .../authenticate (unprotected URL) with credentials; server returns a secure token which contains enough information for the server to validate future requests and remain stateless. This would likely consist of the same information as Spring Security's Remember-Me Token. Client makes subsequent requests to various (protected) URLs, appending the previously obtained token as a query parameter (or, less desirably, an HTTP request header). Client cannot be expected to store cookies. Since we use Spring already, the solution should make use of Spring Security.

我们一直在碰壁,试图让这个工作,所以希望有人已经解决了这个问题。

对于上面的场景,您如何解决这个特殊的需求呢?


当前回答

我们设法让它完全按照OP中描述的那样工作,希望其他人可以使用这个解决方案。以下是我们所做的:

像这样设置安全上下文:

<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>

正如您所看到的,我们已经创建了一个自定义的AuthenticationEntryPoint,如果请求没有通过AuthenticationTokenProcessingFilter在过滤器链中进行身份验证,它基本上只返回401 Unauthorized。

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);
    }
}

显然,TokenUtils包含一些私密的(非常特定于特定情况的)代码,不能轻易共享。这是它的界面:

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

这应该会让你有一个良好的开端。

其他回答

对于承载信息的令牌,JSON Web tokens (http://jwt.io)是一项出色的技术。主要概念是将信息元素(声明)嵌入到令牌中,然后对整个令牌进行签名,以便验证端可以验证声明确实是值得信任的。

我使用这个Java实现:https://bitbucket.org/b_c/jose4j/wiki/Home

还有一个Spring模块(Spring -security-jwt),但我还没有研究它支持什么。

为什么你不开始使用OAuth JSON WebTokens

http://projects.spring.io/spring-security-oauth/

OAuth2是一个标准化的授权协议/框架。根据官方OAuth2规范:

你可以在这里找到更多信息

我们设法让它完全按照OP中描述的那样工作,希望其他人可以使用这个解决方案。以下是我们所做的:

像这样设置安全上下文:

<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>

正如您所看到的,我们已经创建了一个自定义的AuthenticationEntryPoint,如果请求没有通过AuthenticationTokenProcessingFilter在过滤器链中进行身份验证,它基本上只返回401 Unauthorized。

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);
    }
}

显然,TokenUtils包含一些私密的(非常特定于特定情况的)代码,不能轻易共享。这是它的界面:

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

这应该会让你有一个良好的开端。

您可以考虑摘要访问身份验证。基本上协议如下:

客户端发出请求 服务器响应一个唯一的nonce字符串 客户端提供了一个用户名和密码(和一些其他值)md5哈希与nonce;这个哈希值称为HA1 然后,服务器能够验证客户机的身份并提供所请求的材料 与nonce的通信可以继续,直到服务器提供一个新的nonce(计数器用于消除重放攻击)

所有这些通信都是通过头进行的,正如jmort253指出的那样,这通常比在url参数中通信敏感内容更安全。

Spring Security支持摘要访问身份验证。请注意,尽管文档说您必须访问客户机的明文密码,但如果您拥有客户机的HA1散列,您就可以成功地进行身份验证。