1. 引言

在这个教程中,我们将学习如何在Spring Security配置中使用两个不同的登录页面,通过配置两个不同的http元素来实现。

2. 配置2个Http元素

我们可能需要两个登录页面的情况是,当应用程序有一个管理员和一个普通用户的不同登录界面时。我们将配置两个http元素,它们将通过每个关联的URL模式进行区分:

  • **/user***:需要普通用户身份验证才能访问的页面
  • **/admin***:由管理员访问的页面

每个http元素将有不同的登录页面和登录处理URL。

为了配置两个不同的http元素,我们将创建两个带有@Configuration注解的静态类。

这两个都将放置在一个常规的@Configuration类内:

@Configuration
@EnableWebSecurity
public class SecurityConfig {
    ...
}

现在,让我们定义“ADMIN”用户的ConfigurerAdapter

   @Configuration
    @Order(1)
    public static class App1ConfigurationAdapter {

        @Bean
        public SecurityFilterChain filterChainApp1(HttpSecurity http, HandlerMappingIntrospector introspector) throws Exception {
            MvcRequestMatcher.Builder mvcMatcherBuilder = new MvcRequestMatcher.Builder(introspector);
            http.securityMatcher("/admin*")
                .authorizeHttpRequests(authorizationManagerRequestMatcherRegistry ->
                            authorizationManagerRequestMatcherRegistry.requestMatchers(mvcMatcherBuilder.pattern("/admin*")).hasRole("ADMIN"))
                // log in
                .formLogin(httpSecurityFormLoginConfigurer ->
                        httpSecurityFormLoginConfigurer.loginPage("/loginAdmin")
                                .loginProcessingUrl("/admin_login")
                                .failureUrl("/loginAdmin?error=loginError")
                                .defaultSuccessUrl("/adminPage"))
                // logout
                .logout(httpSecurityLogoutConfigurer ->
                        httpSecurityLogoutConfigurer.logoutUrl("/admin_logout")
                                .logoutSuccessUrl("/protectedLinks")
                                .deleteCookies("JSESSIONID"))
                .exceptionHandling(httpSecurityExceptionHandlingConfigurer ->
                        httpSecurityExceptionHandlingConfigurer.accessDeniedPage("/403"))
                .csrf(AbstractHttpConfigurer::disable);

            return http.build();
        }
    }

接着,定义普通用户的ConfigurerAdapter

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

        @Bean
        public SecurityFilterChain filterChainApp2(HttpSecurity http, HandlerMappingIntrospector introspector) throws Exception {
            MvcRequestMatcher.Builder mvcMatcherBuilder = new MvcRequestMatcher.Builder(introspector);
            http.securityMatcher("/user*")
                .authorizeHttpRequests(authorizationManagerRequestMatcherRegistry ->
                            authorizationManagerRequestMatcherRegistry.requestMatchers(mvcMatcherBuilder.pattern("/user*")).hasRole("USER"))
                // log in
                .formLogin(httpSecurityFormLoginConfigurer ->
                        httpSecurityFormLoginConfigurer.loginPage("/loginUser")
                                .loginProcessingUrl("/user_login")
                                .failureUrl("/loginUser?error=loginError")
                                .defaultSuccessUrl("/userPage"))
                // logout
                .logout(httpSecurityLogoutConfigurer ->
                        httpSecurityLogoutConfigurer.logoutUrl("/user_logout")
                                .logoutSuccessUrl("/protectedLinks")
                                .deleteCookies("JSESSIONID"))
                .exceptionHandling(httpSecurityExceptionHandlingConfigurer ->
                        httpSecurityExceptionHandlingConfigurer.accessDeniedPage("/403"))
                .csrf(AbstractHttpConfigurer::disable);
            return http.build();
        }
    }

请注意,通过在每个静态类上放置@Order注解,我们指定了在请求URL匹配时两个类被考虑的顺序。

两个配置类不能有相同的顺序。

3. 定制登录页面

我们将为每种类型的用户创建自定义登录页面。对于管理员用户,登录表单的动作将按照配置设定为"user_login"

<p>User login page</p>
<form name="f" action="user_login" method="POST">
    <table>
        <tr>
            <td>User:</td>
            <td><input type="text" name="username" value=""></td>
        </tr>
        <tr>
            <td>Password:</td>
            <td><input type="password" name="password" /></td>
        </tr>
        <tr>
            <td><input name="submit" type="submit" value="submit" /></td>
        </tr>
    </table>
</form>

管理员的登录页面类似,但表单动作将根据Java配置设定为"admin_login"

4. 身份验证配置

现在我们需要为我们的应用程序配置身份验证。让我们看看两种实现方法——一种是使用共同的用户身份验证源,另一种是使用两个独立的身份验证源。

4.1. 使用共同的用户身份验证源

如果两个登录页面共享一个共同的用户身份验证源,你可以创建一个类型为UserDetailsService的单例bean,它将处理身份验证。

让我们用一个InMemoryUserDetailsManager示例来演示这种情况,它定义了两个用户——一个具有“USER”角色,另一个具有“ADMIN”角色:

@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 static PasswordEncoder encoder() {
    return new BCryptPasswordEncoder();
}

4.2. 使用两个不同的用户身份验证源

如果你有两个不同的用户身份验证源——一个是管理员,一个是普通用户——你可以在每个静态@Configuration类中配置一个AuthenticationManagerBuilder。以下是为“ADMIN”用户配置身份管理器的一个例子:

@Configuration
@Order(1)
public static class App1ConfigurationAdapter {

    @Bean
    public UserDetailsService userDetailsServiceApp1() {
         UserDetails user = User.withUsername("admin")
             .password(encoder().encode("admin"))
             .roles("ADMIN")
             .build();
         return new InMemoryUserDetailsManager(user);
    }
}

在这种情况下,上一节中的UserDetailsService bean将不再使用。

6. 总结

在这篇快速教程中,我们展示了如何在同一个Spring Security应用中实现两个不同的登录页面。

本文的完整代码可以在GitHub项目中找到。

运行应用时,你可以在*/protectedLinks* URI上查看上述示例。