Арызымды жазып жатып, колдонуучуну электрондук почта аркылуу да, социалдык тармактарда да каттоодон өткөрүү боюнча так макалалардын жоктугуна туш болдум. Классикалык кирүү формасын орнотуу боюнча жакшы окуу куралдары бар болчу. OAuth2 боюнча жакшы окуу куралдары бар болчу . Бул эки ыкманы кантип айкалыштыруу боюнча кылмыштуу аз маалымат болгон. Издөө процессинде биз иштиктүү чечимге келе алдык. Ал акыркы чындык деп ырастаbyte, бирок ал өз милдетин аткарат. Бул макалада мен нөлдөн окшош Spring Security конфигурациясы менен ноталарды сактоо кызматын кантип ишке ашырууну көрсөтөм. Эскертүү: Окурман жаз боюнча жок дегенде бир нече окуу куралдарынан өтүп кетсе жакшы болот, анткени көңүл жазгы коопсуздукка гана бурулат, репозиторийлер, контроллерлор ж.б.у.с. деталдуу түшүндүрмөлөр жок. Болбосо, ансыз деле чоң макала чыгып калмак. гигант болуу. Мазмун
Базада жаңы колдонуучу пайда болгонун көрүп жатабыз. Сырсөз шифрленген.
Эскертүүлөр маалымат базасына сакталат.
Долбоор ийгorктүү ишке ашып, иштеп жатканын көрүп жатабыз. Толук бакыт үчүн бизге социалдык тармактар аркылуу кирүү мүмкүнчүлүгү гана керек. Мейли, баштайлы!
Эми Колдонуучу жана OAuth2Authentication экөө тең Негизги катары иштей алышат. Биз UserServiceде колдонуучунун Google маалыматтары аркылуу жүктөлүшүн алдын ала эске алгандыктан, тиркеме эки типтеги колдонуучулар үчүн да иштейт. Биз долбоордун башкы бет контроллерин өзгөртүп, ал OAuth2 аркылуу кирген колдонуучуларды эскертүүлөр барагына багыттайбыз.
Колдонуучу кадимки форма аркылуу да, Google аккаунту аркылуу да ийгorктүү кирет. Бул биз каалаган нерсе! Бул макалада веб-тиркеме түзүү, аны Spring Security менен камсыздоо жана ар кандай кирүү ыкмаларын айкалыштыруу боюнча айрым ойлорду чечти деп үмүттөнөм. Толук долбоордун codeу менен сиз жасай аласыз
- Долбоорду түзүү
- Объекттерди жана Колдонмо логикасын түзүү
- Классикалык кирүү үчүн жазгы коопсуздукту конфигурациялоо
- Негизги конфигурация SecurityConfig
- Ыңгайлаштырылган колдонуучу кирүү
- Контроллорду жакшырталы
- Ишке киргизүү
- Жазгы коопсуздукта мисал катары Google аркылуу OAuth2 орнотуу
- Чыпка конфигурациясын жана application.properties
- Google Cloud Platform менен тиркемени каттоонун негизги учурлары
- CustomUserInfoTokenServices
- Долбоордун акыркы башталышы
Долбоорду түзүү
Биз start.spring.io сайтына кирип , долбоордун негизин түзөбүз:- Web - камтылган Tomcat, url карталары жана башка ушул сыяктуу тиркемелерди ишке киргизүү;
- JPA - маалымат базасына кошулуу;
- Мурут - веб-баракчаларды түзүү үчүн колдонулган шаблон кыймылдаткычы;
- Коопсуздук - тиркемени коргоо. Бул макала эмне үчүн түзүлгөн.
<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
Келгиле, маалымат базасы an objectилерин жайгаштырган пакетти түзөлү . Колдонуучу Spring Security конфигурациясына керектелүүчү User
интерфейсти ишке ашырган класс тарабынан сүрөттөлөт . UserDetails
Колдонуучунун идентификатору, колдонуучу аты (бул электрондук почта), сырсөз, аты, ролу, аракет желеги, 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();
}
}
Келгиле, идентификатор, нота аталышы, жазуунун негизги бөлүгү жана ал таандык болгон колдонуучунун идентификатору менен нота классын түзөлү:
@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
Класс RegistrationController
колдонуучуну каттоо үчүн жооптуу. Пост-карталоо формадан маалыматтарды алып, колдонуучуну маалымат базасына сактайт жана кирүү барагына багыттайт. 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);
}
}
Барактар
Мен барактарды түзүү үчүн Mostache үлгү кыймылдаткычын колдоном . Башкасын ишке ашыра аласыз, бул маанилүү эмес. Бардык беттерде колдонулган мета маалымат үчүн meta.mustache файлы түзүлдү. Ал ошондой эле биздин долбоордун барактарын сулуураак кылуу үчүн Bootstrapди камтыйт. Барактар "src/main/resources/templates" каталогунда түзүлөт. Файлдарда кеңейтүү муруту бар. HTML codeун түздөн-түз макалага жайгаштыруу аны өтө чоң кылат, андыктан бул жерде долбоордун GitHub репозиторийиндеги калыптар папкасына шилтеме .Классикалык кирүү үчүн жазгы коопсуздукту конфигурациялоо
Spring Security колдонмону жана анын ресурстарын уруксатсыз кирүүдөн коргоого жардам берет. Биз топтомго жайгаштырган мураскаSecurityConfig
алынган класста кыскача жумушчу конфигурацияны түзөбүз . Аны Spring Security колдоосун иштете турган @EnableWebSecurity annotationсы жана бул класста кандайдыр бир конфигурация бар экенин көрсөткөн @Configuration annotationсы менен белгилейли. Эскертүү: автоматтык түрдө конфигурацияланган pom.xml Spring Boot негизги компонентинин 2.1.4.RELEASE versionсын камтыган, бул Коопсуздукту белгиленген жол менен ишке ашырууга тоскоол болгон. Долбоордогу чыр-чатактарды болтурбоо үчүн versionны 2.0.1.RELEASEге өзгөртүү сунушталат. WebSecurityConfigurerAdapter
config
Негизги конфигурация SecurityConfig
Биздин конфигурация төмөнкүлөрдү аткара алат:-
Сырсөздөрдү шифрлөө аркылуу
BCryptPasswordEncoder
:@Autowired private PasswordEncoder passwordEncoder; @Bean PasswordEncoder passwordEncoder() { PasswordEncoder passwordEncoder = new BCryptPasswordEncoder(); return passwordEncoder; }
-
Атайын жазылган аутентификация провайдери аркылуу кирүү:
@Autowired private AuthProvider authProvider; @Override protected void configure(AuthenticationManagerBuilder auth) { auth.authenticationProvider(authProvider); }
-
Анонимдүү колдонуучуларга үй баракчасына, каттоо жана кирүү баракчаларына кирүүгө уруксат бериңиз. Бардык башка суроо-талаптар кирген колдонуучулар тарабынан аткарылышы керек. Кирүү баракчасы катары мурда сүрөттөлгөн "/ login" ды дайындайлы. Кирүү ийгorктүү болсо, колдонуучу эскертүүлөрдүн тизмеси бар баракка өтөт, эгер ката болсо, колдонуучу кирүү бетинде кала берет. Ийгorктүү чыккандан кийин, колдонуучу башкы бетке өтөт.
@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
колдонуучуга электрондук почта аркылуу гана эмес, колдонуучу аты менен да кирүүгө мүмкүнчүлүк берет.
@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 аккаунтунун аты жана 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
!
Контроллорду жакшырталы
Жазгы коопсуздукту конфигурацияладык. Эми ноталарыңыздын контроллеруңузда мунун пайдасын көрүүгө убакыт келди. Эми ар бир карта кошумча Негизги параметрди кабыл алат, ал аркылуу колдонуучуну табууга аракет кылат. Эмне үчүн мен түздөн-түз классты киргизе албаймUser
? Анан социалдык тармактар аркылуу логин жазганда колдонуучулардын түрлөрү дал келбегендиктен чыр-чатактар болот. Биз алдын ала керектүү ийкемдүүлүктү камсыздайбыз. Биздин ноталардын контроллерунун codeу азыр мындай көрүнөт:
@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()), же макаланын автору катары, csrf белгиси менен жашыруун талааны кошууну унутпаңыз. бардык билдирүүлөр үчүн.
Ишке киргизүү
Долбоорду ишке киргизүүгө аракет кылып жатабыз.Жазгы коопсуздукта мисал катары Google аркылуу OAuth2 орнотуу
OAuth2ди ишке ашырууда мен Жаздын бул расмий окуу куралына таяндым . OAuth2ди колдоо үчүн, pom.xml файлына төмөнкү китепкананы кошуңуз:<dependency>
<groupId>org.springframework.security.oauth.boot</groupId>
<artifactId>spring-security-oauth2-autoconfigure</artifactId>
<version>2.0.0.RELEASE</version>
</dependency>
Келгиле, жазгы коопсуздук конфигурациябызды өзгөртөлү SecurityConfig
. Биринчиден, @EnableOAuth2Client annotationсын кошолу. Ал автоматтык түрдө сиз социалдык тармактар аркылуу кирүү үчүн керектүү нерселерди чыгарат.
Чыпка конфигурациясын жана application.properties
Келгиле, коопсуздук конфигурациябызда колдонуу үчүн OAuth2ClientContext сайып көрөлү.@Autowired
private OAuth2ClientContext oAuth2ClientContext;
OAuth2ClientContext колдонуучунун социалдык кирүү өтүнүчүн ырастаган чыпка түзүүдө колдонулат. Чыпка @EnableOAuth2Client annotationсынын аркасында жеткorктүү. Биз эмне кылышыбыз керек , негизги 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;
}
Ошондой эле конфигурациялоо (HttpSecurity http) функциясына жаңы чыпка кошуу керек:
http.addFilterBefore(ssoFilter(), UsernamePasswordAuthenticationFilter.class);
Фильтр, ошондой эле кардар Google аркылуу катталганын бorши керек. @ConfigurationProperties annotationсы application.properties ичинде кайсы конфигурация касиеттерин издөө керектигин аныктайт.
@Bean
@ConfigurationProperties("google.client")
public AuthorizationCodeResourceDetails google()
{
return new AuthorizationCodeResourceDetails();
}
Аутентификацияны аяктоо үчүн сиз Google колдонуучу маалыматынын акыркы чекитин көрсөтүшүңүз керек:
@Bean
@ConfigurationProperties("google.resource")
public ResourceServerProperties googleResource()
{
return new ResourceServerProperties();
}
Колдонмобузду Google Булут Платформасында каттагандан кийин , биз 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 мүмкүндүк алуу өтүнүчүнүн терезеси:- Колдонмонун аталышы: Жазгы кирүү формасы жана OAuth2 окуу куралы
- Колдоо электрондук почта дареги: сиздин электрондук почта
- Google API чөйрөсү: электрондук почта, профиль, openid
- Ыйгарым укуктуу домендер: me.org
- Тиркеменин негизги бетине шилтеме: http://me.org:8080
- Колдонмонун купуялык саясатына шилтеме: http://me.org:8080
- Колдонмо шарттарына шилтеме: http://me.org:8080
- Түрү: Веб колдонмо
- Аталышы: Жазгы кирүү формасы жана OAuth2 окуу куралы
- Уруксат берилген JavaScript булактары: http://me.org, http://me.org:8080
- Уруксат берилген багыттоо URI'лери: http://me.org:8080/login, http://me.org:8080/login/google
CustomUserInfoTokenServices
Чыпка функциясынын сүрөттөмөсүндө Custom деген сөздү байкадыңызбы? КлассCustomUserInfoTokenServices
. Ооба, биз блэкджек жана колдонуучуну маалымат базасында сактоо мүмкүнчүлүгү менен өзүбүздүн классыбызды түзөбүз! UserInfoTokenServices
IntelliJ IDEAдагы Ctrl-N клавиатура жарлыгын колдонуп, демейки кантип ишке ашырылып жатканын таап, көрө аласыз . Келгиле, анын codeун жаңы түзүлгөн класска көчүрөлү CustomUserInfoTokenServices
. Анын көбү өзгөрүүсүз калтырылышы мүмкүн. Функциялардын логикасын өзгөртүүдөн мурун класстын жеке талаалары катары UserRepo
кошолу PasswordEncoder
. Келгиле, алар үчүн орнотуучуларды түзөлү. SecurityConfig классына @Autowired UserRepo userRepo кошолу. Чыпкаларды түзүү ыкмасындагы катаны көрсөткөн көрсөткүч кантип жоголуп жатканын карап, кубанабыз. Эмне үчүн @Autowired түздөн-түз CustomUserInfoTokenServices кызматына колдонула алган жок? Анткени бул класс көз карандылыкты кабыл алbyte, анткени анын өзү эч кандай Жазгы annotation менен белгиленбейт жана чыпка жарыяланганда анын конструктору ачык түзүлөт. Демек, Жаздын DI механизми бул тууралуу билбейт. Бул класстагы кандайдыр бир нерсеге @Autowired annotationсын жазсак, колдонулганда NullPointerException алабыз. Бирок ачык орнотуучулар аркылуу баары абдан жакшы иштейт. Керектүү компоненттерди ишке ашыргандан кийин, кызыгуунун негизги an objectиси loadAuthentication функциясы болуп калат, мында колдонуучу жөнүндө маалымат менен Map<String, Object> чыгарылат. Дал ушул долбоордо мен социалдык тармак аркылуу кирген колдонуучуну маалымат базасына сактоону ишке ашырдым. Биз Google каттоо эсебин OAuth2 провайдери катары колдонуп жаткандыктан, картада Google үчүн мүнөздүү "кошумча" талаа камтылганын текшеребиз. Эгерде ал бар болсо, бул колдонуучу тууралуу маалымат туура кабыл алынганын билдирет. Биз жаңы колдонуучуну түзүп, аны маалымат базасына сактайбыз.
@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 ичинде ар кандай опцияларды белгилеп, чыпкалоо декларациясынын методунда окшош кызматтардын ар кандай класстарын каттай аласыз.
@GetMapping("/")
public String index(Principal principal)
{
if(principal != null)
{
return "redirect:/notes";
}
return "index";
}
GO TO FULL VERSION