Управление безопасностью конечных точек и подходы к тестированию - PullRequest
1 голос
/ 21 апреля 2020

Я работаю над небольшим проектом, который включает в себя регистрацию учетной записи, вход пользователя в систему и запрос данных через определенные конечные точки. Проверка подлинности выполняется с помощью идентификаторов сеансов безопасности Spring. Конечные точки состоят из общедоступных конечных точек (т. Е. Регистрируются или забыли пароль) и некоторых конечных точек, требующих входа пользователя (т. Е. Меняют пароль, получают некоторый контент и т. Д. c). Примерно так:

@RestController
public class FightController {

//publicly available
    @GetMapping("/public/foo")
    String methodForEveryone() { 
        return "Hi common dude";
    }

    @GetMapping("secret/bar")
    String methodForSpecialPeople() {
        return "What happens in fight controller...";
    }
}

Spring Security затем имеет список опубликованных c конечных точек в состоянии c WHITE_LIST

private static final String[] AUTH_WHITELIST = {
            //public endpoints
            "/public/foo", "swagger", "etc"
}
@Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .addFilterAfter(customAuthFilter(), RequestHeaderAuthenticationFilter.class)
                .authorizeRequests()
                .antMatchers(AUTH_WHITELIST).permitAll()
                .antMatchers("/**").authenticated()
                .and()

Тесты в настоящее время выполняются нажатием каждого конечная точка и определение того, ведет ли она себя так, как ожидалось (с помощью пользовательских антипатчей в WebSecurityConfigurer) Например:

package com.fight.testpackages

public class EndpointList {

    public static class PublicEndpoints implements ArgumentsProvider {
        @Override
        public Stream<? extends Arguments> provideArguments(ExtensionContext context) {
            return Stream.of(
                    Arguments.of("/public/foo"),
            );
        }
    }

    public static class PrivateEndoints implements ArgumentsProvider {
        @Override
        public Stream<? extends Arguments> provideArguments(ExtensionContext context) {
            return Stream.of(
                    Arguments.of("secret/bar"),
            );
        }
    }

и затем с помощью таких тестов, как

    @ParameterizedTest
    @ArgumentsSource(PrivateContentEndpoints.class)
    public void privateEndpoint_unauthorizedUser_isUnauthorizedResponse(String url) throws Exception {
        assertFalse(super.isAuthenticated(url));
    }

    @WithMockUser(roles = "USER")
    @ParameterizedTest
    @ArgumentsSource(PublicAccountManagementEndpoints.class)
    public void publicEndpoint_authorizedUser_hasAccess(String url) throws Exception {
        assertTrue(super.isAuthenticated(url));
    }

. Проблема, которую я пытаюсь решить, лучше всего описать с помощью следующего сценария:

  • Разработчик добавляет новую конечную точку;
  • Они добавляют конечную точку в список antmatchers (если она должна быть опубликована c);
  • И затем они добавляют конечную точку в список publi c и частные конечные точки, которые включаются в тесты.

Проблема здесь в том, что нет принудительного применения этого поведения, и очень легко забыть добавить конечную точку в тест, или если имена конечных точек изменяются, то тесты необходимо обновить.

Текущая настройка работает, но мне было интересно, существует ли стандарт для этого ? Я посмотрел на @PreAuthorize и @RolesAllowed и др. c, но они кажутся полезными только для защиты метода, а не для его публикации c. На самом деле я хочу, чтобы обратное (то есть конечная точка была частной по умолчанию, а затем было намеренно помечено как общедоступное).

Я пришел к следующему решению:

  • Создать аннотацию
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface EndpointSecurity {

    boolean isPublic() default false;
}
  • назначить аннотацию для метода, если вы хотите, чтобы она была опубликована c
    @EndpointSecurity(isPublic = true)    
    @GetMapping("/public/foo")
    String methodForEveryone() { 
        return "Hi common dude";
    }
  • Создайте сканер, который проверяет все методы RestController для аннотации EndpointSecurity и аннотации отображения REST, как показано ниже. Надеюсь, этого достаточно, чтобы получить точку ..
@DependsOn("classScanner")
public class ClassMethodScanner {

    private final List<Class<? extends Annotation>> annotationFilters;
    private List<Method> annotatedMethods;
    private final AnnotationScanner<?> classScanner;

    public <T extends Annotation> ClassMethodScanner(ClassScanner<T> classScanner) {
        this(classScanner, Collections.emptyList());
    }

    public <T extends Annotation> ClassMethodScanner(ClassScanner<T> classScanner,
                                                     List<Class<? extends Annotation>> annotations) {
        this.classScanner = classScanner;
        this.annotationFilters = annotations;
    }

    @PostConstruct
    void extractAnnotatedMethods() throws ClassNotFoundException {
        if (annotatedMethods == null) {
            annotatedMethods =
                    classScanner.getAnnotatedHandlers().stream()
                            .sequential()
                            .map(Class::getDeclaredMethods)
                            .flatMap(Arrays::stream)
                            .filter(this::hasExpectedAnnotations)
                            .collect(Collectors.toUnmodifiableList());
        }
    }

    private boolean hasExpectedAnnotations(Method method) {
        return
                (annotationFilters.isEmpty() && method.getAnnotations().length > 0)
                        || annotationFilters.stream().anyMatch(method::isAnnotationPresent);
    }

    //Is there a good way of making this protected?
    public List<Method> getAnnotatedMethods() {
        return annotatedMethods;
    }
}
  • И наконец, создайте список публикуемых c и частных конечных точек, которые передаются в HttpSecurity.
public class SecurityEndpoints {

    private List<String> publicEndpoints;
    private List<String> privateEndpoints;
    private final EndpointDetailsCollector<?> collector;

   public String[] getWhiteList() {
  • и передаются в EndpointList, о котором я упоминал выше.

Это кажется несколько запутанным, хотя. Так было интересно, что такое стандартный подход, или я слишком много тестирую конечные точки ??

...