Пользователь Диана Машкина
Диана Машкина
8 уровень

Дружим обычный вход через email и OAuth2 в Spring Security на примере сервиса заметок

Статья из группы Random
Во время написания своего приложения столкнулась с отсутствием внятных статей, как сделать так, чтобы пользователь регистрировался и через email, и через социальные сети. Были хорошие туториалы по настройке классической формы входа. Были хорошие туториалы по OAuth2. Как подружить два способа, информации было преступно мало. В процессе поисков удалось вывести работоспособное решение. Оно не претендует на истину в последней инстанции, однако свою функцию выполняет. В этой статье я покажу, как с нуля реализовать сервис для хранения заметок с подобной конфигурацией Spring Security. Дружим обычный вход через email и OAuth2 в Spring Security на примере сервиса заметок - 1Примечание: хорошо, если читатель прошел хотя бы пару туториалов по Spring, потому что внимание будет акцентироваться только на Spring Security, без детальных объяснений репозиториев, контроллеров и т. п. Иначе и так немаленькая статья получилась бы гигантской. Содержание
  1. Создание проекта
  2. Создание сущностей и логики приложения
    1. Сущности
    2. Репозитории
    3. Контроллеры
    4. Страницы
  3. Настройка Spring Security для классического входа
    1. Основная конфигурация SecurityConfig
    2. Кастомный вход пользователя
    3. Усовершенствуем контроллер
    4. Запуск
  4. Настройка OAuth2 на примере Google в Spring Security
    1. Конфигурация фильтра и application.properties
    2. Основные моменты регистрации приложения в Google Cloud Platform
    3. CustomUserInfoTokenServices
  5. Итоговый запуск проекта

Создание проекта

Идем на start.spring.io и формируем основу проекта:
  • Web — запуск приложения на встроенном Tomcat, url-сопоставления и тому подобное;
  • JPA — связь с базой данных;
  • Mustache — шаблонизатор, используется для генерации веб-страниц;
  • Security — защита приложения. То, ради чего эта статья и создавалась.
Скачиваем получившийся архив и распаковываем в нужной вам папке. Запускаем его в IDE. Вы можете выбирать БД на свое усмотрение. В качестве базы данных для проекта я использую MySQL, поэтому в файл pom.xml в блок <dependencies> добавляю следующую зависимость:

<dependency>
     <groupId>mysql</groupId>
     <artifactId>mysql-connector-java</artifactId>
     <version>5.1.34</version>
</dependency>
Конфигурация application.properties на данный момент следующая:

spring.datasource.url=jdbc:mysql://localhost:3306/springsectut?createDatabaseIfNotExist=true&useSSL=false&autoReconnect=true&useLegacyDatetimeCode=false&serverTimezone=UTC&useUnicode=yes&characterEncoding=UTF-8
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.username=yourUsername
spring.datasource.password=yourPassword

spring.jpa.generate-ddl=true
spring.jpa.hibernate.ddl-auto=update

spring.jpa.properties.connection.characterEncoding=utf-8
spring.jpa.properties.connection.CharSet=utf-8
spring.jpa.properties.connection.useUnicode=true

spring.mustache.expose-request-attributes=true

Создание сущностей и логики приложения

Сущности

Создадим пакет entities, в который поместим сущности базы данных. Пользователь будет описываться классом User, реализующим интерфейс UserDetails, что понадобится для конфигурации Spring Security. У пользователя будет id, username (в качестве него выступает email), пароль, имя, роль, флаг активности, имя и email аккаунта Google (googleName и googleUsername).

@Entity
@Table(name = "user")
public class User implements UserDetails
{
  @Id
  @GeneratedValue(strategy = GenerationType.AUTO)
  private Long id;

  private String username;
  private String password;
  private String name;
  private boolean active;
  private String googleName;
  private String googleUsername;

  @ElementCollection(targetClass = Role.class, fetch = FetchType.EAGER)
  @CollectionTable(name = "user_role", joinColumns = @JoinColumn(name = "user_id"))
  @Enumerated(EnumType.STRING)
  private Set<Role> roles;

