Multiple Entry Points in Spring Security – Spring安全中的多个入口点

最后修改: 2017年 3月 14日

中文/混合/英文(键盘快捷键:t)

1. Overview

1.概述

In this quick tutorial, we’re going to take a look at how to define multiple entry points in a Spring Security application.

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

This mainly entails defining multiple http blocks in an XML configuration file or multiple HttpSecurity instances by creating the SecurityFilterChain bean multiple times.

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

2. Maven Dependencies

2.Maven的依赖性

For development, we will need the following dependencies:

对于开发,我们将需要以下的依赖性。

<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>

The latest versions of spring-boot-starter-security, spring-boot-starter-webspring-boot-starter-thymeleaf, spring-boot-starter-test, spring-security-test can be downloaded from Maven Central.

spring-boot-starter-securityspring-boot-starter-webspring-boot-starter-thymeleafspring-boot-starter-testspring-security-test的最新版本可以从Maven中心下载。

3. Multiple Entry Points

3.多个入口点

3.1. Multiple Entry Points With Multiple HTTP Elements

3.1.具有多个HTTP元素的多个入口点

Let’s define the main configuration class that will hold a user source:

让我们定义主配置类,它将容纳一个用户源。

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

Now, let’s look at how we can define multiple entry points in our security config.

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

We’re going to use an example driven by Basic Authentication here, and we’re going to make good use of the fact that Spring Security supports the definition of multiple HTTP elements in our configurations.

我们将在这里使用一个由Basic Authentication驱动的例子,我们将很好地利用Spring Security支持在我们的配置中定义多个HTTP元素这一事实。

When using Java configuration, the way to define multiple security realms is to have multiple @Configuration classes – each with its own security configuration. These classes can be static and placed inside the main config.

在使用Java配置时,定义多个安全领域的方法是拥有多个@Configuration类–每个类都有自己的安全配置。这些类可以是静态的,放在主配置里面。

The main motivation for having multiple entry points in one application is if there are different types of users that can access different portions of the application.

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

Let’s define a configuration with three entry points, each with different permissions and authentication modes:

让我们定义一个有三个入口点的配置,每个入口点有不同的权限和认证模式。

  • one for administrative users using HTTP Basic Authentication
  • one for regular users that use form authentication
  • and one for guest users that do not require authentication

