Точки входа аутентификации - Spring Security

Точки входа аутентификации - Spring Security

Александр Косарев

Для того чтобы Spring Security инициировал процесс аутентификации пользователя используются специальные компоненты - точки входа, которые реализуют интерфейс AuthenticationEntryPoint. Точка входа может инициировать процесс аутентификации в зависимости от выбранного способа аутентификации. Так, если в приложении используется Basic-аутентификация, пользователю будет отправлен HTTP-ответ со статусом 401 Unauthorized и заголовком WWW-Authenticate: Authorize Basic …​, о чём я уже рассказывал в статье о Basic-аутентификации.

Точки входа аутентификации - Spring Security

Точка входа описывается интерфейсом AuthenticationEntryPoint:

public interface AuthenticationEntryPoint {

    void commence(HttpServletRequest request,
                  HttpServletResponse response,
                  AuthenticationException authException)
            throws IOException, ServletException;
}

Сигнатура метода commence указывает на то, что точки входа используются для обработки исключений AuthenticationException. Например, фильтры аутентификации используют их для повторного перенаправления пользователя на начало процесса аутентификации в случае, если предыдущая попытка аутентификации завершилась неудачей.

Благодаря доступу к объекту HTTP-ответа точка входа может полностью управлять ответом, который будет отправлен пользователю: задавать нужный код состояния, заголовки и, естественно, содержимое ответа.

Самая простая точка входа - Http403ForbiddenEntryPoint, которая в случае попытки получения не аутентифицированным пользователем доступа к защищённому ресурсу вернёт пользователю HTTP-ответ со статусом 403 Forbidden и пустым телом.

public class Http403ForbiddenEntryPoint implements AuthenticationEntryPoint {

    /**
     * Always returns a 403 error code to the client.
     */
    @Override
    public void commence(HttpServletRequest request,
                         HttpServletResponse response,
                         AuthenticationException exception)
            throws IOException {
        response.sendError(HttpServletResponse.SC_FORBIDDEN, "Access Denied");
    }
}

Вы обязательно столкнётесь с этой точкой входа, если не предоставите пользователям каких-либо способов аутентификации, так как она используется по умолчанию.

Регистрация точек входа

Точки входа регистрируются при помощи DSL exceptionHandling настроек цепочки фильтров безопасности. При помощи метода defaultAuthenticationEntryPointFor можно задать точку входа, специфичную для запросов, удовлетворяющих неким требованиям. Например, мы можем потребовать от клиента Basic-аутентификацию для запросов, путь которых начинается с /api, как это показано в примере кода ниже. Если ни одна специфичная точка входа не может быть использована, то будет использована основная точка входа, которая может быть задана при помощи метода authenticationEntryPoint:

@Configuration
public class SecurityConfiguration {

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        return http
            .exceptionHandling(c ->
                // основная точка входа
                c.authenticationEntryPoint(
                    new LoginUrlAuthenticationEntryPoint("/login"))
                // точка входа для REST API
                        .defaultAuthenticationEntryPointFor(
                    new BasicAuthenticationEntryPoint(),
                    new AntPathRequestMatcher("/api/**")))
            .build();
    }
}

Точки входа необязательно указывать явно, если вы используете DSL для фильтров аутентификации, так как они это делают за вас. В приведённом ниже примере DSL для настройки Basic-аутентификации автоматически зарегистрирует в качестве основной точки входа BasicAuthentictionEntryPoint, несмотря на то, что вы не делаете это явно:

@Configuration
public class SecurityConfiguration {

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        return http
            .httpBasic(Customizer.withDefaults())
            .build();
    }
}

Если основная точка входа вообще не указана, то будет использована Http403ForbiddenEntryPoint, указанная выше.

Для некоторых фильтров аутентификации можно указывать собственные точки входа, отличные от глобальных. Например, я хочу использовать в качестве глобальной точки входа BasicAuthenticationEntryPoint, а для фильтра Basic-аутентификации - Http403ForbiddenEntryPoint, сделать я это могу следующим образом:

@Configuration
public class SecurityConfiguration {

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        return http
            // Точка входа на случай ошибки Basic-аутентификации
            .httpBasic(c -> c.authenticationEntryPoint(
                    new Http403ForbiddenEntryPoint()))
            .exceptionHandling(c ->
                // основная точка входа
                c.authenticationEntryPoint(
                    new BasicAuthenticationEntryPoint()))
            .build();
    }
}

В этом случае у не аутентифицированного пользователя будет запрошена Basic-аутентификация, но в случае её ошибки будет отправлен пустой HTTP-ответ со статусом 403 Forbidden.

Использование точек входа

Точки входа используются фильтром ExceptionTranslationFilter, который перехватывает два основных типа исключений Spring Security: AuthenticationException и AccessDeniedException.

Если не аутентифицированный пользователь попытается получить доступ к защищённому ресурсу, то AuthorizationFilter выбросит исключение безопасности, в процессе обработки которого ExceptionTranslationFilter попытается его обработать при помощи наиболее подходящей точки входа. При этом ExceptionTranslationFilter сохранит данные запроса в кэше для автоматического перенаправления пользователя на целевую страницу после успешной аутентификации.

Перенаправление пользователя на страницу входа

Допустим, в приложении используется форма входа, и пользователя необходимо перенаправлять на соответствующую страницу /login, сделать это можно следующим образом:

@Configuration
public class SecurityConfiguration {

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        return http
            .authorizeHttpRequests(c -> c
                .requestMatchers("/login").permitAll()
                .anyRequest().permitAll())
            .exceptionHandling(c ->
                // основная точка входа
                c.authenticationEntryPoint(
                        (req, res, ex) -> res.sendRedirect("/login")))
            .build();
    }
}

Как показано в примере, для точки входа необязательно создавать класс, в некоторых случаях достаточно лямбда-выражений. Впрочем, для этих целей лучше использовать LoginUrlAuthenticationEntryPoint;


@Configuration
public class SecurityConfiguration {

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        return http
            .authorizeHttpRequests(c -> c
                .requestMatchers("/login").permitAll()
                .anyRequest().permitAll())
            .exceptionHandling(c ->
                // основная точка входа
                c.authenticationEntryPoint(
                        new LoginUrlAuthenticationEntryPoint("/login")))
            .build();
    }
}

Да, при использовании DSL formLogin для настройки формы входа вам не придётся настраивать точку входа вручную, но под капотом будет происходить что-то похожее.

При попытке открыть защищённую страницу не аутентифицированный пользователь будет перенаправлен на страницу /login в результате использования этой точки входа.

Логгирование исключений аутентификации

Точку входа так же можно использовать для логгирования ошибок аутентификации:

@Configuration
public class SecurityConfiguration {

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        var entryPoint = new LoginUrlAuthenticationEntryPoint("/login");
        return http
            .authorizeHttpRequests(c -> c
                .requestMatchers("/login").permitAll()
                .anyRequest().permitAll())
            .exceptionHandling(c ->
                // основная точка входа
                c.authenticationEntryPoint((req, res, e) -> {
                    // вывод стека вызова
                    e.printStackTrace();
                    // использование основной точки входа
                    entryPoint.commence(req, res, e);
                }))
            .build();
    }
}

Понравилась статья? Тогда поддержки проект и подкинь монетку:

Больше полезных статей и роликов:

Report Page