Spring Security中的多个入口点

2023/05/17

1. 概述

在本快速教程中,我们将了解如何在Spring Security应用程序中定义多个入口点

这主要需要在XML配置文件中定义多个http块,或者通过多次创建SecurityFilterChain bean来定义多个HttpSecurity实例。

2. Maven依赖

对于开发,我们需要以下依赖项:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
    <version>2.7.2</version>
</dependency>
<dependency> 
    <groupId>org.springframework.boot</groupId> 
    <artifactId>spring-boot-starter-web</artifactId> 
    <version>2.7.2</version> 
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-thymeleaf</artifactId>
    <version>2.7.2</version>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <version>2.7.2</version>
</dependency>    
<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-test</artifactId>
    <version>5.4.0</version>
</dependency>

最新版本的spring-boot-starter-securityspring-boot-starter-webspring-boot-starter-thymeleafspring-boot-starter-testspring-security-test可以从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();
    }
}

现在,让我们看看如何在安全配置中定义多个入口点

我们将在这里使用一个由基本身份验证驱动的示例,我们将充分利用Spring Security支持在我们的配置中定义多个HTTP元素的事实。

使用Java配置时,定义多个安全域(realms)的方法是使用多个@Configuration类-每个类都有自己的安全配置。这些类可以是静态的并放置在主配置类中。

在一个应用程序中拥有多个入口点的主要动机是,如果有不同类型的用户可以访问应用程序的不同部分。

让我们定义一个具有三个入口点的配置,每个入口点具有不同的权限和身份验证模式:

  • 一个用于使用HTTP基本身份验证的管理员用户
  • 一个用于使用表单身份验证的普通用户
  • 一个用于不需要身份验证的来宾用户

为管理员用户定义的入口点保护任何匹配/admin/**的URL,以仅允许具有ADMIN角色的用户访问,并且需要使用authenticationEntryPoint()方法设置的入口点类型为BasicAuthenticationEntryPoint的HTTP基本身份验证:

@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());
        return http.build();
    }

    @Bean
    public AuthenticationEntryPoint authenticationEntryPoint(){
        BasicAuthenticationEntryPoint entryPoint = new BasicAuthenticationEntryPoint();
        entryPoint.setRealmName("admin realm");
        return entryPoint;
    }
}

每个静态类上的@Order注解指示将考虑配置以找到与请求的URL匹配的配置的顺序。每个类的order值必须是唯一的

BasicAuthenticationEntryPoint类型的bean需要设置属性realName。

3.2 多个入口点,相同的HTTP元素

接下来,让我们为/user/**形式的URL定义配置,具有USER角色的普通用户可以使用表单身份验证访问这些URL:

@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/**形式的URL设置了两个不同的入口点。

接下来,我们需要在同一个静态配置类中定义入口点bean:

@Bean
public AuthenticationEntryPoint loginUrlAuthenticationEntryPoint() {
    return new LoginUrlAuthenticationEntryPoint("/userLogin");
}

@Bean
public AuthenticationEntryPoint loginUrlAuthenticationEntryPointWithWarning() {
    return new LoginUrlAuthenticationEntryPoint("/userLoginWithWarning");
}

这里的重点是如何设置这些多个入口点-不一定是每个入口点的实现细节。

在这种情况下,入口点都是LoginUrlAuthenticationEntryPoint类型,并使用不同的登录页面URL:/userLogin用于简单登录页面,/userLoginWithWarning用于在尝试访问/user/私有URL时也会显示警告的登录页面。

此配置还需要定义/userLogin和/userLoginWithWarning MVC映射以及两个具有标准登录表单的页面。

对于表单身份验证,非常重要的是要记住配置所需的任何URL,例如登录处理URL也需要遵循/user/**格式或以其他方式配置为可访问。

如果没有适当角色的用户尝试访问受保护的URL,上述两种配置都将重定向到/403 URL。

请注意为bean使用唯一的名称,即使它们位于不同的静态类中,否则会出现覆盖情况

3.3 新的HTTP元素,没有入口点

最后,让我们为/guest/**形式的URL定义第三个配置,该配置将允许所有类型的用户,包括未经身份验证的用户:

@Configuration
@Order(3)
public static class App3ConfigurationAdapter {

    public SecurityFilterChain filterChainApp3(HttpSecurity http) throws Exception {
        http.antMatcher("/guest/**").authorizeRequests().anyRequest().permitAll();
        return http.build();
    }
}

3.4 XML配置

让我们看一下上一节中三个HttpSecurity实例的等效XML配置。

正如预期的那样,这将包含三个单独的<http>块。

对于/admin/** URL,XML配置将使用http-basic元素的entry-point-ref属性:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:security="http://www.springframework.org/schema/security"
       xmlns="http://www.springframework.org/schema/beans"
       xsi:schemaLocation="http://www.springframework.org/schema/security http://www.springframework.org/schema/security/spring-security-4.2.xsd
		http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">

    <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:access-denied-handler error-page="/403"/>
    </security:http>

    <bean id="authenticationEntryPoint"
          class="org.springframework.security.web.authentication.www.BasicAuthenticationEntryPoint">
        <property name="realmName" value="admin realm"/>
    </bean>
</beans>

**这里需要注意的是,如果使用XML配置,角色必须采用ROLE_的形式**。

/user/** URL的配置必须在xml中分解为两个http块,因为没有直接等效于defaultAuthenticationEntryPointFor()的方法。

URL /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')"/>
    <security:form-login login-processing-url="/user/general/login"
                         authentication-failure-url="/userLogin?error=loginError"
                         default-target-url="/user/myUserPage"/>
    <security:csrf disabled="true"/>
    <security:access-denied-handler error-page="/403"/>
    <security:logout logout-url="/user/logout" delete-cookies="JSESSIONID" logout-success-url="/multipleHttpLinks"/>
</security:http>

<bean id="loginUrlAuthenticationEntryPoint"
      class="org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint">
    <constructor-arg name="loginFormUrl" value="/userLogin"/>
</bean>

对于/user/private/** URL,我们可以定义类似的配置:

<security:http pattern="/user/private/**" use-expressions="true" auto-config="true"
               entry-point-ref="loginUrlAuthenticationEntryPointWithWarning">
    <security:intercept-url pattern="/**" access="hasRole('ROLE_USER')"/>
    <security:form-login login-processing-url="/user/private/login"
                         authentication-failure-url="/userLogin?error=loginError"
                         default-target-url="/user/myUserPage"/>
    <security:csrf disabled="true"/>
    <security:access-denied-handler error-page="/403"/>
    <security:logout logout-url="/user/logout" delete-cookies="JSESSIONID" logout-success-url="/multipleHttpLinks"/>
</security:http>

<bean id="loginUrlAuthenticationEntryPointWithWarning"
      class="org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint">
    <constructor-arg name="loginFormUrl" value="/userLoginWithWarning"/>
</bean>

对于/guest/** URL,我们配置以下http元素:

<security:http pattern="/**" use-expressions="true" auto-config="true">
    <security:intercept-url pattern="/guest/**" access="permitAll()"/>
</security:http>

这里同样重要的是,至少有一个XML <http>块必须匹配/**模式。

4. 访问受保护的URL

4.1 MVC配置

让我们创建与我们保护的URL模式相匹配的请求映射:

@Controller
public class PagesController {

    @RequestMapping("/multipleHttpLinks")
    public String getMultipleHttpLinksPage() {
        return "multipleHttpElems/multipleHttpLinks";
    }

    @RequestMapping("/admin/myAdminPage")
    public String getAdminPage() {
        return "multipleHttpElems/myAdminPage";
    }

    @RequestMapping("/user/general/myUserPage")
    public String getUserPage() {
        return "multipleHttpElems/myUserPage";
    }

    @RequestMapping("/user/private/myPrivateUserPage")
    public String getPrivateUserPage() {
        return "multipleHttpElems/myPrivateUserPage";
    }

    @RequestMapping("/guest/myGuestPage")
    public String getGuestPage() {
        return "multipleHttpElems/myGuestPage";
    }
}

/multipleHttpLinks映射将返回一个简单的HTML页面,其中包含指向受保护URL的链接:

<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应用程序运行,所以让我们用main方法定义一个类:

@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 测试安全配置

让我们设置一个可用于测试受保护URL的JUnit测试类:

@ExtendWith(SpringExtension.class)
@SpringBootTest(classes = MultipleEntryPointsApplication.class)
@WebAppConfiguration
class MultipleEntryPointsIntegrationTest {

    @Autowired
    private WebApplicationContext context;

    @Autowired
    private FilterChainProxy filterChainProxy;

    private MockMvc mockMvc;

    @BeforeEach
    void setUp() {
        mockMvc = MockMvcBuilders.webAppContextSetup(context).addFilter(filterChainProxy).build();
    }
}

接下来,让我们使用管理员用户测试URL。

在没有HTTP基本身份验证的情况下请求/admin/adminPage URL时,我们应该会收到Unauthorized状态码,并且在完成身份验证后状态码应该是200 OK。

如果尝试使用admin用户访问/user/userPage URL,我们应该收到302 Forbidden:

@Test
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());
}

让我们使用常规用户凭据创建一个类似的测试来访问URL:

@Test
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());
}

在第二个测试中,我们可以看到缺少表单身份验证将导致状态为302 Found而不是Unauthorized,因为Spring Security将重定向到登录表单。

最后,让我们创建一个测试,在其中我们访问/guest/guestPage URL将进行所有三种类型的身份验证并验证我们收到200 OK状态码:

@Test
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基本身份验证时无法注销,因此你必须关闭并重新打开浏览器才能删除此前的登录信息。

要运行JUnit测试,请使用已定义的Maven Profile entryPoints和以下命令:

mvn clean install -PentryPoints

与往常一样,本教程的完整源代码可在GitHub上获得。

Show Disqus Comments

Post Directory

扫码关注公众号:Taketoday
发送 290992
即可立即永久解锁本站全部文章