Как часто вам приходится тестировать аутентификацию в ваших юнит тестах Spring Boot приложений? Мне довольно часто.
Я работаю с oauth2 реализацией от Spring Security и в сервисах подключен стартер spring-boot-starter-oauth2-resource-server
.
Каждый раз, когда мне требуется протестировать какой либо класс или метод, где приходится работать с SecurityContextHolder и доставать JwtAuthenticationToken из контекста, я смотрю на способы, с помощью которых я это делаю и мне не нравится реализация.
И я начинаю переписывать, и переписывать и переписывать... И кажется, сейчас я подобрал самый удобный и лаконичный способ и делюсь им с вами.
Давайте перейдем к делу!
Для возможности корректного тестирования аутентификации добавим в зависимости библиотеку spring-security-test:
Maven
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>
Gradle
testImplementation 'org.springframework.security:spring-security-test'
В первую очередь создаю аннотацию WithMockJwtAuthentication:
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
@WithSecurityContext(factory = WithMockJwtAuthenticationSecurityContextFactory.class)
public @interface WithMockJwtAuthentication {
String subject() default "user@example.com";
Claim[] claims() default {};
int expiresInSeconds() default 300;
@AliasFor(annotation = WithSecurityContext.class)
TestExecutionEvent setupBefore() default TestExecutionEvent.TEST_METHOD;
@interface Claim {
String name();
String value();
}
}
И добавляю класс WithMockJwtAuthenticationSecurityContextFactory:
public class WithMockJwtAuthenticationSecurityContextFactory implements WithSecurityContextFactory<WithMockJwtAuthentication> {
private final SecurityContextHolderStrategy securityContextHolderStrategy = SecurityContextHolder.getContextHolderStrategy();
@Override
public SecurityContext createSecurityContext(WithMockJwtAuthentication annotation) {
Instant issuedAt = Instant.now();
Instant expiresAt = issuedAt.plusSeconds(annotation.expiresInSeconds());
Map<String, Object> claims = new HashMap<>();
for (WithMockJwtAuthentication.Claim claim : annotation.claims()) {
claims.put(claim.name(), claim.value());
}
Jwt jwt = Jwt.withTokenValue("token")
.header("alg", "none")
.subject(annotation.subject())
.issuedAt(issuedAt)
.expiresAt(expiresAt)
.claims(it -> it.putAll(claims))
.build();
Authentication authentication = new JwtAuthenticationToken(jwt);
SecurityContext context = this.securityContextHolderStrategy.createEmptyContext();
context.setAuthentication(authentication);
return context;
}
}
На этом все. Если не требуется расширять текущий вариант, то можно переходить к подключению аннотации в тестах...
Добавление аннотации @WithMockJwtAuthentication к тестовому методу:

Попробуем продебажить тест и убедиться, что действительно в security context попадает наш токен аутентификации:

Выводы:
Я получил удобный способ внедрять JwtAuthenticationToken в security context как если бы мой сервис принимал bearer token в заголовках запроса
Вы можете расширить мой пример под свои нужды, упаковать в свою библиотеку и удобно подключать к своим сервисам
А как вы работаете с аутентификацией в юнит тестах?
Lewigh
Тема - чем это удобнее чем одни раз написать функцию и вызывать ее пред тестами, не раскрыта.
Dmitrii_Demchenko Автор
Если вы про
@BeforeEach
, то вы не управляете в каких тестах вам нужна аутентификация, в каких нет.Если про то, что в каждом тесте вызывать метод, который будет инжектить аутентификацию в security context, то у вас появляется лишний код в вашем тесте.
Поэтому решение на аннотациях на мой взгляд выглядит более лаконичным и чистым.
ris58h
Так он у вас и с аннотациями появляется, только с ними код из тэста ещё и за пределы этого тэста уезжает.
Dmitrii_Demchenko Автор
Мое решение с аннотациями является расширением возможностей библиотеки spring-security-test. И если есть такая возможность, почему бы не воспользоваться тем, что нам дали разработчики спринга, и не делать фабричные методы или что-то другое.
Никого ни к чему не принуждаю, каждый выбирает то, что ему нравится больше всего. Всего лишь делюсь своим опытом.