Я работаю над небольшим проектом, который включает в себя регистрацию учетной записи, вход пользователя в систему и запрос данных через определенные конечные точки. Проверка подлинности выполняется с помощью идентификаторов сеансов безопасности 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, о котором я упоминал выше.
Это кажется несколько запутанным, хотя. Так было интересно, что такое стандартный подход, или я слишком много тестирую конечные точки ??