Пользователи и пароли
Пользователь 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(...).