基于Spring的RESTful认证

272

问题:
我们有一个基于Spring MVC的RESTful API,其中包含敏感信息。API应该是安全的,但每次请求时发送用户的凭据(用户名/密码组合)并不可取。根据REST指南(以及内部业务要求),服务器必须保持无状态。该API将以混合风格的方式被另一个服务器消耗。

需求:

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

  • 客户端对各种(受保护的)URL发出后续请求,将之前获取的令牌作为查询参数附加(或者,不太理想的是,作为HTTP请求头附加)。

  • 客户端不能指望存储cookies。

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

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

在上述情况下,您将如何解决这个特定的需求?


50
嗨,Chris,我不确定在查询参数中传递该令牌是否是最好的想法。无论是HTTPS还是HTTP,它都会出现在日志中。请求头可能更安全。只是提供信息。不过问题很好,给你点赞。+1 - jamesmortensen
1
你对无状态的理解是什么?你对Token的要求似乎与我对无状态的理解相冲突。在我看来,HTTP身份验证是唯一的无状态实现。 - Markus Malkusch
9
@MarkusMalkusch中的“stateless”是指服务器对与给定客户端之前的通信所了解的信息。HTTP根据定义是无状态的,而会话cookie使其具有状态。令牌的生命周期(以及来源)并不重要;服务器只关心它是否有效,并且可以与用户(而不是会话)相关联。因此,传递一个标识令牌不会影响状态性。 - Chris Cashwell
1
@ChrisCashwell 你如何确保令牌不被客户端欺骗或生成?你是否在服务器端使用私钥加密令牌,将其提供给客户端,然后在未来的请求中使用相同的密钥进行解密?显然,Base64或其他混淆技术是不够的。你能详细说明验证这些令牌的技术吗? - Craig Otis
6
虽然这篇文章的时间比较久远,而且我已经两年没有接触或更新代码了,但我创建了一个代码片段来进一步扩展这些概念。链接为https://gist.github.com/ccashwell/dfc05dd8bd1a75d189d1。 - Chris Cashwell
显示剩余3条评论
4个回答

195
我们成功地按照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未经授权

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。 - Chris Cashwell
1
如果客户端是浏览器,那么令牌该如何存储?还是每个请求都需要重新进行身份验证? - beginner_
你可以帮忙看一下我的问题吗?它与你的解决方案比较相似:http://stackoverflow.com/questions/18507816/spring-secutity-and-multipart - Sergey
2
很好的提示。@ChrisCashwell - 我找不到的部分是在哪里验证用户凭据并发送令牌?我猜它应该在 /authenticate 端点的实现中的某个地方。我对吗?如果不是,那么 /authenticate 的目标是什么? - Yonatan Maman
3
认证管理器(AuthenticationManager)内部有什么? - MoienGK
显示剩余27条评论

28
您可以考虑使用摘要访问认证。该协议的基本流程如下:
  1. 客户端发出请求
  2. 服务器响应一个唯一的nonce字符串
  3. 客户端提供用户名和密码(以及其他值),使用nonce进行md5哈希;此哈希称为HA1
  4. 然后服务器就能够验证客户端的身份并提供所请求的材料
  5. 与nonce的通信可以继续,直到服务器提供新的nonce(使用计数器消除重放攻击)
所有这些通信都是通过标头进行的,正如jmort253指出的那样,这通常比在URL参数中传递敏感材料更安全。

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


1
虽然这是一种可能的方法,但为了检索令牌而必须进行多次往返使其有些不太理想。 - Chris Cashwell
如果您的客户遵循HTTP身份验证规范,这些往返只会在第一次调用和发生5.时发生。 - Markus Malkusch

7
关于携带信息的令牌,JSON Web Tokens(http://jwt.io)是一项出色的技术。其主要概念是将信息元素(声明)嵌入到令牌中,然后签署整个令牌,以便验证端可以验证这些声明确实值得信任。
我使用的是这个Java实现:https://bitbucket.org/b_c/jose4j/wiki/Home 还有一个Spring模块(spring-security-jwt),但我没有了解它支持什么。

3

2
Spring Security OAuth项目已被弃用。最新的OAuth 2.0支持由Spring Security提供。有关详细信息,请参阅OAuth 2.0迁移指南。 - ACV

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接