Spring Boot Webflux Security - чтение принципала в классе обслуживания при написании тестов - PullRequest
0 голосов
/ 19 июня 2019

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

Мои настройки:

Я пишу REST API Spring Boot 2 с использованием 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 Thread Local для извлечения принципала, и если я делаю это, как мне писать тесты для моей службы?

Если я использую контекст безопасности, как я проверяю реализации моей службы, которые ожидают, что принципал имеет типJWTAuthenticationToken JWTAuthenticationToken

и я всегда получаю 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();
    }

}

Ответы [ 2 ]

1 голос
/ 24 июня 2019

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

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

В этом ответе больше информации, связанной с темой - https://stackoverflow.com/a/51350355/197342

0 голосов
/ 24 июня 2019

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

Я добавил следующие методы для чтения идентификатора из утверждений, где он обычно находится в токене 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 "test@test.com";
    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);
    }

...