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