    //Геттеры, сеттеры, toString(), equals(), hashcode(), имплементация UserDetails
}
Роли пользователя используются для регуляции доступа в Spring Security. Наше приложение будет использовать только одну роль:

public enum Role implements GrantedAuthority
{
  USER;

  @Override
  public String getAuthority()
  {
     return name();
  }
}
Создадим класс заметки с id, заголовком заметки, телом заметки и id пользователя, которому она принадлежит:

@Entity
@Table(name = "note")
public class Note
{
  @Id
  @GeneratedValue(strategy = GenerationType.AUTO)
  private Long id;

  private String title;
  private String note;
  private Long userId;

    //Геттеры, сеттеры, toString(), equals(), hashcode()
}

Репозитории

Для сохранения сущностей в базу данных нам понадобятся репозитории, которые сделают всю грязную работу за нас. Создадим пакет repos, в нем создадим интерфейсы UserRepo, NoteRepo, унаследованные от интерфейса JpaRepository<Entity, Id>.

@Service
@Repository
public interface UserRepo extends JpaRepository<User, Long>
{}

@Service
@Repository
public interface NoteRepo extends JpaRepository<Note, Long>
{
  List<Note> findByUserId(Long userId);
}

Контроллеры

В нашем сервисе заметок будут следующие страницы:
  • Главная;
  • Регистрация;
  • Вход;
  • Список заметок пользователя.
К списку заметок должен быть доступ только у авторизованного пользователя. Остальные страницы являются общедоступными. Создадим пакет controllers, в нем класс IndexController, содержащий обычный get-mapping главной страницы. Класс RegistrationController отвечает за регистрацию пользователя. Post-mapping принимает данные из формы, сохраняет пользователя в базу данных и переадресовывает на страницу входа. PasswordEncoder будет описан позднее. Он используется для шифрования паролей.

@Controller
public class RegistrationController
{
  @Autowired
  private UserRepo userRepo;

  @Autowired
  private PasswordEncoder passwordEncoder;

  @GetMapping("/registration")
  public String registration()
  {
     return "registration";
  }

  @PostMapping("/registration")
  public String addUser(String name, String username, String password)
  {
     User user = new User();
     user.setName(name);
     user.setUsername(username);
     user.setPassword(passwordEncoder.encode(password));
     user.setActive(true);
     user.setRoles(Collections.singleton(Role.USER));

     userRepo.save(user);

     return "redirect:/login";
  }
Контроллер, отвечающий за страницу списка заметок, пока что содержит упрощенный функционал, который усложнится после внедрения Spring Security.

@Controller
public class NoteController
{
  @Autowired
  private NoteRepo noteRepo;
 
  @GetMapping("/notes")
  public String notes(Model model)
  {
     List<Note> notes = noteRepo.findAll();
     model.addAttribute("notes", notes);
    
     return "notes";
  }
 
  @PostMapping("/addnote")
  public String addNote(String title, String note)
  {
     Note newNote = new Note();
     newNote.setTitle(title);
     newNote.setNote(note);
    
     noteRepo.save(newNote);
    
     return "redirect:/notes";
  }
}
Для страницы входа мы не будем прописывать контроллер, потому что она задействована в Spring Security. Вместо этого нам понадобится специальная конфигурация. Привычно создадим очередной пакет, назовем его config, поместим туда класс MvcConfig. Когда мы напишем конфигурацию Spring Security, она будет знать, какую страницу мы имеем в виду при использовании "/login".

@Configuration
public class MvcConfig implements WebMvcConfigurer
{
  public void addViewControllers(ViewControllerRegistry registry)
  {
     registry.addViewController("/login").setViewName("login");
     registry.setOrder(Ordered.HIGHEST_PRECEDENCE);
  }
}

Страницы

Для создания страниц я использую шаблонизатор Mustache. Вы можете внедрить и другой, не принципиально. Для мета-информации, которая используется на всех страницах, создан файл meta.mustache. В нем же подключен Bootstrap, чтобы страницы нашего проекта выглядели симпатичней. Страницы создаются в директории "src/main/resources/templates". Файлы имеют расширение mustache. Размещение html-кода непосредственно в статье сделает ее слишком большой, поэтому вот ссылка на папку с шаблонами в репозитории проекта на GitHub.

Настройка Spring Security для классического входа

Spring Security помогает нам защищать приложение и его ресурсы от несанкционированного доступа. Мы создадим лаконичную рабочую конфигурацию в классе SecurityConfig, унаследованный от WebSecurityConfigurerAdapter, который поместим в пакет config. Пометим его аннотацией @EnableWebSecurity, которая включит поддержку Spring Security, и аннотацией @Configuration, которая указывает, что этот класс содержит некую конфигурацию. Примечание: в автоматически сконфигурированном pom.xml стояла версия родительского компонента Spring Boot 2.1.4.RELEASE, что мешало внедрить Security устоявшимся способом. Во избежание конфликтов в проекте рекомендуется изменить версию на 2.0.1.RELEASE.

Основная конфигурация SecurityConfig

Наша конфигурация будет уметь:
  1. Шифровать пароли с помощью BCryptPasswordEncoder:

    
    @Autowired
    private PasswordEncoder passwordEncoder;
    
