Encodera

Спасибо, что читаете!

Пожертвуйте сайту, мы стараемся ради вас.

Оглавление

Основы безопасности

Сейчас любой может создать или удалить статью. Аутентификация отвечает на вопрос «кто это?», авторизация — «что ему можно?». В 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/articles
HTTP/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.

Спасибо, что читаете!

Пожертвуйте сайту, мы стараемся ради вас.