通过 Spring 进行 RESTful 身份验证

Posted

技术标签:

【中文标题】通过 Spring 进行 RESTful 身份验证【英文标题】:RESTful Authentication via Spring 【发布时间】:2012-06-05 06:41:42 【问题描述】:

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

要求:

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

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

不能期望客户端存储 cookie。

既然我们已经使用了 Spring,那么解决方案应该使用 Spring Security。

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

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

【问题讨论】:

嗨,克里斯,我不确定在查询参数中传递该标记是否是最好的主意。这将显示在日志中,无论是 HTTPS 还是 HTTP。标头可能更安全。仅供参考。很好的问题。 +1 你对无状态的理解是什么?您的令牌要求与我对无状态的理解相冲突。 Http 身份验证答案在我看来是唯一的无状态实现。 @MarkusMalkusch stateless 是指服务器了解与给定客户端的先前通信。 HTTP 根据定义是无状态的,会话 cookie 使其有状态。令牌的生命周期(和来源,就此而言)是无关紧要的;服务器只关心它是否有效并且可以绑定回用户(不是会话)。因此,传递识别令牌不会干扰状态性。 @ChrisCashwell 你如何确保令牌没有被客户端欺骗/生成?您是否在服务器端使用私钥加密令牌,将其提供给客户端,然后在以后的请求中使用相同的密钥对其进行解密?显然 Base64 或其他一些混淆是不够的。您能否详细说明这些令牌的“验证”技术? 虽然这已经过时了,而且我已经超过 2 年没有接触或更新代码,但我已经创建了一个 Gist 来进一步扩展这些概念。 gist.github.com/ccashwell/dfc05dd8bd1a75d189d1 【参考方案1】:

我们设法使这项工作完全按照 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);

这应该会让你有个好的开始。

【讨论】:

令牌随请求一起发送时是否需要对令牌进行身份验证。直接获取用户名信息并在当前上下文/请求中设置如何? @Spring 我不会将它们存储在任何地方......令牌的整个想法是它需要与每个请求一起传递,并且可以(部分)解构以确定其有效性(因此validate(...) 方法)。这很重要,因为我希望服务器保持无状态。我想你可以使用这种方法而不需要使用 Spring。 如果客户端是浏览器,token怎么存储?还是您必须为每个请求重做身份验证? 很棒的提示。 @ChrisCashwell - 我找不到的部分是您在哪里验证用户凭据并发回令牌?我猜它应该在 /authenticate 端点的 impl 中的某个地方。我对吗 ?如果不是 /authenticate 的目标是什么? AuthenticationManager 里面有什么?【参考方案2】:

为什么不开始使用带有 JSON WebTokens 的 OAuth

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

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

您可以找到更多信息here

【讨论】:

Spring Security OAuth 项目已弃用。 Spring Security 提供了最新的 OAuth 2.0 支持。有关详细信息,请参阅 OAuth 2.0 迁移指南。【参考方案3】:

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

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

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

【讨论】:

【参考方案4】:

您可以考虑Digest Access Authentication。基本上协议如下:

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

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

Spring Security 支持摘要式访问身份验证。请注意,尽管文档说您必须有权访问客户的纯文本密码,但您可以为您的客户successfully authenticate if you have the HA1 hash。

【讨论】:

虽然这是一种可能的方法,但为了检索令牌必须进行多次往返,这使得它有点不可取。 如果您的客户端遵循 HTTP 身份验证规范,则这些往返仅在第一次调用和 5. 发生时发生。

以上是关于通过 Spring 进行 RESTful 身份验证的主要内容,如果未能解决你的问题,请参考以下文章

Spring RESTful Web 服务身份验证

使用 Spring Security 和 Redis 对带有 Java 配置的 RESTFul api 进行基于 Cookie 的身份验证

RESTful Web 服务中的身份验证

通过 restful api 进行 Azure AD 身份验证

如何在 Spring Boot 中使用 RESTful 和基本身份验证

如何为 Spring Boot RESTful Web 服务配置多级身份验证?