    @Bean
    PasswordEncoder passwordEncoder()
    {
      PasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
      return passwordEncoder;
    }
    
  2. Осуществлять вход по специально написанному провайдеру аутентификации:

    
    @Autowired
    private AuthProvider authProvider;
    
    @Override
    protected void configure(AuthenticationManagerBuilder auth)
    {
      auth.authenticationProvider(authProvider);
    }
    
  3. Разрешать доступ анонимным пользователям к главной странице, страницам регистрации и входа. Все остальные запросы должны выполняться вошедшими в систему пользователями. Страницей входа назначим ранее описанную "/login". При успешном входе пользователь попадет на страницу со списком заметок, при ошибке — останется на странице входа. При успешном выходе пользователь попадет на главную страницу.

    
    @Override
    protected void configure(HttpSecurity http) throws Exception
    {
      http
            .authorizeRequests()
            .antMatchers("/resources/**", "/", "/login**", "/registration").permitAll()
            .anyRequest().authenticated()
            .and().formLogin().loginPage("/login")       
            .defaultSuccessUrl("/notes").failureUrl("/login?error").permitAll()
            .and().logout().logoutSuccessUrl("/").permitAll();
    }
    

Кастомный вход пользователя

Самостоятельно написанный AuthProvider позволит пользователю входить не только по email, но и по имени пользователя.

@Component
public class AuthProvider implements AuthenticationProvider
{
  @Autowired
  private UserService userService;

  @Autowired
  private PasswordEncoder passwordEncoder;

  public Authentication authenticate(Authentication authentication) throws AuthenticationException
  {
     String username = authentication.getName();
     String password = (String) authentication.getCredentials();

     User user = (User) userService.loadUserByUsername(username);

     if(user != null && (user.getUsername().equals(username) || user.getName().equals(username)))
     {
        if(!passwordEncoder.matches(password, user.getPassword()))
        {
           throw new BadCredentialsException("Wrong password");
        }

        Collection<? extends GrantedAuthority> authorities = user.getAuthorities();

        return new UsernamePasswordAuthenticationToken(user, password, authorities);
     }
     else
        throw new BadCredentialsException("Username not found");
  }

  public boolean supports(Class<?> arg)
  {
     return true;
  }
}
Как вы могли заметить, за подгрузку пользователя отвечает класс UserService, лежащий в пакете services. В нашем случае он ищет пользователя не только по полю username, как встроенная реализация, но еще и по имени пользователя, имени Google аккаунта и email Google аккаунта. Последние два способа пригодятся нам при реализации входа через OAuth2. Здесь класс приведен в сокращенном варианте.

@Service
public class UserService implements UserDetailsService
{
  @Autowired
  private UserRepo userRepo;

