Encodera

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

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

Оглавление

Пользователи и пароли

Пользователь user со случайным паролем нам не подходит: входить должны Анна, Олег и все, кто зарегистрировался. Их данные уже лежат в таблице users. Нужно добавить пароль и роль, научиться безопасно хранить пароль и объяснить Spring Security, где искать пользователя при входе.

Пароль никогда не хранят открытым

В базу записывают не сам пароль, а его хеш — результат необратимого преобразования. При входе введённый пароль хешируют снова и сравнивают. Если базу украдут, паролей в ней не будет. Хешированием занимается PasswordEncoder:

// src/main/java/com/example/blog/SecurityConfig.java
@Configuration
public class SecurityConfig {
 
    @Bean
    public PasswordEncoder passwordEncoder() {
        return PasswordEncoderFactories.createDelegatingPasswordEncoder();
    }
}

Этот кодировщик использует алгоритм bcrypt и дописывает его имя в начало хеша: {bcrypt}$2a$10$.... Если когда-нибудь алгоритм сменится, старые пароли всё равно будут проверяться.

Пароль и роль в сущности

Столбцы добавляет миграция V2__add_password_and_role.sql из урока о Flyway. В User появляются поля:

// src/main/java/com/example/blog/User.java
@Column(nullable = false, length = 100)
private String password;
 
@Column(nullable = false, length = 20)
private String role = "USER";
 
public User(String username, String password) {
    this.username = username;
    this.password = password;
}
 
// и геттеры getPassword(), getRole()

Регистрация

// src/main/java/com/example/blog/NewUserRequest.java
public record NewUserRequest(
        @NotBlank @Pattern(regexp = "[a-z0-9_]{3,20}") String username,
        @NotBlank @Size(min = 8, max = 72) String password) {
}
// src/main/java/com/example/blog/UserController.java
@PostMapping
@ResponseStatus(HttpStatus.CREATED)
public UserResponse register(@Valid @RequestBody NewUserRequest request) {
    if (users.existsByUsername(request.username())) {
        throw new IllegalArgumentException("Имя пользователя занято: " + request.username());
    }
    String hash = passwordEncoder.encode(request.password());
    return UserResponse.from(users.save(new User(request.username(), hash)));
}

PasswordEncoder контроллер получает в конструкторе, как любой бин. Ограничение в 72 символа — особенность bcrypt.

Откуда Spring Security берёт пользователей

// src/main/java/com/example/blog/BlogUserDetailsService.java
@Service
public class BlogUserDetailsService implements UserDetailsService {
 
    private final UserRepository users;
 
    public BlogUserDetailsService(UserRepository users) {
        this.users = users;
    }
 
    @Override
    public UserDetails loadUserByUsername(String username) {
        User user = users.findByUsername(username)
                .orElseThrow(() -> new UsernameNotFoundException(username));
 
        return org.springframework.security.core.userdetails.User
                .withUsername(user.getUsername())
                .password(user.getPassword())
                .roles(user.getRole())
                .build();
    }
}

При входе Spring Security вызывает loadUserByUsername и сравнивает пароли через PasswordEncoder. Класс User из Spring Security пишется с полным именем пакета, чтобы не спутать его с нашей сущностью. Как только такой бин появился, временный пароль из журнала исчезает.

Регистрация открыта для всех

Чтобы зарегистрироваться можно было без входа, добавим в SecurityConfig первые правила для API:

// src/main/java/com/example/blog/SecurityConfig.java
@Bean
public SecurityFilterChain apiSecurity(HttpSecurity http) throws Exception {
    http
            .securityMatcher("/api/**")
            .authorizeHttpRequests(auth -> auth
                    .requestMatchers(HttpMethod.POST, "/api/users").permitAll()
                    .anyRequest().authenticated())
            .httpBasic(Customizer.withDefaults())
            .csrf(csrf -> csrf.disable());
    return http.build();
}

Подробно эти строки разберём в следующем уроке. Проверим:

curl -X POST http://localhost:8080/api/users -H "Content-Type: application/json" \
  -d '{"username":"anna","password":"changeme"}'
curl -X POST http://localhost:8080/api/users -H "Content-Type: application/json" \
  -d '{"username":"ivan","password":"123"}'
{"id":1,"username":"anna"}
{"detail":"Ошибка проверки данных","instance":"/api/users","status":400,"title":"Bad Request","errors":{"password":"размер должен находиться в диапазоне от 8 до 72"}}

В таблице users пароль Анны выглядит как {bcrypt}$2a$10$... — сам changeme нигде не хранится. Теперь curl -u anna:changeme http://localhost:8080/api/posts пускает Анну.

Частая ошибка

Если записать пароль в базу без кодировщика, например вручную через SQL, вход упадёт с ошибкой 500, а в журнале будет:

java.lang.IllegalArgumentException: Given that there is no default password encoder configured, each password must have a password encoding prefix. Please either prefix this password with '{noop}' or set a default password encoder in `DelegatingPasswordEncoder`.

Кодировщик не знает, чем зашифрован пароль без приставки в фигурных скобках. Создавайте пользователей только через passwordEncoder.encode(...).

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

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