通过Spring的RESTful身份验证


262

问题:
我们有一个基于Spring MVC的RESTful API,其中包含敏感信息。API应该是安全的,但是不希望随每个请求一起发送用户凭证(用户/密码组合)。根据REST准则(和内部业务要求),服务器必须保持无状态。该API将由另一台服务器以mashup方式使用。

要求:

  • 客户端.../authenticate使用凭证向(不受保护的URL)发出请求;服务器返回一个安全令牌,该令牌包含足以使服务器验证未来请求并保持无状态的信息。这可能包含与Spring Security的Remember-Me Token相同的信息。

  • 客户端向各种(受保护的)URL发出后续请求,将先前获得的令牌附加为查询参数(或者,不太希望是HTTP请求标头)。

  • 不能期望客户端存储cookie。

  • 由于我们已经使用过Spring,因此该解决方案应该利用Spring Security。

我们一直在努力地解决这个问题,所以希望外面有人已经解决了这个问题。

在上述情况下,您将如何解决这一特殊需求?


49
克里斯,您好,我不确定在查询参数中传递令牌是否是最好的主意。无论HTTPS还是HTTP,它都会显示在日志中。标头可能更安全。仅供参考。很好的问题。+1
jmort253

1
您对无状态的理解是什么?您的令牌要求与我对无状态的理解相冲突。在我看来,Http身份验证答案是唯一的无状态实现。
Markus Malkusch 2014年

9
@MarkusMalkusch无状态是指服务器对与给定客户端的先前通信的了解。HTTP根据定义是无状态的,而会话cookie使其具有状态。令牌的生命周期(和源)无关紧要;服务器只关心它是否有效,并且可以绑定到用户(不是会话)。因此,传递识别令牌不会干扰状态。
克里斯·卡什韦尔

1
@ChrisCashwell如何确保客户端不对令牌进行欺骗/生成?您是否在服务器端使用私钥来加密令牌,将其提供给客户端,然后在以后的请求中使用相同的密钥对其进行解密?显然,Base64或其他一些混淆是不够的。您能否详细说明这些令牌的“验证”技术?
Craig Otis 2014年

6
尽管已过时,并且两年多来我没有触及或更新过代码,但我创建了Gist来进一步扩展这些概念。gist.github.com/ccashwell/dfc05dd8bd1a75d189d1
Chris Cashwell 2014年

Answers:


190

我们设法完全按照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>

如您所见,我们创建了一个自定义AuthenticationEntryPoint401 Unauthorized如果我们的请求未在过滤器链中进行身份验证,则该自定义基本上只会返回一个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);
    }
}

显然,TokenUtils其中包含一些私有(且非常与案例有关)的代码,并且不能轻易共享。这是它的界面:

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

那应该使您有个良好的开端。快乐的编码。:)


令牌与请求一起发送时是否需要对令牌进行身份验证。如何直接获取用户名信息并在当前上下文/请求中进行设置?
费舍尔

1
@Spring我不会将它们存储在任何地方...令牌的整个思想是它需要随每个请求一起传递,并且可以对其进行解构(部分地)以确定其有效性(因此称为validate(...)方法)。这很重要,因为我希望服务器保持无状态。我想您可以使用这种方法而无需使用Spring。
克里斯·卡什韦尔

1
如果客户端是浏览器,那么如何存储令牌?还是您必须为每个请求重做身份验证?
初学者

2
很棒的提示。@ChrisCashwell-我找不到的部分是您在哪里验证用户凭据并发回令牌?我想它应该在/ authenticate端点的暗示中。我对吗 ?如果不是,/ authenticate的目标是什么?
Yonatan Maman 2014年

3
AuthenticationManager里面有什么?
MoienGK 2015年

25

您可能考虑使用摘要访问身份验证。本质上,协议如下:

  1. 来自客户的请求
  2. 服务器以唯一的随机数字符串响应
  3. 客户端提供用随机数散列的用户名和密码(以及其他一些值)md5;此哈希称为HA1
  4. 然后,服务器可以验证客户的身份并提供所请求的材料
  5. 与随机数的通信可以继续,直到服务器提供新的随机数(使用计数器来消除重放攻击)为止

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

Spring Security支持摘要访问身份验证。请注意,尽管文档说您必须有权访问客户端的纯文本密码,但是如果您具有客户端的HA1哈希可以成功进行身份验证


1
尽管这是一种可能的方法,但必须执行几次往返操作才能检索令牌,这使它有些不可取。
克里斯·卡什韦尔

如果您的客户端遵循HTTP身份验证规范,则这些往返仅在首次调用和发生5.时发生。
Markus Malkusch 2014年

5

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

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

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


By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.