  @Override
  public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException
  {
     User userFindByUsername = userRepo.findByUsername(username);
     //Остальные поиски

     if(userFindByUsername != null)
     {
        return userFindByUsername;
     }
     //Остальные проверки
     return null;
  }
}
Примечание: не забудьте прописать нужные методы в UserRepo!

Усовершенствуем контроллер

Мы настроили Spring Security. Самое время воспользоваться этим в контроллере заметок. Теперь каждый mapping будет принимать дополнительный параметр Principal, по которому постарается найти пользователя. Почему нельзя напрямую внедрить класс User? Тогда произойдет конфликт из-за несовпадения типов пользователей, когда мы напишем вход через социальные сети. Мы заранее обеспечиваем необходимую гибкость. Теперь наш код контроллера заметок выглядит так:

@GetMapping("/notes")
public String notes(Principal principal, Model model)
{
  User user = (User) userService.loadUserByUsername(principal.getName());
  List<Note> notes = noteRepo.findByUserId(user.getId());
  model.addAttribute("notes", notes);
  model.addAttribute("user", user);

  return "notes";
}

@PostMapping("/addnote")
public String addNote(Principal principal, String title, String note)
{
  User user = (User) userService.loadUserByUsername(principal.getName());

  Note newNote = new Note();
  newNote.setTitle(title);
  newNote.setNote(note);
  newNote.setUserId(user.getId());

  noteRepo.save(newNote);

  return "redirect:/notes";
}
Примечание: в проекте по умолчанию включена защита CSRF, поэтому либо отключите ее для себя (http.csrf().disable()), либо не забывайте, как автор статьи, добавлять во все post-запросы скрытое поле с csrf-токеном.

Запуск

Пробуем запустить проект.
Дружим обычный вход через email и OAuth2 в Spring Security на примере сервиса заметок - 1
Дружим обычный вход через email и OAuth2 в Spring Security на примере сервиса заметок - 2
Видим, что новый пользователь появился в базе данных. Пароль зашифрован.
Дружим обычный вход через email и OAuth2 в Spring Security на примере сервиса заметок - 3
Дружим обычный вход через email и OAuth2 в Spring Security на примере сервиса заметок - 4
Дружим обычный вход через email и OAuth2 в Spring Security на примере сервиса заметок - 5
Дружим обычный вход через email и OAuth2 в Spring Security на примере сервиса заметок - 6
Заметки сохранились в базу данных.
Дружим обычный вход через email и OAuth2 в Spring Security на примере сервиса заметок - 7
Видим, что проект успешно запускается и работает. Для полного счастья нам не хватает только возможности входа через социальные сети. Что ж, приступим!

Настройка OAuth2 на примере Google в Spring Security

При внедрении OAuth2 я опиралась на этот официальный туториал от Spring. Для поддержки OAuth2 добавим в pom.xml следующую библиотеку:

<dependency>
  <groupId>org.springframework.security.oauth.boot</groupId>
  <artifactId>spring-security-oauth2-autoconfigure</artifactId>
  <version>2.0.0.RELEASE</version>
</dependency>
Модифицируем нашу конфигурацию Spring Security в классе SecurityConfig. Для начала добавим аннотацию @EnableOAuth2Client. Она автоматически подтянет нужное для логина через соцсети.

Конфигурация фильтра и application.properties

Внедрим инъекцию OAuth2ClientContext, чтобы использовать ее в нашей конфигурации безопасности.

@Autowired
private OAuth2ClientContext oAuth2ClientContext;
OAuth2ClientContext используется при создании фильтра, который проверяет пользовательский запрос на вход через соцсеть. Фильтр доступен благодаря аннотации @EnableOAuth2Client. Все, что нам нужно, вызвать его в правильном порядке, до основного фильтра Spring Security. Только в таком случае мы сможем поймать перенаправления в процессе входа с OAuth2. Для этого используем FilterRegistrationBean, в котором выставляем приоритет нашего фильтра на -100.

@Bean
public FilterRegistrationBean oAuth2ClientFilterRegistration(OAuth2ClientContextFilter oAuth2ClientContextFilter)
{
  FilterRegistrationBean registration = new FilterRegistrationBean();
  registration.setFilter(oAuth2ClientContextFilter);
  registration.setOrder(-100);
  return registration;
}

private Filter ssoFilter()
{
  OAuth2ClientAuthenticationProcessingFilter googleFilter = new OAuth2ClientAuthenticationProcessingFilter("/login/google");
  OAuth2RestTemplate googleTemplate = new OAuth2RestTemplate(google(), oAuth2ClientContext);
  googleFilter.setRestTemplate(googleTemplate);
  CustomUserInfoTokenServices tokenServices = new CustomUserInfoTokenServices(googleResource().getUserInfoUri(), google().getClientId());
  tokenServices.setRestTemplate(googleTemplate);
  googleFilter.setTokenServices(tokenServices);
  tokenServices.setUserRepo(userRepo);
  tokenServices.setPasswordEncoder(passwordEncoder);
  return googleFilter;
}
Также необходимо добавить новый фильтр в функцию configure(HttpSecurity http):

http.addFilterBefore(ssoFilter(), UsernamePasswordAuthenticationFilter.class);
Фильтру также необходимо знать о регистрации клиента через Google. Аннотация @ConfigurationProperties указывает, на какие свойства конфигурации следует обратить внимание в application.properties.

@Bean
@ConfigurationProperties("google.client")
public AuthorizationCodeResourceDetails google()
{
  return new AuthorizationCodeResourceDetails();
}
Чтобы завершить аутентификацию, нужно указать конечную точку пользовательской информации Google:

@Bean
@ConfigurationProperties("google.resource")
public ResourceServerProperties googleResource()
{
  return new ResourceServerProperties();
}
Зарегистрировав наше приложение в Google Cloud Platform, в application.properties добавим свойства с соответствующими префиксами:

google.client.clientId=yourClientId
google.client.clientSecret=yourClientSecret
google.client.accessTokenUri=https://www.googleapis.com/oauth2/v4/token
google.client.userAuthorizationUri=https://accounts.google.com/o/oauth2/v2/auth
google.client.clientAuthenticationScheme=form
google.client.scope=openid,email,profile
google.resource.userInfoUri=https://www.googleapis.com/oauth2/v3/userinfo
google.resource.preferTokenInfo=true

Основные моменты регистрации приложения в Google Cloud Platform

Путь: API и сервисы -> Учетные данные Окно запроса доступа OAuth:
  • Название приложения: Spring login form and OAuth2 tutorial
  • Адрес электронной почты службы поддержки: ваш email
  • Область действия для API Google: email, profile, openid
  • Авторизованные домены: me.org
  • Ссылка на главную страницу приложения: http://me.org:8080
  • Ссылка на политику конфиденциальности приложения: http://me.org:8080
  • Ссылка на условия использования приложения: http://me.org:8080
Учетные данные:
  • Тип: Веб-приложение
  • Название: Spring login form and OAuth2 tutorial
  • Разрешенные источники JavaScript: http://me.org, http://me.org:8080
  • Разрешенные URI перенаправления: http://me.org:8080/login, http://me.org:8080/login/google
Примечание: так как с адресом localhost:8080 Google работать не хочет, внесите в файл C:\Windows\System32\drivers\etc\hosts в конце строчку “127.0.0.1 me.org” или подобную ей. Главное, чтобы домен был в классическом виде.

CustomUserInfoTokenServices

Вы заметили слово Custom в функции-описании фильтра? Класс CustomUserInfoTokenServices. Да, мы создадим свой класс с блэкджеком и возможностью сохранить пользователя в БД! С помощью сочетания клавиш Ctrl-N в IntelliJ IDEA можно найти и посмотреть, как реализован UserInfoTokenServices, используемый по умолчанию. Скопируем его код в свежесозданный класс CustomUserInfoTokenServices. Большую часть можно оставить без изменений. Прежде, чем изменять логику функций, допишем в качестве приватных полей класса UserRepo и PasswordEncoder. Создадим для них сеттеры. Внесем в класс SecurityConfig @Autowired UserRepo userRepo. Смотрим, как исчезает указатель на ошибку в методе создания фильтра, радуемся. Почему нельзя было применить @Autowired непосредственно в CustomUserInfoTokenServices? Потому что этот класс не подтянет зависимость, так как сам не помечен какой-либо аннотацией Spring, к тому же его конструктор создается явно при объявлении фильтра. Соответственно, механизм DI Spring о нем не знает. Если мы аннотируем @Autowired что-либо в этом классе, то при использовании получим NullPointerException. А вот через явные сеттеры все очень даже работает. После внедрения нужных компонентов главным объектом интереса становится функция loadAuthentication, в которой извлекается Map<String, Object> с информацией о пользователе. Именно в ней в своем и этом проекте я реализовала сохранение в базу данных вошедшего через соцсеть пользователя. Так как в качестве провайдера OAuth2 мы используем Google аккаунт, то проверяем, содержит ли map характерное для Google поле “sub”. Если оно присутствует, значит, информация о пользователе дошла верно. Создаем нового пользователя и сохраняем его в базу данных.

@Override
public OAuth2Authentication loadAuthentication(String accessToken)
     throws AuthenticationException, InvalidTokenException
{
  Map<String, Object> map = getMap(this.userInfoEndpointUrl, accessToken);

  if(map.containsKey("sub"))
  {
     String googleName = (String) map.get("name");
     String googleUsername = (String) map.get("email");

     User user = userRepo.findByGoogleUsername(googleUsername);

     if(user == null)
     {
        user = new User();
        user.setActive(true);
        user.setRoles(Collections.singleton(Role.USER));
     }

     user.setName(googleName);
     user.setUsername(googleUsername);
     user.setGoogleName(googleName);
     user.setGoogleUsername(googleUsername);
     user.setPassword(passwordEncoder.encode("oauth2user"));

     userRepo.save(user);
  }

  if (map.containsKey("error"))
  {
     this.logger.debug("userinfo returned error: " + map.get("error"));
     throw new InvalidTokenException(accessToken);
  }
  return extractAuthentication(map);
}
При использовании нескольких провайдеров можно и указывать разные варианты в одном CustomUserInfoTokenServices, и прописывать разные классы подобных сервисов в методе объявления фильтра. Теперь в качестве Principal у нас может выступать и User, и OAuth2Authentication. Так как мы заблаговременно учли в UserService подгрузку пользователя через данные Google, приложение будет работать для обоих видов пользователей. Модифицируем контроллер главной страницы проекта, чтобы он перенаправлял вошедших с помощью OAuth2 пользователей на страницу заметок.

@GetMapping("/")
public String index(Principal principal)
{
  if(principal != null)
  {
     return "redirect:/notes";
  }
  return "index";
}

Итоговый запуск проекта

После небольших косметических изменений и добавления кнопки выхода, проводим итоговый запуск проекта.
Дружим обычный вход через email и OAuth2 в Spring Security на примере сервиса заметок - 8
Дружим обычный вход через email и OAuth2 в Spring Security на примере сервиса заметок - 9
Дружим обычный вход через email и OAuth2 в Spring Security на примере сервиса заметок - 10
Дружим обычный вход через email и OAuth2 в Spring Security на примере сервиса заметок - 11
Дружим обычный вход через email и OAuth2 в Spring Security на примере сервиса заметок - 12
Дружим обычный вход через email и OAuth2 в Spring Security на примере сервиса заметок - 13
Дружим обычный вход через email и OAuth2 в Spring Security на примере сервиса заметок - 14
Дружим обычный вход через email и OAuth2 в Spring Security на примере сервиса заметок - 15
Пользователь успешно входит и через обычную форму, и через Google аккаунт. Этого мы и добивались! Надеюсь, эта статья прояснила определенные моменты в создании веб-приложения, его защите с помощью Spring Security и комбинировании разных способов входа. C полным кодом проекта вы можете
Комментарии (27)
Чтобы просмотреть все комментарии или оставить свой,
перейдите в полную версию
satird 27 уровень, Минск
24 декабря 2020
Хорошая статья. Все заработало. Жаль что уже многие методы и аннотации помечены deprecated. Уж очень быстро все устаревает. Хотелось бы что бы кто-нибудь мог поделиться актуальным на данный момент способом авторизации. Автор молодец)
Akira Rokudo 20 уровень, Москва
10 мая 2020
Хорошая статья, хотя видна костыльность - можно проанотировать кастомный сервис как сервис и не возиться с сэттерами. Т.к. в моем случае потребовалось редиректить после аутентификации , то также в фильтр добавил хендлер для успешной аутентификации - иначе он просто возвращал на страницу логина
Никита 20 уровень, Москва
20 декабря 2019
Здравствуйте. Туториал очень хороший, разобрался со всем, кроме одного момента: спринг в упор не хочет видеть страницу логина, и этот метод в файле MvcConfig.java помечен как не использующийся. Любые попытки перейти на /login заканчиваются 404 и выводом: 2019-12-20 21:58:11.076 WARN 15516 --- [nio-8090-exec-4] o.s.web.servlet.PageNotFound: No mapping for GET /login Есть ли способ это пофиксить? Я убирал в SecurityConfig параметр loginPage() для корректной работы страницы, но с этим далеко не уйти, так как мне нужен функционал всего сайта, а не только возможность ввода логина/мыла и пароля :)

