Основы безопасности
Сейчас любой может создать или удалить статью. Аутентификация отвечает на вопрос «кто это?», авторизация — «что ему можно?». В Micronaut обе задачи решает модуль Micronaut Security. Мы начнём с самого простого способа входа — HTTP Basic: логин и пароль передаются в заголовке каждого запроса, curl умеет это ключом -u.
Подключение
Возможность генератора называется security-jwt (JWT понадобится в следующем уроке). В pom.xml добавьте зависимость io.micronaut.security:micronaut-security-jwt с областью compile и обработчик io.micronaut.security:micronaut-security-processor с версией ${micronaut.security.version} в <annotationProcessorPaths>. Точный фрагмент покажет mn feature-diff --features=security-jwt.
Сразу после подключения закрыто всё:
curl -i http://localhost:8080/articlesHTTP/1.1 401 Unauthorized
{"_links":{"self":[{"href":"/articles","templated":false}]},"_embedded":{"errors":[{"message":"Unauthorized"}]},"message":"Unauthorized"}Это безопасное поведение по умолчанию: доступ открывают явно.
Проверка логина и пароля
Micronaut не знает, где хранятся пользователи. Это сообщает бин, реализующий HttpRequestAuthenticationProvider:
// src/main/java/com/example/BlogAuthenticationProvider.java
package com.example;
import io.micronaut.core.annotation.Nullable;
import io.micronaut.http.HttpRequest;
import io.micronaut.security.authentication.AuthenticationFailureReason;
import io.micronaut.security.authentication.AuthenticationRequest;
import io.micronaut.security.authentication.AuthenticationResponse;
import io.micronaut.security.authentication.provider.HttpRequestAuthenticationProvider;
import jakarta.inject.Singleton;
import java.util.List;
import java.util.Map;
@Singleton
public class BlogAuthenticationProvider<B> implements HttpRequestAuthenticationProvider<B> {
private record User(String password, List<String> roles) {
}
private final Map<String, User> users = Map.of(
"anna", new User("changeme", List.of("ADMIN")),
"oleg", new User("changeme", List.of("AUTHOR")));
@Override
public AuthenticationResponse authenticate(@Nullable HttpRequest<B> request,
AuthenticationRequest<String, String> authRequest) {
User user = users.get(authRequest.getIdentity());
if (user != null && user.password().equals(authRequest.getSecret())) {
return AuthenticationResponse.success(authRequest.getIdentity(), user.roles());
}
return AuthenticationResponse.failure(AuthenticationFailureReason.CREDENTIALS_DO_NOT_MATCH);
}
}getIdentity() — логин, getSecret() — пароль. При успехе возвращаем имя и роли — метки прав. В учебном примере пользователи лежат в Map; в настоящем приложении их берут из базы, а пароли хранят только в виде хеша (например, BCrypt).
Правила доступа
Аннотация @Secured ставится на класс или метод:
// src/main/java/com/example/ArticleController.java
@ExecuteOn(TaskExecutors.BLOCKING)
@Controller("/articles")
@Secured(SecurityRule.IS_AUTHENTICATED)
public class ArticleController {
// поле articleService и конструктор — как раньше
@Secured(SecurityRule.IS_ANONYMOUS)
@Get
public List<Article> list(@QueryValue @Nullable String q) {
return q == null ? articleService.findAll() : articleService.search(q);
}
@Secured("ADMIN")
@Delete("/{id}")
public HttpResponse<Void> delete(@PathVariable Long id) {
articleService.delete(id);
return HttpResponse.noContent();
}
}| Значение | Кого пускает |
|---|---|
SecurityRule.IS_ANONYMOUS | Всех, даже без входа |
SecurityRule.IS_AUTHENTICATED | Любого вошедшего |
"ADMIN" | Только пользователей с этой ролью |
Аннотация на методе важнее аннотации на классе. Так читать статьи могут все, писать — вошедшие, удалять — только администратор. HelloController и AboutController пометьте @Secured(SecurityRule.IS_ANONYMOUS) целиком.
Проверка
curl -i -u oleg:changeme -X POST http://localhost:8080/articles \
-H "Content-Type: application/json" -d '{"title":"Защищено","body":"Текст"}'
curl -i -u oleg:changeme -X DELETE http://localhost:8080/articles/1
curl -i -u anna:changeme -X DELETE http://localhost:8080/articles/1Олег создаёт статью (201 Created), но удалить её не может — 403 Forbidden: он вошёл, но нужной роли нет. Анна удаляет — 204 No Content. С неверным паролем ответ 401 Unauthorized.
Узнать, кто вошёл, можно через параметр Authentication в методе контроллера:
// src/main/java/com/example/MeController.java
package com.example;
import io.micronaut.http.annotation.Controller;
import io.micronaut.http.annotation.Get;
import io.micronaut.security.annotation.Secured;
import io.micronaut.security.authentication.Authentication;
import io.micronaut.security.rules.SecurityRule;
import java.util.Map;
@Controller("/me")
@Secured(SecurityRule.IS_AUTHENTICATED)
public class MeController {
@Get
public Map<String, Object> me(Authentication authentication) {
return Map.of("name", authentication.getName(), "roles", authentication.getRoles());
}
}Запрос curl -u anna:changeme http://localhost:8080/me вернёт {"name":"anna","roles":["ADMIN"]}.
Частая ошибка
После подключения модуля перестают проходить и старые тесты: POST /articles без пароля теперь получает 401. Передайте учётные данные в тестовом запросе:
// src/test/java/com/example/ArticleControllerTest.java
HttpRequest.POST("/articles", new ArticleForm("Тестирование", "Текст"))
.basicAuth("anna", "changeme")А для проверки защиты добавьте тест, который ждёт HttpStatus.UNAUTHORIZED на запрос без basicAuth.