1. 概述
本文将深入探讨如何在 Spring Security 应用中配置多个入口点。核心在于通过 XML 配置文件定义多个 http 块,或在 Java 配置中多次创建 SecurityFilterChain Bean 来实现多个 HttpSecurity 实例。
2. Maven 依赖
开发环境需要以下依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<version>5.4.0</version>
</dependency>
最新版本可在 Maven Central 获取:
- spring-boot-starter-security
- spring-boot-starter-web
- spring-boot-starter-thymeleaf
- spring-boot-starter-test
- spring-security-test
3. 多入口点配置
3.1. 多 HTTP 元素实现多入口点
首先定义主配置类,包含用户数据源:
@Configuration
@EnableWebSecurity
public class MultipleEntryPointsSecurityConfig {
@Bean
public UserDetailsService userDetailsService() throws Exception {
InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
manager.createUser(User
.withUsername("user")
.password(encoder().encode("userPass"))
.roles("USER").build());
manager.createUser(User
.withUsername("admin")
.password(encoder().encode("adminPass"))
.roles("ADMIN").build());
return manager;
}
@Bean
public PasswordEncoder encoder() {
return new BCryptPasswordEncoder();
}
}
现在看如何定义多个入口点。这里以 HTTP Basic 认证为例,利用 Spring Security 支持配置中定义多个 HTTP 元素的特性。
使用 Java 配置时,定义多个安全域的方式是创建多个 @Configuration 类——每个类包含独立的安全配置。这些类可以是静态内部类,放在主配置类中。
多入口点的主要应用场景:当应用需要为不同类型用户开放不同访问区域时。
我们配置三个入口点,分别对应不同权限和认证模式:
- 管理员用户:HTTP Basic 认证
- 普通用户:表单认证
- 访客用户:无需认证
管理员入口点保护 /admin/* 路径,仅允许 ADMIN 角色,使用 BasicAuthenticationEntryPoint 作为入口点:
@Configuration
@Order(1)
public static class App1ConfigurationAdapter {
@Bean
public SecurityFilterChain filterChainApp1(HttpSecurity http) throws Exception {
http.antMatcher("/admin/**")
.authorizeRequests().anyRequest().hasRole("ADMIN")
.and().httpBasic().authenticationEntryPoint(authenticationEntryPoint())
.and().exceptionHandling().accessDeniedPage("/403");
return http.build();
}
@Bean
public AuthenticationEntryPoint authenticationEntryPoint(){
BasicAuthenticationEntryPoint entryPoint = new BasicAuthenticationEntryPoint();
entryPoint.setRealmName("admin realm");
return entryPoint;
}
}
⚠️ 每个静态类的 @Order 注解表示配置匹配请求 URL 的优先级,每个类的 order 值必须唯一。
BasicAuthenticationEntryPoint Bean 必须设置 realmName 属性。
3.2. 同一 HTTP 元素下的多入口点
接下来配置 /user/* 路径,允许 USER 角色通过表单认证访问:
@Configuration
@Order(2)
public static class App2ConfigurationAdapter {
@Bean
public SecurityFilterChain filterChainApp2(HttpSecurity http) throws Exception {
http.antMatcher("/user/**")
.authorizeRequests().anyRequest().hasRole("USER")
.and().formLogin().loginProcessingUrl("/user/login")
.failureUrl("/userLogin?error=loginError").defaultSuccessUrl("/user/myUserPage")
.and().logout().logoutUrl("/user/logout").logoutSuccessUrl("/multipleHttpLinks")
.deleteCookies("JSESSIONID")
.and().exceptionHandling()
.defaultAuthenticationEntryPointFor(loginUrlauthenticationEntryPointWithWarning(), new AntPathRequestMatcher("/user/private/**"))
.defaultAuthenticationEntryPointFor(loginUrlauthenticationEntryPoint(), new AntPathRequestMatcher("/user/general/**"))
.accessDeniedPage("/403")
.and().csrf().disable();
return http.build();
}
}
✅ 除了 authenticationEntryPoint() 方法,还可使用 defaultAuthenticationEntryPointFor() 定义多个入口点,通过 RequestMatcher 匹配不同条件。
RequestMatcher 接口提供多种实现(路径匹配、媒体类型、正则等)。本例使用 AntPathRequestMatch 为 /user/private/* 和 /user/general/* 设置不同入口点。
在相同静态配置类中定义入口点 Bean:
@Bean
public AuthenticationEntryPoint loginUrlauthenticationEntryPoint(){
return new LoginUrlAuthenticationEntryPoint("/userLogin");
}
@Bean
public AuthenticationEntryPoint loginUrlauthenticationEntryPointWithWarning(){
return new LoginUrlAuthenticationEntryPoint("/userLoginWithWarning");
}
核心在于如何设置多个入口点,而非具体实现细节。这里两个入口点都是 LoginUrlAuthenticationEntryPoint 类型,分别指向不同登录页:
- /userLogin:普通登录页
- /userLoginWithWarning:访问 /user/ 私有路径时显示警告的登录页
此配置还需定义 /userLogin 和 /userLoginWithWarning 的 MVC 映射及对应登录表单页面。
⚠️ 表单认证中,配置所需的 URL(如登录处理 URL)必须遵循 /user/* 格式,或单独配置为可访问。
以上配置在无权限用户访问受保护 URL 时,都会重定向到 /403。
❌ 即使在不同静态类中,Bean 名称也必须唯一,否则会相互覆盖。
3.3. 无入口点的新 HTTP 元素
最后配置 /guest/* 路径,允许所有用户(包括未认证)访问:
@Configuration
@Order(3)
public static class App3ConfigurationAdapter {
@Bean
public SecurityFilterChain filterChainApp3(HttpSecurity http) throws Exception {
http.antMatcher("/guest/**")
.authorizeRequests()
.anyRequest()
.permitAll();
return http.build();
}
}
3.4. XML 配置方式
看下前述三个 HttpSecurity 实例的等效 XML 配置。需要三个独立的
对于 /admin/* 路径,XML 使用 http-basic 元素的 entry-point-ref 属性:
<security:http pattern="/admin/**" use-expressions="true" auto-config="true">
<security:intercept-url pattern="/**" access="hasRole('ROLE_ADMIN')"/>
<security:http-basic entry-point-ref="authenticationEntryPoint" />
</security:http>
<bean id="authenticationEntryPoint"
class="org.springframework.security.web.authentication.www.BasicAuthenticationEntryPoint">
<property name="realmName" value="admin realm" />
</bean>
⚠️ XML 配置中角色必须为 ROLE_
/user/* 路径的配置需拆分为两个 http 块,因 XML 无 defaultAuthenticationEntryPointFor() 的直接等效方法。
/user/general/* 配置:
<security:http pattern="/user/general/**" use-expressions="true" auto-config="true"
entry-point-ref="loginUrlAuthenticationEntryPoint">
<security:intercept-url pattern="/**" access="hasRole('ROLE_USER')" />
<!-- form-login 配置 -->
</security:http>
<bean id="loginUrlAuthenticationEntryPoint"
class="org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint">
<constructor-arg name="loginFormUrl" value="/userLogin" />
</bean>
/user/private/* 配置:
<security:http pattern="/user/private/**" use-expressions="true" auto-config="true"
entry-point-ref="loginUrlAuthenticationEntryPointWithWarning">
<security:intercept-url pattern="/**" access="hasRole('ROLE_USER')"/>
<!-- form-login 配置 -->
</security:http>
<bean id="loginUrlAuthenticationEntryPointWithWarning"
class="org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint">
<constructor-arg name="loginFormUrl" value="/userLoginWithWarning" />
</bean>
/guest/* 路径配置:
<security:http pattern="/**" use-expressions="true" auto-config="true">
<security:intercept-url pattern="/guest/**" access="permitAll()"/>
</security:http>
⚠️ 至少有一个 XML
4. 访问受保护 URL
4.1. MVC 配置
创建与安全路径匹配的请求映射:
@Controller
public class PagesController {
@GetMapping("/admin/myAdminPage")
public String getAdminPage() {
return "multipleHttpElems/myAdminPage";
}
@GetMapping("/user/general/myUserPage")
public String getUserPage() {
return "multipleHttpElems/myUserPage";
}
@GetMapping("/user/private/myPrivateUserPage")
public String getPrivateUserPage() {
return "multipleHttpElems/myPrivateUserPage";
}
@GetMapping("/guest/myGuestPage")
public String getGuestPage() {
return "multipleHttpElems/myGuestPage";
}
@GetMapping("/multipleHttpLinks")
public String getMultipleHttpLinksPage() {
return "multipleHttpElems/multipleHttpLinks";
}
}
/multipleHttpLinks 返回包含受保护 URL 链接的简单 HTML 页面:
<a th:href="@{/admin/myAdminPage}">Admin page</a>
<a th:href="@{/user/general/myUserPage}">User page</a>
<a th:href="@{/user/private/myPrivateUserPage}">Private user page</a>
<a th:href="@{/guest/myGuestPage}">Guest page</a>
每个受保护 URL 对应的 HTML 页面包含简单文本和返回链接:
Welcome admin!
<a th:href="@{/multipleHttpLinks}" >Back to links</a>
4.2. 应用初始化
以 Spring Boot 应用运行,定义主类:
@SpringBootApplication
public class MultipleEntryPointsApplication {
public static void main(String[] args) {
SpringApplication.run(MultipleEntryPointsApplication.class, args);
}
}
若使用 XML 配置,需在主类添加 @ImportResource({"classpath:spring-security-multiple-entry.xml"})* 注解。
4.3. 安全配置测试
设置 JUnit 测试类验证受保护 URL:
@RunWith(SpringRunner.class)
@WebAppConfiguration
@SpringBootTest(classes = MultipleEntryPointsApplication.class)
public class MultipleEntryPointsTest {
@Autowired
private WebApplicationContext wac;
@Autowired
private FilterChainProxy springSecurityFilterChain;
private MockMvc mockMvc;
@Before
public void setup() {
this.mockMvc = MockMvcBuilders.webAppContextSetup(this.wac)
.addFilter(springSecurityFilterChain).build();
}
}
用 admin 用户测试 URL:
- 无 HTTP Basic 认证访问 /admin/adminPage → 401 Unauthorized
- 添加认证后 → 200 OK
- 用 admin 用户访问 /user/userPage → 403 Forbidden
@Test
public void whenTestAdminCredentials_thenOk() throws Exception {
mockMvc.perform(get("/admin/myAdminPage")).andExpect(status().isUnauthorized());
mockMvc.perform(get("/admin/myAdminPage")
.with(httpBasic("admin", "adminPass"))).andExpect(status().isOk());
mockMvc.perform(get("/user/myUserPage")
.with(user("admin").password("adminPass").roles("ADMIN")))
.andExpect(status().isForbidden());
}
用普通用户凭证测试:
- 无表单认证访问 /user/general/myUserPage → 302 Found(重定向到登录页)
- 认证后 → 200 OK
- 访问 /admin/myAdminPage → 403 Forbidden
@Test
public void whenTestUserCredentials_thenOk() throws Exception {
mockMvc.perform(get("/user/general/myUserPage")).andExpect(status().isFound());
mockMvc.perform(get("/user/general/myUserPage")
.with(user("user").password("userPass").roles("USER")))
.andExpect(status().isOk());
mockMvc.perform(get("/admin/myAdminPage")
.with(user("user").password("userPass").roles("USER")))
.andExpect(status().isForbidden());
}
最后测试 /guest/guestPage,所有认证方式都应返回 200 OK:
@Test
public void givenAnyUser_whenGetGuestPage_thenOk() throws Exception {
mockMvc.perform(get("/guest/myGuestPage")).andExpect(status().isOk());
mockMvc.perform(get("/guest/myGuestPage")
.with(user("user").password("userPass").roles("USER")))
.andExpect(status().isOk());
mockMvc.perform(get("/guest/myGuestPage")
.with(httpBasic("admin", "adminPass")))
.andExpect(status().isOk());
}
5. 总结
本文详细演示了在 Spring Security 中配置多入口点的完整方案。完整源码可在 GitHub 获取。
运行应用:
- 取消 pom.xml 中 MultipleEntryPointsApplication 的 start-class 注释
- 执行
mvn spring-boot:run
- 访问 /multipleHttpLinks URL
⚠️ HTTP Basic 认证无法直接登出,需关闭浏览器清除认证状态。
运行 JUnit 测试:
mvn clean install -PentryPoints