Пользовательская аутентификация в Spring Security

В предыдущем примере была реализована In-Memory аутентификация. Но это игрушечный случай, так как пользователи обычно хранятся не в памяти приложения, а в другом месте. В статье мы продолжим предыдущий пример: реализуем пользовательскую аутентификацию, при которой пользователи хранятся в базе. Но теоретически они могут браться отовсюду, хоть по REST из внешнего источника.

Мы сделаем два варианта аутентификации:

  • Реализуем пользовательский UserDetailsService  – то есть напишем метод loadUserByUsername(). По сути тут надо всего лишь реализовать извлечение пользователя из базы.
  • Реализуем пользовательский AuthencationProvider. Тут надо переопределить метод authencate(Authencation authencation). То есть помимо извлечения пользователя из базы, мы будем сами сравнивать пароли и формировать объект Authencation (либо выбрасывать исключение).

Подготовка

Итак, пользователи будут храниться в базе, так что добавим в проект зависимости для работы с базой.

Maven-зависимость

Во-первых, это стартер для работы с базой через JPA:

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>

Во-вторых,  In-Memory база данных H2 (она удобна для учебных примеров, поскольку не нужно ставить на компьютер реальную базу):

<dependency>
    <groupId>com.h2database</groupId>
    <artifactId>h2</artifactId>
    <scope>runtime</scope>
    <version>1.4.199</version>
</dependency>

Модель пользователя

Пусть пользователь содержит логин, пароль, роль и еще, к примеру, должность:

@Entity
public class MyUser {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    long id;
    private String login;
    private String password;
    private String position;
    private String role;

    //сеттеры/геттеры
}

Репозиторий

Пропишем в репозитории метод, который понадобится в дальнейшем:

@Repository
public interface MyUserRepository extends JpaRepository<MyUser, Long> {
    MyUser findByLogin(String login);
}

Схема и данные

Наконец, заполним базу парой пользователей с помощью data.sql (этот файл надо положить в папку resources):

insert into my_user(login, position, password, role) values('user', 'position1', 'password', 'USER');
insert into my_user( login, position, password, role) values('admin', 'position2', 'password', 'ADMIN');

Чтобы на старте приложения таблица создавалась на основе помеченным аннотацией @Entity класса MyUser, а sql-код из data.sql запускался, поставим такие настройки в application.yml:

spring:
  jpa:
    hibernate:
      ddl-auto: create
      show-sql: true
  datasource:
    initialization-mode: always

Все готово для реализации пользовательской аутентификации.

Пользовательский UserDetailsService

Итак, первый способ. Реализуем метод loadUserByUsername():

@Service
public class CustomUserDetailsService implements UserDetailsService {
    @Autowired
    private MyUserRepository dao;
    @Override
    public UserDetails loadUserByUsername(String userName) throws UsernameNotFoundException {
        MyUser myUser= dao.findByLogin(userName);
        if (myUser == null) {
            throw new UsernameNotFoundException("Unknown user: "+userName);
        }
        UserDetails user = User.builder()
                .username(myUser.getLogin())
                .password(myUser.getPassword())
                .roles(myUser.getRole())
                .build();
        return user;
    }
}

И настроим AuthenticationManager:

@EnableWebSecurity(debug = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Autowired
    private CustomUserDetailsService userDetailsService;

    ...

    @Override
    public void configure(AuthenticationManagerBuilder auth) throws Exception {
      auth.userDetailsService(userDetailsService);
    }
}

Все, теперь пример работает как предыдущий, но данные берутся из базы.

Теперь реализуем альтернативный подход.

Пользовательский AuthencationProvider

Тут делается то же самое, плюс требуется самостоятельно сравнить полученный пароль с переданным и выбросить исключение при необходимости.

В параметр Authentication уже приходят имя и пароль, взятые с запроса (в каком фильтре данные берутся из запроса, описано в статье про аутентификацию). Они приходят в поля principal и credentials объекта Authencation (который в аргументе). А сформировать и вернуть необходимо новый Authentication , где реальный пользователь лежит в Principal в виде UserDetails:

@Component
public class CustomAuthencationProvider implements AuthenticationProvider {
    @Autowired
    private MyUserRepository dao;

    @Override
    public Authentication authenticate(Authentication authentication) 
                                          throws AuthenticationException {
        String userName = authentication.getName();
        String password = authentication.getCredentials().toString();
        //получаем пользователя
        MyUser myUser = dao.findByLogin(userName);
        //смотрим, найден ли пользователь в базе

        if (myUser == null) {
            throw new BadCredentialsException("Unknown user "+userName);
        }
        if (!password.equals(myUser.getPassword())) {
            throw new BadCredentialsException("Bad password");
        }
        UserDetails principal = User.builder()
                .username(myUser.getLogin())
                .password(myUser.getPassword())
                .roles(myUser.getRole())
                .build();
        return new UsernamePasswordAuthenticationToken(
                principal, password, principal.getAuthorities());

    }

    @Override
    public boolean supports(Class<?> authentication) {
        return authentication.equals(UsernamePasswordAuthenticationToken.class);
    }
}

Настройка аутентификации теперь отличается одной строчкой:

@EnableWebSecurity(debug = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    
    //теперь внедряем свой провайдер   
    @Autowired
    private CustomAuthencationProvider customAuthencationProvider;


    @Override
    public void configure(AuthenticationManagerBuilder auth) throws Exception {
       //и добавляем его сюда
       auth.authenticationProvider(customAuthencationProvider);
    }
    ...
}

Итоги

Пример есть на GitHub.

Дальше рассмотрим jdbc-аутентификацию.

 

Пользовательская аутентификация в Spring Security: 7 комментариев

  1. Спасибо!! побольше выпускай подобные статьи и не только на эту тему) обязательно буду следить за новыми статьями…

  2. Приветствую. Мне нравится, интересно. Вопрос.
    У Вас class MyUser содержит поле private String role; -> в классе class CustomAuthencationProvider в методе .roles(myUser.getRole()), параметры String.
    А как быть если у меня пользователь концептуально заложен так: private Set roles; и соответствующий метод не возвращает Srting.
    Как переписать аудентификацию с таким условием?

    1. .roles() принимает также массив ролей, так что можно сделать так: .roles(roles.toArray(String[]::new))

  3. Здравствуйте.
    У меня получилось выполнить Ajax форму входа из примера: https://github.com/hardselius/spring-security-ajax-login/blob/master/src/main/java/no/buypass/bptestutil/components/controllers/AjaxLoginController.java

    Вопрос к Вам, а как перехватить сообщение об ошибке выбрасываемое в вашем коде
    if (myUser == null) {
    throw new BadCredentialsException(“Unknown user “+userName);
    в контроллере из примера по ссылке.
    Ваш пример я примени и все получилось, но вот сообщения не могу перехватить.

    1. AuthenticationFailureHandler- переопределение данного фильтра позволяет добавить в ответ об ошибке jason сообщения

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *