Encodera

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

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

Оглавление

Правила доступа к 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/1
HTTP/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").

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

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