1. 概述

本文将讲解如何在同一个 REST API 的 URI 结构下,同时支持 Basic 认证Digest 认证。在之前的文章中,我们讨论过基于表单的认证方式,但这种方式并不适合 RESTful 风格的服务,因为其依赖 Session,违反了 REST 的无状态约束。

而 Basic 和 Digest 认证更符合 RESTful 的设计原则,是更适合 REST API 的认证方式。


2. Basic 认证配置

表单认证在 REST 服务中不理想的原因在于 Spring Security 会使用 Session。这显然违背了 REST 架构中的无状态要求。

我们从 Basic 认证的配置开始,先移除主 <http> 安全元素中旧的自定义入口点和过滤器:

<http create-session="stateless">
   <intercept-url pattern="/api/admin/**" access="ROLE_ADMIN" />

   <http-basic />
</http>

注意,我们仅通过 <http-basic /> 这一行就完成了 Basic 认证的支持。它自动创建并配置了 BasicAuthenticationFilterBasicAuthenticationEntryPoint

2.1. 满足无状态要求 —— 去除 Session

REST 架构的一个核心约束是客户端与服务器之间的通信必须是 无状态的。这意味着每次请求都必须携带所有必要的信息,服务器不能保存任何会话状态。

Spring Security 3.2 引入了 create-session="stateless" 这个配置选项,确保 Spring Security 不会创建或使用 Session。它会从安全过滤器链中移除所有与 Session 相关的组件,从而确保每次请求都独立进行认证。


3. Digest 认证配置

接下来,我们基于上面的配置,手动添加 Digest 认证所需的组件:入口点(Entry Point)和过滤器(Filter)

我们需要将 Digest 认证的入口点设为主入口点,并将 Digest 过滤器插入到安全过滤器链中 Basic 认证之后。

<http create-session="stateless" entry-point-ref="digestEntryPoint">
   <intercept-url pattern="/api/admin/**" access="ROLE_ADMIN" />

   <http-basic />
   <custom-filter ref="digestFilter" after="BASIC_AUTH_FILTER" />
</http>

<beans:bean id="digestFilter" class=
 "org.springframework.security.web.authentication.www.DigestAuthenticationFilter">
   <beans:property name="userDetailsService" ref="userService" />
   <beans:property name="authenticationEntryPoint" ref="digestEntryPoint" />
</beans:bean>

<beans:bean id="digestEntryPoint" class=
 "org.springframework.security.web.authentication.www.DigestAuthenticationEntryPoint">
   <beans:property name="realmName" value="Contacts Realm via Digest Authentication"/>
   <beans:property name="key" value="acegi" />
</beans:bean>

<authentication-manager>
   <authentication-provider>
      <user-service id="userService">
         <user name="eparaschiv" password="eparaschiv" authorities="ROLE_ADMIN" />
         <user name="user" password="user" authorities="ROLE_USER" />
      </user-service>
   </authentication-provider>
</authentication-manager>

⚠️ 注意:Spring Security 的命名空间目前不支持像 <http-basic> 那样自动配置 Digest 认证。因此,我们需要手动定义并配置相关 Bean。


4. 在同一个 RESTful 服务中支持两种认证方式

单独使用 Basic 或 Digest 认证在 Spring Security 中都比较简单,但要让它们 同时作用于同一个 URI 映射路径,配置和测试就变得复杂一些。

4.1. 匿名请求

当一个请求没有携带任何认证信息(即没有 Authorization 请求头)时,Spring Security 会如何处理?

  • Basic 和 Digest 认证过滤器都会检测不到认证信息,继续执行安全过滤器链。
  • 因为请求未认证,Spring Security 抛出 AccessDeniedException,并被 ExceptionTranslationFilter 捕获。
  • 然后启动入口点(entry point),返回认证挑战。

由于我们配置了 Digest 作为主入口点,因此匿名请求会触发 Digest 认证挑战。也就是说,Digest 是默认的认证方式

4.2. 带有认证信息的请求

  • Basic 认证:请求头中包含 Authorization: Basic [base64]
  • Digest 认证:请求头中包含 Authorization: Digest [digest params]

Spring Security 的认证过滤器会根据请求头中的前缀识别认证类型,并分别处理。


5. 测试两种认证方式

我们通过测试用例验证服务是否支持 Basic 和 Digest 两种认证方式。

@Test
public void givenAuthenticatedByBasicAuth_whenAResourceIsCreated_then201IsReceived(){
   // Given
   // When
   Response response = given()
    .auth().preemptive().basic( "admin", "password" )
    .contentType( "application/json" ).body( new Foo( "test" ) )
    .post( "/api/admin/foos" );

   // Then
   assertThat( response.getStatusCode(), is( 201 ) );
}

@Test
public void givenAuthenticatedByDigestAuth_whenAResourceIsCreated_then201IsReceived(){
   // Given
   // When
   Response response = given()
    .auth().digest( "admin", "password" )
    .contentType( "application/json" ).body( new Foo( "test" ) )
    .post( "/api/admin/foos" );

   // Then
   assertThat( response.getStatusCode(), is( 201 ) );
}

✅ 测试中使用了 REST Assured 进行 HTTP 请求测试。

⚠️ 注意:Basic 认证测试中使用了 .preemptive().basic(...),这是为了在服务器未挑战之前主动发送认证信息。因为默认入口点是 Digest,如果服务器挑战,会要求 Digest 认证,Basic 可能无法生效。


6. 小结

本文介绍了如何在 Spring Security 中为 RESTful 服务配置 Basic 和 Digest 认证,并通过手动配置实现了两者的共存。主要要点包括:

  • ✅ 使用 create-session="stateless" 确保服务无状态
  • ✅ Basic 认证可通过 <http-basic /> 快速启用
  • ✅ Digest 认证需手动配置 DigestAuthenticationFilterDigestAuthenticationEntryPoint
  • ✅ 同时支持两种认证方式时,Digest 是默认挑战方式
  • ✅ 测试时需注意 Basic 认证的 preemptive 使用方式

这两种认证方式虽然简单,但在某些场景下仍具备实用价值,尤其是在需要轻量级认证机制的 REST API 中。


原始标题:Basic and Digest Auth for a REST API with Spring Security