Spring Boot Webflux Security — чтение Принципала в классе обслуживания при написании тестов

Я новичок в экосистеме Spring в целом и в Webflux. Есть две вещи, которые я пытаюсь выяснить и не могу найти никаких подробностей.

Моя установка:

Я пишу Spring Boot 2 REST API, используя WebFlux (не используя контроллеры, а скорее функции обработчика). Сервер аутентификации — это отдельная служба, которая выдает токены JWT, которые прикрепляются к каждому запросу в виде заголовков аутентификации. Вот простой пример метода запроса:

public Mono<ServerResponse> all(ServerRequest serverRequest) {
        return principal(serverRequest).flatMap(principal ->
                ReactiveResponses.listResponse(this.projectService.all(principal)));
    }

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

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

Проблемы:

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

Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal();

Вопрос 1:

Является ли это хорошей практикой в ​​целом при написании функционального кода (если я делаю это вместо распространения принципала)? это хороший подход, несмотря на сложность чтения и отправки принципала из запроса в сервис в каждом методе?

Вопрос 2:

Должен ли я вместо этого использовать локальный поток SecurityContextHolder для получения принципала, и если я это сделаю, как мне написать тесты для моей службы?

Если я использую контекст безопасности, как мне протестировать свои реализации службы, ожидающие принципала типа JWTAuthenticationTokenSpring Boot Webflux Security — чтение Принципала в классе обслуживания при написании тестов

и я всегда получаю null, когда пытаюсь сделать что-то вроде описанного здесь: Модульное тестирование с Spring Security

В сервисных тестах В тестах то, что мне удалось сделать до сих пор, - это распространить принципала на методы службы и использовать mockito для имитации принципала. Это довольно просто. В тестах конечной точки я использую @WithMockUser для заполнения принципала при выполнении запросов, и я имитирую уровень сервиса. Недостатком этого является то, что основной тип отличается.

Вот как выглядит мой тестовый класс для сервисного слоя:

@DataMongoTest
@Import({ProjectServiceImpl.class})
class ProjectServiceImplTest extends BaseServiceTest {

    @Autowired
    ProjectServiceImpl projectService;

    @Autowired
    ProjectRepository projectRepository;

    @Mock
    Principal principal;

    @Mock
    Principal principal2;

    @BeforeEach
    void setUp() {
        initMocks(this);

        when(principal.getName()).thenReturn("uuid");
        when(principal2.getName()).thenReturn("uuid2");
    }

    // Cleaned for brevity 

    @Test
    public void all_returnsOnlyOwnedProjects() {
        Flux<Project> saved = projectRepository.saveAll(
                Flux.just(
                        new Project(null, "First", "uuid"),
                        new Project(null, "Second", "uuid2"),
                        new Project(null, "Third", "uuid3")
                )
        );
        Flux<Project> all = projectService.all(principal2);
        Flux<Project> composite = saved.thenMany(all);

        StepVerifier
                .create(composite)
                .consumeNextWith(project -> {
                    assertThat(project.getOwnerUserId()).isEqualTo("uuid2");
                })
                .verifyComplete();
    }

}
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Приходилось ли вам сталкиваться с требованиями, в которых вас могли попросить поднять тревогу или выдать ошибку, когда метод Java занимает больше...
Версия Java на основе версии загрузки
Версия Java на основе версии загрузки
Если вы зайдете на официальный сайт Spring Boot , там представлен start.spring.io , который упрощает создание проектов Spring Boot, как показано ниже.
Документирование API с помощью Swagger на Springboot
Документирование API с помощью Swagger на Springboot
В предыдущей статье мы уже узнали, как создать Rest API с помощью Springboot и MySql .
2
0
5 162
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Поскольку вы используете Webflux, вы должны использовать ReactiveSecurityContextHolder для получения принципала следующим образом: Object principal = ReactiveSecurityContextHolder.getContext().getAuthentication().getPrincipal();

Использование нереактивного вернет null, как вы видите.

В этом ответе есть дополнительная информация по теме - https://stackoverflow.com/a/51350355/197342

Большое спасибо. Я не знаю, как я пропустил это. Это привело меня к документации и помогло с пользовательским фиктивным токеном. Я опубликую свое окончательное решение ниже в качестве ссылки.

DArkO 24.06.2019 15:45
Ответ принят как подходящий

Основываясь на другом ответе, мне удалось решить эту проблему следующим образом.

Я добавил следующие методы для чтения идентификатора из утверждений, где он обычно находится в токене JWT.

    public static Mono<String> currentUserId() {
        return jwt().map(jwt -> jwt.getClaimAsString(USER_ID_CLAIM_NAME));
    }


    public static Mono<Jwt> jwt() {
        return ReactiveSecurityContextHolder.getContext()
                .map(context -> context.getAuthentication().getPrincipal())
                .cast(Jwt.class);
    }

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

Сложной частью всегда было тестирование. Я могу решить эту проблему с помощью пользовательского SecurityContextFactory. Я создал аннотацию, которую я могу прикрепить так же, как @WithMockUser, но вместо этого с некоторыми деталями претензии, которые мне нужны.

@Retention(RetentionPolicy.RUNTIME)
@WithSecurityContext(factory = WithMockTokenSecurityContextFactory.class)
public @interface WithMockToken {
    String sub() default "uuid";
    String email() default "[email protected]";
    String name() default "Test User";
}

Затем Фабрика:

String token = "....ANY_JWT_TOKEN_GOES_HERE";

    @Override
    public SecurityContext createSecurityContext(WithMockToken tokenAnnotation) {
        SecurityContext context = SecurityContextHolder.createEmptyContext();
        HashMap<String, Object> headers = new HashMap<>();
        headers.put("kid", "SOME_ID");
        headers.put("typ", "JWT");
        headers.put("alg", "RS256");
        HashMap<String, Object> claims = new HashMap<>();
        claims.put("sub", tokenAnnotation.sub());
        claims.put("aud", new ArrayList<>() {{
            add("SOME_ID_HERE");
        }});
        claims.put("updated_at", "2019-06-24T12:16:17.384Z");
        claims.put("nickname", tokenAnnotation.email().substring(0, tokenAnnotation.email().indexOf("@")));
        claims.put("name", tokenAnnotation.name());
        claims.put("exp", new Date());
        claims.put("iat", new Date());
        claims.put("email", tokenAnnotation.email());
        Jwt jwt = new Jwt(token, Instant.now(), Instant.now().plus(1, ChronoUnit.HOURS), headers,
                claims);
        JwtAuthenticationToken jwtAuthenticationToken = new JwtAuthenticationToken(jwt, AuthorityUtils.NO_AUTHORITIES); // Authorities are needed to pass authentication in the Integration tests
        context.setAuthentication(jwtAuthenticationToken);


        return context;
    }

Тогда простой тест будет выглядеть так:

    @Test
    @WithMockToken(sub = "uuid2")
    public void delete_whenNotOwner() {
        Mono<Void> deleted = this.projectService.create(projectDTO)
                .flatMap(saved -> this.projectService.delete(saved.getId()));

        StepVerifier
                .create(deleted)
                .verifyError(ProjectDeleteNotAllowedException.class);
    }

Другие вопросы по теме