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 获取:

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 获取。

运行应用:

  1. 取消 pom.xmlMultipleEntryPointsApplicationstart-class 注释
  2. 执行 mvn spring-boot:run
  3. 访问 /multipleHttpLinks URL

⚠️ HTTP Basic 认证无法直接登出,需关闭浏览器清除认证状态。

运行 JUnit 测试:

mvn clean install -PentryPoints

原始标题:Multiple Entry Points in Spring Security

« 上一篇: Google Guice 指南
» 下一篇: Java WebSocket API 指南