The entry point defined for administrative users secures URLs of the form /admin/** to only allow users with a role of ADMIN and requires HTTP Basic Authentication with an entry point of type BasicAuthenticationEntryPoint that is set using the authenticationEntryPoint() method:

为管理用户定义的入口点保证了/admin/**形式的URL的安全,只允许角色为ADMIN的用户,并且需要HTTP基本认证,入口点类型为BasicAuthenticationEntryPoint,使用authenticationEntryPoint()方法设置。

@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;
    }
}

The @Order annotation on each static class indicates the order in which the configurations will be considered to find one that matches the requested URL. The order value for each class must be unique.

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

The bean of type BasicAuthenticationEntryPoint requires the property realName be set.

BasicAuthenticationEntryPoint类型的Bean要求设置realName属性。

3.2. Multiple Entry Points, Same HTTP Element

3.2.多个入口点,同一个HTTP元素

Next, let’s define the configuration for URLs of the form /user/** that can be accessed by regular users with a USER role using form authentication:

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

@Configuration
@Order(2)
public static class App2ConfigurationAdapter {

    public SecurityFilterChain filterChainApp2(HttpSecurity http) throws Exception {
        http.antMatcher("/user/**")
            .authorizeRequests().anyRequest().hasRole("USER")
            .and()
            // formLogin configuration
            .and()
            .exceptionHandling()
            .defaultAuthenticationEntryPointFor(
              loginUrlauthenticationEntryPointWithWarning(),
              new AntPathRequestMatcher("/user/private/**"))
            .defaultAuthenticationEntryPointFor(
              loginUrlauthenticationEntryPoint(), 
              new AntPathRequestMatcher("/user/general/**"));
        return http.build();
    }
}

As we can see, another way of defining entry points, besides the authenticationEntryPoint() method, is to use the defaultAuthenticationEntryPointFor() method. This can define multiple entry points that match different conditions based on a RequestMatcher object.

我们可以看到,除了authenticationEntryPoint()方法,另一种定义入口点的方法是使用defaultAuthenticationEntryPointFor()方法。这可以定义多个入口点,根据RequestMatcher对象来匹配不同的条件。

The RequestMatcher interface has implementations based on different types of conditions, such as matching path, media type or regexp. In our example, we have used the AntPathRequestMatch to set two different entry points for URLs of the forms /user/private/** and /user/general/**.

RequestMatcher接口有基于不同类型条件的实现,如匹配路径、媒体类型或regexp。在我们的例子中,我们使用AntPathRequestMatch为/user/private/**/user/general/**形式的URL设置两个不同的入口点。

Next, we need to define the entry points beans in the same static configuration class:

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

@Bean
public AuthenticationEntryPoint loginUrlauthenticationEntryPoint(){
    return new LoginUrlAuthenticationEntryPoint("/userLogin");
}
        
@Bean
public AuthenticationEntryPoint loginUrlauthenticationEntryPointWithWarning(){
    return new LoginUrlAuthenticationEntryPoint("/userLoginWithWarning");
}

The main point here is how to set up these multiple entry points – not necessarily the implementation details of each one.

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

In this case, the entry points are both of type LoginUrlAuthenticationEntryPoint, and use different login page URL: /userLogin for a simple login page and /userLoginWithWarning for a login page that also displays a warning when attempting to access the /user/ private URLs.

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

This configuration will also require defining the /userLogin and /userLoginWithWarning MVC mappings and two pages with a standard login form.

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

For the form authentication, it’s very important to remember that any URL necessary for the configuration, such as the login processing URL also needs to follow the /user/** format or be otherwise configured to be accessible.

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

Both of the above configurations will redirect to a /403 URL if a user without the appropriate role attempts to access a protected URL.

如果一个没有相应角色的用户试图访问受保护的URL,上述两种配置都会重定向到/403 URL。

Be careful to use unique names for the beans even if they are in different static classes, otherwise one will override the other.

注意为Bean使用唯一的名字,即使它们在不同的静态类中,否则一个会覆盖另一个。

3.3. New HTTP Element, No Entry Point

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

Finally, let’s define the third configuration for URLs of the form /guest/** that will allow all types of users, including unauthenticated ones:

最后,让我们为/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 Configuration

3.4.XML配置

Let’s take a look at the equivalent XML configuration for the three HttpSecurity instances in the previous section.

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

As expected, this will contain three separate XML <http> blocks.

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

For the /admin/** URLs the XML configuration will use the entry-point-ref attribute of http-basic element:

对于/admin/**网址,XML配置将使用http-basic元素的入口点-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>

Of note here is that if using XML configuration, the roles have to be of the form ROLE_<ROLE_NAME>.

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

The configuration for the /user/** URLs will have to be broken up into two http blocks in xml because there is no direct equivalent to the defaultAuthenticationEntryPointFor() method.

/user/** URL的配置将不得不在xml中分成两个http块,因为没有直接对应的defaultAuthenticationEntryPointFor()方法。

The configuration for URLs /user/general/** is:

URLs/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 configuration      
</security:http>

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

For the /user/private/** URLs we can define a similar configuration:

对于/user/private/** URLs,我们可以定义一个类似的配置。

<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 configuration
</security:http>

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

For the /guest/** URLs we will have the http element:

对于/guest/** URLs,我们将使用http元素。

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

Also important here is that at least one XML <http> block must match the /** pattern.

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

4. Accessing Protected URLs

4.访问受保护的URLs

4.1. MVC Configuration

4.1.MVC配置

Let’s create request mappings that match the URL patterns we have secured:

让我们创建与我们所确保的URL模式相匹配的请求映射。

@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";
    }
}

The /multipleHttpLinks mapping will return a simple HTML page with links to the protected URLs:

/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>

Each of the HTML pages corresponding to the protected URLs will have a simple text and a backlink:

与受保护的URL相对应的每个HTML页面都会有一个简单的文本和一个反向链接。

Welcome admin!

<a th:href="@{/multipleHttpLinks}" >Back to links</a>

4.2. Initializing the Application

4.2.初始化应用程序

We will run our example as a Spring Boot application, so let’s define a class with the main method:

我们将以Spring Boot应用程序的形式运行我们的例子,所以让我们定义一个带有main方法的类。

@SpringBootApplication
public class MultipleEntryPointsApplication {
    public static void main(String[] args) {
        SpringApplication.run(MultipleEntryPointsApplication.class, args);
    }
}

If we want to use the XML configuration, we also need to add the @ImportResource({“classpath*:spring-security-multiple-entry.xml”}) annotation to our main class.

如果我们想使用XML配置,我们还需要在主类中添加@ImportResource({“classpath*:spring-security-multiple-entry.xml”})注解。

4.3. Testing the Security Configuration

4.3.测试安全配置

Let’s set up a JUnit test class that we can use to test our protected URLs:

让我们建立一个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();
    }
}

Next, let’s test the URLs using the admin user.

接下来,让我们用admin用户来测试一下URLs。

When requesting the /admin/adminPage URL without an HTTP Basic Authentication, we should expect to receive an Unauthorized status code, and after adding the authentication the status code should be 200 OK.

当请求/admin/adminPage URL而没有HTTP基本认证时,我们应该期待收到一个未经授权的状态代码,而在添加认证后,状态代码应该是200 OK。

If attempting to access the /user/userPage URL with the admin user, we should receive status 302 Forbidden:

如果试图用管理员用户访问/user/userPage URL,我们应该收到状态302 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());
}

Let’s create a similar test using the regular user credentials to access the URLs:

让我们创建一个类似的测试,使用普通的用户凭证来访问URLs。

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

In the second test, we can see that missing the form authentication will result in a status of 302 Found instead of Unauthorized, as Spring Security will redirect to the login form.

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

Finally, let’s create a test in which we access the /guest/guestPage URL will all three types of authentication and verify we receive a status of 200 OK:

最后,让我们创建一个测试,其中我们访问/guest/guestPage URL将所有三种类型的认证,并验证我们收到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. Conclusion

5.结论

In this tutorial, we have demonstrated how to configure multiple entry points when using Spring Security.

在本教程中,我们已经演示了在使用Spring Security时如何配置多个入口点。

The complete source code for the examples can be found over on GitHub. To run the application, uncomment the MultipleEntryPointsApplication start-class tag in the pom.xml and run the command mvn spring-boot:run, then accesses the /multipleHttpLinks URL.

示例的完整源代码可以在GitHub上找到over。要运行该应用程序,请取消注释MultipleEntryPointsApplication start-class标签,并运行mvn spring-boot:run命令,然后访问/multipleHttpLinksURL

Note that it is not possible to log out when using HTTP Basic Authentication, so you will have to close and reopen the browser to remove this authentication.

请注意,在使用HTTP基本认证时,不可能注销,所以你必须关闭并重新打开浏览器来删除这个认证。

To run the JUnit test, use the defined Maven profile entryPoints with the following command:

要运行JUnit测试,请使用定义的Maven配置文件entryPoints,命令如下。

mvn clean install -PentryPoints

mvn clean install -PentryPoints