Правила доступа к API
Пользователи есть, теперь нужны правила: кто что может делать. Читать статьи должен любой, писать — только вошедший, удалять — только администратор. Правила задаются в бине SecurityFilterChain: это цепочка фильтров, через которую проходит каждый запрос.
Цепочка для API
// src/main/java/com/example/blog/SecurityConfig.java
@Bean
@Order(1)
public SecurityFilterChain apiSecurity(HttpSecurity http) throws Exception {
http
.securityMatcher("/api/**")
.authorizeHttpRequests(auth -> auth
.requestMatchers(HttpMethod.GET, "/api/posts/**").permitAll()
.requestMatchers(HttpMethod.POST, "/api/users").permitAll()
.requestMatchers(HttpMethod.DELETE, "/api/posts/**").hasRole("ADMIN")
.anyRequest().authenticated())
.httpBasic(Customizer.withDefaults())
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.csrf(csrf -> csrf.disable());
return http.build();
}| Строка | Смысл |
|---|---|
securityMatcher("/api/**") | Цепочка отвечает только за адреса API |
requestMatchers(GET, "/api/posts/**").permitAll() | Читать статьи и комментарии можно без входа |
hasRole("ADMIN") | Удалять статьи может только роль ADMIN |
anyRequest().authenticated() | Всё остальное — после входа |
httpBasic(...) | Вход по имени и паролю в каждом запросе |
STATELESS | Сервер не заводит сессию: клиент API присылает пароль каждый раз |
csrf.disable() | CSRF-атака опасна для cookie браузера, а API cookie не использует |
Правила проверяются сверху вниз, и срабатывает первое подходящее. @Order(1) пригодится в следующем уроке, когда появится вторая цепочка — для страниц сайта.
Кто сделал запрос
Автором статьи теперь будет вошедший пользователь, а не число из запроса. Из NewPostRequest поле authorId удаляем, а контроллер узнаёт пользователя через @AuthenticationPrincipal:
// src/main/java/com/example/blog/PostController.java
@PostMapping
public ResponseEntity<PostResponse> create(@Valid @RequestBody NewPostRequest request,
@AuthenticationPrincipal UserDetails user) {
PostResponse post = postService.create(request, user.getUsername());
return ResponseEntity.created(URI.create("/api/posts/" + post.id())).body(post);
}// src/main/java/com/example/blog/PostService.java
@Transactional
public PostResponse create(NewPostRequest request, String username) {
User author = users.findByUsername(username)
.orElseThrow(() -> new IllegalArgumentException("Неизвестный автор " + username));
Post post = new Post(request.title(), request.body(), author, Instant.now(clock));
return PostResponse.from(posts.save(post));
}UserDetails — тот самый объект, который вернул BlogUserDetailsService при входе. Комментарии меняются так же: NewCommentRequest остаётся с одним полем text.
Проверка
curl -i -X POST http://localhost:8080/api/posts -H "Content-Type: application/json" \
-d '{"title":"Привет","body":"x"}'
curl -X POST -u anna:changeme http://localhost:8080/api/posts -H "Content-Type: application/json" \
-d '{"title":"Привет, Spring Security","body":"У статей теперь настоящий автор"}'
curl -i -u anna:changeme -X DELETE http://localhost:8080/api/posts/1HTTP/1.1 401
{"id":1,"title":"Привет, Spring Security","body":"У статей теперь настоящий автор","author":"anna","createdAt":"2026-10-04T09:04:38.954220400Z"}
HTTP/1.1 403Гость получил 401, Анна создала статью, но удалить её не может: 403 — вход выполнен, а роли ADMIN нет. Сделаем Анну администратором прямо в базе:
update users set role = 'ADMIN' where username = 'anna';После этого то же удаление отвечает 204. Роль читается при каждом входе, поэтому перезапуск не нужен.
Частая ошибка
Правило anyRequest() должно быть последним. Если поставить после него другие, приложение не запустится:
Can't configure requestMatchers after anyRequestПохожая ловушка — hasRole("ROLE_ADMIN"). Приставку ROLE_ Spring добавляет сам, поэтому получите ошибку ROLE_ADMIN should not start with ROLE_ since ROLE_ is automatically prepended. Пишите просто hasRole("ADMIN").