@Configuration
public class MvcConfig implements WebMvcConfigurer {

    public void addViewContollers(ViewControllerRegistry registry) {
        registry.addViewController("/login").setViewName("login");
        registry.addViewController("/logout").setViewName("logout");
        registry.setOrder(Ordered.HIGHEST_PRECEDENCE);
    }
}
Eugene Skiba 1 уровень
10 декабря 2019
Здравствуйте. спасибо за туториал, очень понравился, особенно то что он нерабочий.
Ayta 17 уровень, Москва
5 мая 2019
а как реализовывается logout?
Ярослав 40 уровень, Днепр Master
3 мая 2019
Ещё насчёт пропуска сервисного слоя: нежелательно так делать еще по той причине, что именно на уровне сервисов должны управляться транзакции, а так транзакции в репозитории работают по auto-commit=true считай (на самом деле флажок в JDBC не стоит, но реализация репов спринговских как раз такая), ведь каждый метод репозитория - отдельная транзакция (репы помечены по-умолчанию @Transactional аннотациями, класс JpaRepository так же обладает @Transactional(readonly=true) аннотацией, которая наследуется, потому объявленные методы в наших репах, унаследованных от JpaRepository, по чтению работают, ведь неявно помечены @Transactional, однако если попробовать объявить метод удаления или изменения, а потом воспользоваться им, у нас выбьет ошибка, что транзакция не открыта. Так же если мы рассчитывали изменить поля вычитанной сущности и чтобы они изменились в БД, как и работает ORM, нас ждет разочарование, ведь после чтения, транзакция уже закоммичена, а значит сущность перешла в состояние Detached, и изменения сущности не отправятся в БД. Потому транзакциями нужно управлять вручную. Слишком много капканов, на которые можно встать. Ситуация ещё сильнее усугубляется, когда есть сущность со связями с другими сущностями, с LAZY связями. Если мы вычитали эту сущность через репо. а потом попробовали получить доступ к LAZY сущности внутри этой сущности, мы падаем с ошибкой LazyInitializationException.
Ярослав 40 уровень, Днепр Master
3 мая 2019

User userFindByUsername = userRepo.findByUsername(username);
В коде репозитория выше этого метода попросту нет, а по-умолчанию из коробки можно искать только по полю, которое помечено, как @Id.
Ярослав 40 уровень, Днепр Master
3 мая 2019
UserRepo, NoteRepo - не следует укрощать названия классов, жертвуя их читабельностью. Repository.
Ярослав 40 уровень, Днепр Master
3 мая 2019

@Controller
public class RegistrationController
{
  @Autowired
  private UserRepo userRepo;

  @Autowired
  private PasswordEncoder passwordEncoder;

  ...
}
Плохой архитектурный ход пихать логику прямиком в контроллеры, должен быть слой бизнес-логики - севисный слой, и для задачи по регистрации должен был быть отдельный сервис. Контроллер и, в целом, транспортный слой или слой инфраструктуры, как его еще называют, должен отвечать только за принятие и отдачу данных любыми средствами - RPC (HTTP), AMQP. За логику должен отвечать другой слой между репозиториями и контроллерами - сервисный слой.
Ярослав 40 уровень, Днепр Master
3 мая 2019

@Service
@Repository
public interface NoteRepo extends JpaRepository<Note, Long>
{
  List<Note> findByUserId(Long userId);
}
Аннотация @Service бесполезна, она помечает интерфейс компонентом контекста, однако Repository делает все то же самое + человеческую обработку ошибок с подлежащей базы данных.