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 认证的支持。它自动创建并配置了 BasicAuthenticationFilter
和 BasicAuthenticationEntryPoint
。
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 认证需手动配置
DigestAuthenticationFilter
和DigestAuthenticationEntryPoint
- ✅ 同时支持两种认证方式时,Digest 是默认挑战方式
- ✅ 测试时需注意 Basic 认证的 preemptive 使用方式
这两种认证方式虽然简单,但在某些场景下仍具备实用价值,尤其是在需要轻量级认证机制的 REST API 中。