Привет, хабрахабр!
На данный момент тут уже довольно много гайдов по такой связке, но они на мой взгляд во-первых немного устаревшие, во вторых — я считаю что должен быть гайд как сделать что-то осязаемое но простое, чтобы показать что и такое возможно.
Итак, если вам хочется попробовать Spring MVC с сохранением в базе и 0(нулем) файлов xml-конфигураций, прошу под кат!
Конечно хотелось бы сразу запустить приложение, но сначала немного подготовимся.
Вся разработка будет вестись на Intellij IDEA, но не думаю что реализация в другой IDE будет сильно сложнее.
Сначала создадим папку проекта, назовем ее ForHabrahabr
Для нашего проекта в корне нужно создать вот такое дерево папок:

(можно же просто сделать по инструкции для остальных в следующем разделе)
Теперь можно использвать gradlew.bat/gradlew в зависимости от ОС.
В качестве БД выберем MySQL как самую простую для quickstart. Создаем ее на localhost,
в ней создаем базу forhabrahabr, в дальнейшем в ней будем создавать таблички
users
roles
users_roles
posts
likes
Но об этом позже, пока достаточно создать БД.
Итак, для начала откроем наш только что созданный проект в Intellj IDEA, она увидит Gradle и предложит использовать его:
(Welcome to ItelliJ IDEA -> Open -> ForHabrahabr).

В этом окошке просто жмете ок, если его нет(или проблемы с Gradle JVM) — пишите в лс, буду разбираться что не так.
В итоге должен получиться такой проект:

Первым делом создадим пакет для всех классов, назовем его habraspring(обычная папка в src/main/java/), а в нем — первый
класс Application:
Но в таком виде наше приложение еще не запустится, надо показать автоконфигуратору где находится база данных, для этого добавим файл в папку resources/ файл application.properties.
Также надо создать в resources/ папку templates/ для шаблонизатора.
Папка ресурсов будет выглядеть вот так:

Не обращайте внимание на файлы .gitkeep, для работы программы они не нужны, можно их спокойно удалять/не создавать.
Готово, можете впервые запустить ваше приложение без падения.
Для запуска нужно запустить таску bootRun (двойной клик по ней):

Если нет такой панельки, идем в View -> Tool Windows -> Gradle.
В логе приложения будет что-то вроде такого:
Попробуйте теперь зайти по адресу http://localhost:8080, должен работать.
Ну а теперь хотелось бы увидеть немного контента, не так ли?
Для этого нам понадобится создать два класса конфигурации(помните, никаких XML!) во вложенной папке config, а также добавить home.html (аналог index.html) в папку resources.
Файлы конфигурации у нас простейшие, ведь мы их используем для страниц без контроллера:
Ну и простейшая домашняя страничка:
Как должен выглядеть проект на этом этапе можно посмотреть(и скачать) тут:
github.com/MaxPovver/ForHabrahabr/tree/withbasicmvc
*не забудьте в папке проекта написать в консоли git checkout withbasicmvc
Если на данный момент все сделано правильно, по http://localhost:8080 у вас должно выводится
Итак, мы хотим добавить контроллеров, и чтобы доступ к ним выдавался только авторизованным юзерам, но у нас их пока нет.
Для того, чтобы механизм авторизации заработал, нам надо добавить сущность юзера в проект.
Необходимые шаги:
Сначала создадим в бд простейшую табличку users с полями id, username, password.
Теперь создадим подпакет entities для сущностей и создадим в нем класс User:
Никаких hbm.xml не нужно, даже аннотировать поля не нужно(исключение — поле ID, его всегда надо отмечать)
Здесь Spring все делает за нас, достаточно отнаследоваться чтобы он понял что ему генерировать, код же писать не нужно вообще:
Для этого нам надо создать класс реализующий интерфейс UserDetailsService и подлючить его в WebSecurityConfig
Теперь изменим код WebSecurityConfig:
Добавим страничку входа login.html и «секретную» (только для авторизованных) страничку secret.html:
И сделаем новые странички доступными без контроллера, добавив в WebMvcConfig 2 строчки:
Готово! Теперь по адресу http://localhost:8080 у вас должно все выводиться нормально,
а вот по адресу http://localhost:8080/secret Вы пройти не сможете — будет кидать в /login, требуя валидную пару юзер/пароль.
Теперь добавьте в вашу таблицу forhabrahabr.users запись c паролем и логином user, user (или запустите скрипт github.com/MaxPovver/ForHabrahabr/blob/withauth/import_me.sql в вашей дб).
Если вы все сделали правильно, теперь вас должно пускать в /secret.
Итак, мы уже используем полноценное Spring MVC приложение с использованием Spring Security для безопасности и Spring JPA для работы с БД. И никаких XML.
Многое пришлось опустить/не объяснять чтобы не запутать окончательно, но если считаете необходимым добавить что-то уже сейчас — пишите в лс.
Осталось материала еще минимум на одну часть, если, конечно, тема актуальна. (Controllers, EntityToEntity(ManyToOne OneToOne etc), User Roles, Testing etc)
В самой статье пришлось некоторые момоменты пропустить, постараюсь про максимальное их количество написать здесь. Эта часть не нужна для запуска приложения, но может пригодиться в выяснении непонятных моментов.
UPD вторая часть: Spring без XML. Часть 2
На данный момент тут уже довольно много гайдов по такой связке, но они на мой взгляд во-первых немного устаревшие, во вторых — я считаю что должен быть гайд как сделать что-то осязаемое но простое, чтобы показать что и такое возможно.
Итак, если вам хочется попробовать Spring MVC с сохранением в базе и 0(нулем) файлов xml-конфигураций, прошу под кат!
Содержание
1. Подготовка к запуску
1.1 IDE
1.2 Структура папок
1.3 Gradle & Git
1.3.1 Для остальных
1.4 База данных
2. Начинаем кодить
2.1 Создание проекта
2.2 Добавляем первый код
2.3 Контент
3. Добавляем работу с БД
3.1 Сущность «User»
3.2 Репозиторий UsersRepository
3.3 Добавление связи между юзером и Spring Security
4. К чему мы пришли
4.1 Для желающих запустить готовый проект
Конечно хотелось бы сразу запустить приложение, но сначала немного подготовимся.
1. Подготовка к запуску
1.1 IDE
Вся разработка будет вестись на Intellij IDEA, но не думаю что реализация в другой IDE будет сильно сложнее.
1.2 Структура папок
Сначала создадим папку проекта, назовем ее ForHabrahabr
Для нашего проекта в корне нужно создать вот такое дерево папок:

(можно же просто сделать по инструкции для остальных в следующем разделе)
1.3 Gradle & Git
Для самостоятельных
Итак, каркас приложения мы получили.
Теперь добавим в него контроль версий и сборщик.
Для этого в ForHabrahabr добавим .gitignore с вот таким содержанием:
.gradle
.idea
*.iml
build/
Заходим в эту директорию через консоль и пишем
Теперь добавим bulid.gradle со всеми зависимостями которые нам пригодятся в процессе написания приложения.
После чего в консольке в той же директории где build.gradle пишем
Теперь добавим в него контроль версий и сборщик.
Для этого в ForHabrahabr добавим .gitignore с вот таким содержанием:
.gradle
.idea
*.iml
build/
Заходим в эту директорию через консоль и пишем
git init
Теперь добавим bulid.gradle со всеми зависимостями которые нам пригодятся в процессе написания приложения.
build.gradle
buildscript {
repositories {
mavenCentral()
}
dependencies {
classpath(«org.springframework.boot:spring-boot-gradle-plugin:1.2.5.RELEASE»)
classpath 'mysql:mysql-connector-java:5.1.34'
}
}
apply plugin: 'java'
apply plugin: 'eclipse'
apply plugin: 'idea'
apply plugin: 'spring-boot'
jar {
baseName = 'gs-rest-service'
version = '0.1.0'
}
repositories {
mavenCentral()
}
sourceCompatibility = 1.8
targetCompatibility = 1.8
dependencies {
compile(«org.springframework.boot:spring-boot-starter-web»)
compile(«org.springframework.boot:spring-boot-starter-data-jpa»)
compile(«org.springframework.boot:spring-boot-starter-security»)
compile(«org.springframework.boot:spring-boot-starter-thymeleaf»)
compile 'mysql:mysql-connector-java:5.1.31'
compile 'commons-dbcp:commons-dbcp:1.4'
testCompile(«org.springframework:spring-test»)
testCompile(«junit:junit»)
testCompile 'org.springframework.security:spring-security-test:4.0.1.RELEASE'
}
task wrapper(type: Wrapper) {
gradleVersion = '2.3'
}
repositories {
mavenCentral()
}
dependencies {
classpath(«org.springframework.boot:spring-boot-gradle-plugin:1.2.5.RELEASE»)
classpath 'mysql:mysql-connector-java:5.1.34'
}
}
apply plugin: 'java'
apply plugin: 'eclipse'
apply plugin: 'idea'
apply plugin: 'spring-boot'
jar {
baseName = 'gs-rest-service'
version = '0.1.0'
}
repositories {
mavenCentral()
}
sourceCompatibility = 1.8
targetCompatibility = 1.8
dependencies {
compile(«org.springframework.boot:spring-boot-starter-web»)
compile(«org.springframework.boot:spring-boot-starter-data-jpa»)
compile(«org.springframework.boot:spring-boot-starter-security»)
compile(«org.springframework.boot:spring-boot-starter-thymeleaf»)
compile 'mysql:mysql-connector-java:5.1.31'
compile 'commons-dbcp:commons-dbcp:1.4'
testCompile(«org.springframework:spring-test»)
testCompile(«junit:junit»)
testCompile 'org.springframework.security:spring-security-test:4.0.1.RELEASE'
}
task wrapper(type: Wrapper) {
gradleVersion = '2.3'
}
После чего в консольке в той же директории где build.gradle пишем
gradle wrapper ./gradlew build (или для windows ./gradlew.bat build)
Теперь можно использвать gradlew.bat/gradlew в зависимости от ОС.
1.3.1 Для остальных
- Заходите через консоль в папку где находятся ваши проекты Idea
- git clone github.com/MaxPovver/ForHabrahabr.git
- git cd ForHabrahabr/
- git checkout quikstart
- Все, теперь у вас есть готовая структура проекта.
1.4 База данных
В качестве БД выберем MySQL как самую простую для quickstart. Создаем ее на localhost,
в ней создаем базу forhabrahabr, в дальнейшем в ней будем создавать таблички
users
roles
users_roles
posts
likes
Но об этом позже, пока достаточно создать БД.
2. Начинаем кодить
2.1 Создание проекта
Итак, для начала откроем наш только что созданный проект в Intellj IDEA, она увидит Gradle и предложит использовать его:
(Welcome to ItelliJ IDEA -> Open -> ForHabrahabr).

В этом окошке просто жмете ок, если его нет(или проблемы с Gradle JVM) — пишите в лс, буду разбираться что не так.
В итоге должен получиться такой проект:

2.2 Добавляем первый код
Первым делом создадим пакет для всех классов, назовем его habraspring(обычная папка в src/main/java/), а в нем — первый
класс Application:
Код класса
package habraspring;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
@SpringBootApplication
@ComponentScan
@EnableJpaRepositories(basePackages = {"habraspring"})
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
Но в таком виде наше приложение еще не запустится, надо показать автоконфигуратору где находится база данных, для этого добавим файл в папку resources/ файл application.properties.
С вот таким содержанием
#settings for database spring.datasource.url=jdbc:mysql://localhost/forhabrahabr spring.datasource.username=root spring.datasource.password= spring.datasource.driver-class-name=com.mysql.jdbc.Driver #turned on to enable lazy loading spring.jpa.properties.hibernate.enable_lazy_load_no_trans = true
Также надо создать в resources/ папку templates/ для шаблонизатора.
Папка ресурсов будет выглядеть вот так:

Не обращайте внимание на файлы .gitkeep, для работы программы они не нужны, можно их спокойно удалять/не создавать.
Готово, можете впервые запустить ваше приложение без падения.
Для запуска нужно запустить таску bootRun (двойной клик по ней):

Если нет такой панельки, идем в View -> Tool Windows -> Gradle.
В логе приложения будет что-то вроде такого:
Лог запуска
15:24:47: Executing external task 'bootRun'... :compileJava UP-TO-DATE :processResources :classes :findMainClass :bootRun . ____ _ __ _ _ /\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \ ( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \ \\/ ___)| |_)| | | | | || (_| | ) ) ) ) ' |____| .__|_| |_|_| |_\__, | / / / / =========|_|==============|___/=/_/_/_/ :: Spring Boot :: (v1.2.5.RELEASE) 2015-07-11 14:24:49.180 INFO 12590 --- [ main] habraspring.Application : Starting Application on MacBook-Pro-Maksim.local with PID 12590 (/Users/admin/IdeaProjects/ForHabrahabr/build/classes/main started by admin in /Users/admin/IdeaProjects/ForHabrahabr) 2015-07-11 14:24:49.230 INFO 12590 --- [ main] ationConfigEmbeddedWebApplicationContext : Refreshing org.springframework.boot.context.embedded.AnnotationConfigEmbeddedWebApplicationContext@2eda0940: startup date [Sat Jul 11 14:24:49 MSK 2015]; root of context hierarchy 2015-07-11 14:24:50.029 INFO 12590 --- [ main] o.s.b.f.s.DefaultListableBeanFactory : Overriding bean definition for bean 'beanNameViewResolver': replacing [Root bean: class [null]; scope=; abstract=false; lazyInit=false; autowireMode=3; dependencyCheck=0; autowireCandidate=true; primary=false; factoryBeanName=org.springframework.boot.autoconfigure.web.ErrorMvcAutoConfiguration$WhitelabelErrorViewConfiguration; factoryMethodName=beanNameViewResolver; initMethodName=null; destroyMethodName=(inferred); defined in class path resource [org/springframework/boot/autoconfigure/web/ErrorMvcAutoConfiguration$WhitelabelErrorViewConfiguration.class]] with [Root bean: class [null]; scope=; abstract=false; lazyInit=false; autowireMode=3; dependencyCheck=0; autowireCandidate=true; primary=false; factoryBeanName=org.springframework.boot.autoconfigure.web.WebMvcAutoConfiguration$WebMvcAutoConfigurationAdapter; factoryMethodName=beanNameViewResolver; initMethodName=null; destroyMethodName=(inferred); defined in class path resource [org/springframework/boot/autoconfigure/web/WebMvcAutoConfiguration$WebMvcAutoConfigurationAdapter.class]] 2015-07-11 14:24:50.701 INFO 12590 --- [ main] trationDelegate$BeanPostProcessorChecker : Bean 'org.springframework.transaction.annotation.ProxyTransactionManagementConfiguration' of type [class org.springframework.transaction.annotation.ProxyTransactionManagementConfiguration$$EnhancerBySpringCGLIB$$1f1e9ae] is not eligible for getting processed by all BeanPostProcessors (for example: not eligible for auto-proxying) 2015-07-11 14:24:50.727 INFO 12590 --- [ main] trationDelegate$BeanPostProcessorChecker : Bean 'transactionAttributeSource' of type [class org.springframework.transaction.annotation.AnnotationTransactionAttributeSource] is not eligible for getting processed by all BeanPostProcessors (for example: not eligible for auto-proxying) 2015-07-11 14:24:50.741 INFO 12590 --- [ main] trationDelegate$BeanPostProcessorChecker : Bean 'transactionInterceptor' of type [class org.springframework.transaction.interceptor.TransactionInterceptor] is not eligible for getting processed by all BeanPostProcessors (for example: not eligible for auto-proxying) 2015-07-11 14:24:50.746 INFO 12590 --- [ main] trationDelegate$BeanPostProcessorChecker : Bean 'org.springframework.transaction.config.internalTransactionAdvisor' of type [class org.springframework.transaction.interceptor.BeanFactoryTransactionAttributeSourceAdvisor] is not eligible for getting processed by all BeanPostProcessors (for example: not eligible for auto-proxying) 2015-07-11 14:24:51.168 INFO 12590 --- [ main] s.b.c.e.t.TomcatEmbeddedServletContainer : Tomcat initialized with port(s): 8080 (http) 2015-07-11 14:24:51.408 INFO 12590 --- [ main] o.apache.catalina.core.StandardService : Starting service Tomcat 2015-07-11 14:24:51.409 INFO 12590 --- [ main] org.apache.catalina.core.StandardEngine : Starting Servlet Engine: Apache Tomcat/8.0.23 2015-07-11 14:24:51.601 INFO 12590 --- [ost-startStop-1] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring embedded WebApplicationContext 2015-07-11 14:24:51.601 INFO 12590 --- [ost-startStop-1] o.s.web.context.ContextLoader : Root WebApplicationContext: initialization completed in 2374 ms 2015-07-11 14:24:52.570 INFO 12590 --- [ost-startStop-1] b.a.s.AuthenticationManagerConfiguration : Using default security password: bd1659e1-4c49-43a2-9fd6-2ca7d46e9e23 2015-07-11 14:24:52.614 INFO 12590 --- [ost-startStop-1] o.s.s.web.DefaultSecurityFilterChain : Creating filter chain: Ant [pattern='/css/**'], [] 2015-07-11 14:24:52.614 INFO 12590 --- [ost-startStop-1] o.s.s.web.DefaultSecurityFilterChain : Creating filter chain: Ant [pattern='/js/**'], [] 2015-07-11 14:24:52.614 INFO 12590 --- [ost-startStop-1] o.s.s.web.DefaultSecurityFilterChain : Creating filter chain: Ant [pattern='/images/**'], [] 2015-07-11 14:24:52.614 INFO 12590 --- [ost-startStop-1] o.s.s.web.DefaultSecurityFilterChain : Creating filter chain: Ant [pattern='/**/favicon.ico'], [] 2015-07-11 14:24:52.614 INFO 12590 --- [ost-startStop-1] o.s.s.web.DefaultSecurityFilterChain : Creating filter chain: Ant [pattern='/error'], [] 2015-07-11 14:24:52.650 INFO 12590 --- [ost-startStop-1] o.s.s.web.DefaultSecurityFilterChain : Creating filter chain: OrRequestMatcher [requestMatchers=[Ant [pattern='/**']]], [org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter@5854c7d0, org.springframework.security.web.context.SecurityContextPersistenceFilter@874f491, org.springframework.security.web.header.HeaderWriterFilter@34c74c36, org.springframework.security.web.authentication.logout.LogoutFilter@609329b3, org.springframework.security.web.authentication.www.BasicAuthenticationFilter@a37632c, org.springframework.security.web.savedrequest.RequestCacheAwareFilter@33a36df4, org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter@a3153e3, org.springframework.security.web.authentication.AnonymousAuthenticationFilter@1b8b1dc9, org.springframework.security.web.session.SessionManagementFilter@5ad0989a, org.springframework.security.web.access.ExceptionTranslationFilter@3e313564, org.springframework.security.web.access.intercept.FilterSecurityInterceptor@1fb86c05] 2015-07-11 14:24:52.723 INFO 12590 --- [ost-startStop-1] o.s.b.c.embedded.FilterRegistrationBean : Mapping filter: 'characterEncodingFilter' to: [/*] 2015-07-11 14:24:52.724 INFO 12590 --- [ost-startStop-1] o.s.b.c.embedded.FilterRegistrationBean : Mapping filter: 'hiddenHttpMethodFilter' to: [/*] 2015-07-11 14:24:52.724 INFO 12590 --- [ost-startStop-1] o.s.b.c.embedded.FilterRegistrationBean : Mapping filter: 'springSecurityFilterChain' to: [/*] 2015-07-11 14:24:52.724 INFO 12590 --- [ost-startStop-1] o.s.b.c.e.ServletRegistrationBean : Mapping servlet: 'dispatcherServlet' to [/] 2015-07-11 14:24:53.410 INFO 12590 --- [ main] j.LocalContainerEntityManagerFactoryBean : Building JPA container EntityManagerFactory for persistence unit 'default' 2015-07-11 14:24:53.425 INFO 12590 --- [ main] o.hibernate.jpa.internal.util.LogHelper : HHH000204: Processing PersistenceUnitInfo [ name: default ...] 2015-07-11 14:24:53.500 INFO 12590 --- [ main] org.hibernate.Version : HHH000412: Hibernate Core {4.3.10.Final} 2015-07-11 14:24:53.503 INFO 12590 --- [ main] org.hibernate.cfg.Environment : HHH000206: hibernate.properties not found 2015-07-11 14:24:53.505 INFO 12590 --- [ main] org.hibernate.cfg.Environment : HHH000021: Bytecode provider name : javassist 2015-07-11 14:24:53.628 INFO 12590 --- [ main] o.hibernate.annotations.common.Version : HCANN000001: Hibernate Commons Annotations {4.0.5.Final} 2015-07-11 14:24:53.711 INFO 12590 --- [ main] org.hibernate.dialect.Dialect : HHH000400: Using dialect: org.hibernate.dialect.MySQL5Dialect 2015-07-11 14:24:53.774 INFO 12590 --- [ main] o.h.h.i.ast.ASTQueryTranslatorFactory : HHH000397: Using ASTQueryTranslatorFactory 2015-07-11 14:24:54.244 INFO 12590 --- [ main] s.w.s.m.m.a.RequestMappingHandlerAdapter : Looking for @ControllerAdvice: org.springframework.boot.context.embedded.AnnotationConfigEmbeddedWebApplicationContext@2eda0940: startup date [Sat Jul 11 14:24:49 MSK 2015]; root of context hierarchy 2015-07-11 14:24:54.328 INFO 12590 --- [ main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/error]}" onto public org.springframework.http.ResponseEntity<java.util.Map<java.lang.String, java.lang.Object>> org.springframework.boot.autoconfigure.web.BasicErrorController.error(javax.servlet.http.HttpServletRequest) 2015-07-11 14:24:54.328 INFO 12590 --- [ main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/error],produces=[text/html]}" onto public org.springframework.web.servlet.ModelAndView org.springframework.boot.autoconfigure.web.BasicErrorController.errorHtml(javax.servlet.http.HttpServletRequest) 2015-07-11 14:24:54.356 INFO 12590 --- [ main] o.s.w.s.handler.SimpleUrlHandlerMapping : Mapped URL path [/webjars/**] onto handler of type [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler] 2015-07-11 14:24:54.357 INFO 12590 --- [ main] o.s.w.s.handler.SimpleUrlHandlerMapping : Mapped URL path [/**] onto handler of type [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler] 2015-07-11 14:24:54.393 INFO 12590 --- [ main] o.s.w.s.handler.SimpleUrlHandlerMapping : Mapped URL path [/**/favicon.ico] onto handler of type [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler] 2015-07-11 14:24:54.723 INFO 12590 --- [ main] o.s.j.e.a.AnnotationMBeanExporter : Registering beans for JMX exposure on startup 2015-07-11 14:24:54.800 INFO 12590 --- [ main] s.b.c.e.t.TomcatEmbeddedServletContainer : Tomcat started on port(s): 8080 (http) 2015-07-11 14:24:54.803 INFO 12590 --- [ main] habraspring.Application : Started Application in 5.945 seconds (JVM running for 6.529)
Попробуйте теперь зайти по адресу http://localhost:8080, должен работать.
Ну а теперь хотелось бы увидеть немного контента, не так ли?
2.3 Контент
Для этого нам понадобится создать два класса конфигурации(помните, никаких XML!) во вложенной папке config, а также добавить home.html (аналог index.html) в папку resources.
Файлы конфигурации у нас простейшие, ведь мы их используем для страниц без контроллера:
config/MvcConfig.java
package habraspring.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
@Configuration
public class MvcConfig extends WebMvcConfigurerAdapter {
@Override
public void addViewControllers(ViewControllerRegistry registry) {
registry.addViewController("/home").setViewName("home");
registry.addViewController("/").setViewName("home");
}
}
config/WebSecurityConfig.java
package habraspring.config;
import org.springframework.boot.autoconfigure.security.SecurityProperties;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.annotation.web.servlet.configuration.EnableWebMvcSecurity;
@Configuration
@EnableWebMvcSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
@Order(SecurityProperties.ACCESS_OVERRIDE_ORDER)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/", "/home").permitAll();
}
}
Ну и простейшая домашняя страничка:
home.html
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org" xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
<head>
<title>Habrahabr</title>
</head>
<body>
<h1>Welcome!</h1>
<p>Yours home page.</p>
</body>
</html>
Как должен выглядеть проект на этом этапе можно посмотреть(и скачать) тут:
github.com/MaxPovver/ForHabrahabr/tree/withbasicmvc
*не забудьте в папке проекта написать в консоли git checkout withbasicmvc
Если на данный момент все сделано правильно, по http://localhost:8080 у вас должно выводится
Welcome! Yours home page.
3. Добавляем работу с БД
Итак, мы хотим добавить контроллеров, и чтобы доступ к ним выдавался только авторизованным юзерам, но у нас их пока нет.
Для того, чтобы механизм авторизации заработал, нам надо добавить сущность юзера в проект.
Необходимые шаги:
- Добавить класс, описывающий сущность юзера в бд
- Добавить репозиторий новой сущности
- Добавить «связь» между механизмом Spring Security и нашей сущностью
- Все везде зарегистрировать
- «Включить» Spring Security
3.1 Сущность «User»
Сначала создадим в бд простейшую табличку users с полями id, username, password.
Теперь создадим подпакет entities для сущностей и создадим в нем класс User:
entities/User.java
package habraspring.entities;
import javax.persistence.*;
@Entity
@Table(name="users")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private long id;
private String username;
private String password;
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public long getId() {
return id;
}
public void setId(long id) {
this.id = id;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
protected User(){}
public User(String name, String pass) {
username = name;
password = pass;
}
}
Никаких hbm.xml не нужно, даже аннотировать поля не нужно(исключение — поле ID, его всегда надо отмечать)
3.2 Репозиторий UsersRepository
Здесь Spring все делает за нас, достаточно отнаследоваться чтобы он понял что ему генерировать, код же писать не нужно вообще:
UsersRepository.java
package habraspring.repositories;
import habraspring.entities.User;
import org.springframework.data.repository.CrudRepository;
public interface UsersRepository extends CrudRepository<User, Long> {
User findByUsername(String username);
}
3.3 Добавление связи между юзером и Spring Security
Для этого нам надо создать класс реализующий интерфейс UserDetailsService и подлючить его в WebSecurityConfig
utils/MySQLUserDetailsService.java
package habraspring.utils;
import habraspring.entities.User;
import habraspring.repositories.UsersRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.InternalAuthenticationServiceException;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
@Service
public class MySQLUserDetailsService implements UserDetailsService {
@Autowired
UsersRepository users;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
UserDetails loadedUser;
try {
User client = users.findByUsername(username);
loadedUser = new org.springframework.security.core.userdetails.User(
client.getUsername(), client.getPassword(),
DummyAuthority.getAuth());
} catch (Exception repositoryProblem) {
throw new InternalAuthenticationServiceException(repositoryProblem.getMessage(), repositoryProblem);
}
return loadedUser;
}
static class DummyAuthority implements GrantedAuthority
{
static Collection<GrantedAuthority> getAuth()
{
List<GrantedAuthority> res = new ArrayList<>(1);
res.add(new DummyAuthority());
return res;
}
@Override
public String getAuthority() {
return "USER";
}
}
}
Теперь изменим код WebSecurityConfig:
Заголовок спойлера
package habraspring.config;
import habraspring.utils.MySQLUserDetailsService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.security.SecurityProperties;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.annotation.web.servlet.configuration.EnableWebMvcSecurity;
@Configuration
@EnableWebMvcSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
@Order(SecurityProperties.ACCESS_OVERRIDE_ORDER)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/", "/home").permitAll()
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/login")
.permitAll()
.and()
.logout()
.permitAll();
}
@Autowired
private MySQLUserDetailsService mySQLUserDetailsService;
@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(mySQLUserDetailsService);
}
}
Добавим страничку входа login.html и «секретную» (только для авторизованных) страничку secret.html:
Их код
secret.html
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org"
xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
<head>
<title>Secret page</title>
</head>
<body>
<h1 th:inline="text">Hello [[${#httpServletRequest.remoteUser}]]!</h1>
<form th:action="@{/logout}" method="post">
<input type="submit" value="Sign Out"/>
</form>
</body>
</html>
login.html
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org"
xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
<head>
<title>Login page</title>
</head>
<body>
<div th:if="${param.error}">
Invalid username and password.
</div>
<div th:if="${param.logout}">
You have been logged out.
</div>
<form th:action="@{/login}" method="post">
<div><label> User Name : <input type="text" name="username"/> </label></div>
<div><label> Password: <input type="password" name="password"/> </label></div>
<div><input type="submit" value="Sign In"/></div>
</form>
</body>
</html>
И сделаем новые странички доступными без контроллера, добавив в WebMvcConfig 2 строчки:
registry.addViewController("/login").setViewName("login");
registry.addViewController("/secret").setViewName("secret");
Готово! Теперь по адресу http://localhost:8080 у вас должно все выводиться нормально,
а вот по адресу http://localhost:8080/secret Вы пройти не сможете — будет кидать в /login, требуя валидную пару юзер/пароль.
Теперь добавьте в вашу таблицу forhabrahabr.users запись c паролем и логином user, user (или запустите скрипт github.com/MaxPovver/ForHabrahabr/blob/withauth/import_me.sql в вашей дб).
Если вы все сделали правильно, теперь вас должно пускать в /secret.
4. К чему мы пришли
Итак, мы уже используем полноценное Spring MVC приложение с использованием Spring Security для безопасности и Spring JPA для работы с БД. И никаких XML.
4.1 Для желающих запустить готовый проект
- git clone github.com/MaxPovver/ForHabrahabr.git
- cd ForHabrahabr/
- git checkout withauth
- запускаем в своей локальной mysql бд import_me.sql(или создаем руками табличку и данные для нее)
- Открываем через IDEA созданную папку ForHabrahabr
- Нету панельки Gradle? Открываем ее тут
- Запускаем bootRun
- На этом шаге уже должно все работать
Многое пришлось опустить/не объяснять чтобы не запутать окончательно, но если считаете необходимым добавить что-то уже сейчас — пишите в лс.
Осталось материала еще минимум на одну часть, если, конечно, тема актуальна. (Controllers, EntityToEntity(ManyToOne OneToOne etc), User Roles, Testing etc)
Комментарии к первой части
В самой статье пришлось некоторые момоменты пропустить, постараюсь про максимальное их количество написать здесь. Эта часть не нужна для запуска приложения, но может пригодиться в выяснении непонятных моментов.
Читать...
Приведу еще раз его код:
Что делает этот метод? Он привязывает какой-то запрос из адресной строки к какому-то шаблону из папки resources/.
К примеру если у нашего сервера просят показать содержимое "/" или "/home", он вернет home.html.
Аналогично при запросе "/login" вернется login.html.
Рассмотрим данный класс по порядку:
разрешаем отдавать запросы из этого списка любому запросившему:
Все остальное разрешаем открывать только авторизованным пользователям, указываем где находится форма логина, открыв для всех ее и страницу для выхода:
Также мы определяем откуда доставать пользователей нашей системе защиты, для этого используем @Autowired аннотацию, Spring сам подгрузит туда инстанс нужного сервиса:
И передаем его в тот метод, который позволяет определить наш сервис для соединения юзеров Spring Security и юзеров из базы данных.
Рассмотрим имплементацию loadUserByUsername.
Здесь мы снова используем @Autowired чтобы spring подставил в users реализованный интерфейс репозитория юзеров из базы данных и вытаскиваем с его помощью пользователя с заданным никнеймом из базы:
А здесь мы возвращаем «сконвертированного» из сущности базы данных в сущность Spring Security пользователя. Вот только появляется проблема — наш пользователь еще не имеет привязанных ролей(их сделаем позже), так что создадим класс заглушку, выдающий любому существующему юзеру пользовательские права. В случае если юзер не существует — код вылетит раньше с исключением. Дальше Spring сам для этого пользователя проверит соответствие введенного пароля и пароля объекта в базе с таким именем пользователя:
Рассмотрим код построчно:
Добавляя аннотацию Entity мы указываем сканнеру Spring что этот класс нужно привязать к таблице в базе данных.
В аннотации Table мы указываем название таблицы, к которой будем привязывать этот класс (зачастую его тоже можно не указывать, но лучше так не делать, иначе при смене названия таблицы можно словить проблем).
С полями все намного проще — они привязываются автоматически к полям в базе с таким же названием, вообще ничего писать не надо! Только нужно указать какое поле — ID, и как его генерировать для новых сущностей при сохранении в базу.
Дальше идут автоматически сгенерированные геттеры/сеттеры(лучше их делать даже если не нужны, просто на случай если ВНЕЗАПНО захотите туда логики добавить).
Ну а после идут два конструктора — пустой — только для сериализатора, напрямую в программе его использовать нельзя, и user friendly для создание новы юзеров с последующим их сохранением в UsersRepository.
Вот тут перечислены зависимости, которые Gradle автоматически подгрузит если их нет локально:
А тут перечислены зависимости, нужные для проведения интеграционных тестов (о них позже):
MvcConfig
Приведу еще раз его код:
MvcConfig.java
package habraspring.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
@Configuration
public class MvcConfig extends WebMvcConfigurerAdapter {
@Override
public void addViewControllers(ViewControllerRegistry registry) {
registry.addViewController("/home").setViewName("home");
registry.addViewController("/").setViewName("home");
registry.addViewController("/login").setViewName("login");
registry.addViewController("/secret").setViewName("secret");
}
}
Что делает этот метод? Он привязывает какой-то запрос из адресной строки к какому-то шаблону из папки resources/.
К примеру если у нашего сервера просят показать содержимое "/" или "/home", он вернет home.html.
Аналогично при запросе "/login" вернется login.html.
WebSecurityConfig
WebSecurityConfig.java
package habraspring.config;
import habraspring.utils.MySQLUserDetailsService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.security.SecurityProperties;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.annotation.web.servlet.configuration.EnableWebMvcSecurity;
@Configuration
@EnableWebMvcSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
@Order(SecurityProperties.ACCESS_OVERRIDE_ORDER)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/", "/home").permitAll()
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/login")
.permitAll()
.and()
.logout()
.permitAll();
}
@Autowired
private MySQLUserDetailsService mySQLUserDetailsService;
@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(mySQLUserDetailsService);
}
}
Рассмотрим данный класс по порядку:
разрешаем отдавать запросы из этого списка любому запросившему:
.authorizeRequests()
.antMatchers("/", "/home").permitAll()
Все остальное разрешаем открывать только авторизованным пользователям, указываем где находится форма логина, открыв для всех ее и страницу для выхода:
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/login")
.permitAll()
.and()
.logout()
.permitAll();
Также мы определяем откуда доставать пользователей нашей системе защиты, для этого используем @Autowired аннотацию, Spring сам подгрузит туда инстанс нужного сервиса:
@Autowired
private MySQLUserDetailsService mySQLUserDetailsService;
И передаем его в тот метод, который позволяет определить наш сервис для соединения юзеров Spring Security и юзеров из базы данных.
@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(mySQLUserDetailsService);
}
MySQLUserDetailsService
MySQLUserDetailsService.java
package habraspring.utils;
import habraspring.entities.User;
import habraspring.repositories.UsersRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.InternalAuthenticationServiceException;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
@Service
public class MySQLUserDetailsService implements UserDetailsService {
@Autowired
UsersRepository users;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
UserDetails loadedUser;
try {
User client = users.findByUsername(username);
loadedUser = new org.springframework.security.core.userdetails.User(
client.getUsername(), client.getPassword(),
DummyAuthority.getAuth());
} catch (Exception repositoryProblem) {
throw new InternalAuthenticationServiceException(repositoryProblem.getMessage(), repositoryProblem);
}
return loadedUser;
}
static class DummyAuthority implements GrantedAuthority
{
static Collection<GrantedAuthority> getAuth()
{
List<GrantedAuthority> res = new ArrayList<>(1);
res.add(new DummyAuthority());
return res;
}
@Override
public String getAuthority() {
return "USER";
}
}
}
Рассмотрим имплементацию loadUserByUsername.
Здесь мы снова используем @Autowired чтобы spring подставил в users реализованный интерфейс репозитория юзеров из базы данных и вытаскиваем с его помощью пользователя с заданным никнеймом из базы:
@Autowired
UsersRepository users;
....
User client = users.findByUsername(username);
А здесь мы возвращаем «сконвертированного» из сущности базы данных в сущность Spring Security пользователя. Вот только появляется проблема — наш пользователь еще не имеет привязанных ролей(их сделаем позже), так что создадим класс заглушку, выдающий любому существующему юзеру пользовательские права. В случае если юзер не существует — код вылетит раньше с исключением. Дальше Spring сам для этого пользователя проверит соответствие введенного пароля и пароля объекта в базе с таким именем пользователя:
loadedUser = new org.springframework.security.core.userdetails.User(
client.getUsername(), client.getPassword(),
DummyAuthority.getAuth());
User
User.java
package habraspring.entities;
import javax.persistence.*;
@Entity
@Table(name="users")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private long id;
private String username;
private String password;
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public long getId() {
return id;
}
public void setId(long id) {
this.id = id;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
protected User(){}
public User(String name, String pass) {
username = name;
password = pass;
}
}
Рассмотрим код построчно:
Добавляя аннотацию Entity мы указываем сканнеру Spring что этот класс нужно привязать к таблице в базе данных.
В аннотации Table мы указываем название таблицы, к которой будем привязывать этот класс (зачастую его тоже можно не указывать, но лучше так не делать, иначе при смене названия таблицы можно словить проблем).
@Entity
@Table(name="users")
public class User {
С полями все намного проще — они привязываются автоматически к полям в базе с таким же названием, вообще ничего писать не надо! Только нужно указать какое поле — ID, и как его генерировать для новых сущностей при сохранении в базу.
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private long id;
private String username;
private String password;
Дальше идут автоматически сгенерированные геттеры/сеттеры(лучше их делать даже если не нужны, просто на случай если ВНЕЗАПНО захотите туда логики добавить).
Ну а после идут два конструктора — пустой — только для сериализатора, напрямую в программе его использовать нельзя, и user friendly для создание новы юзеров с последующим их сохранением в UsersRepository.
protected User(){}
public User(String name, String pass) {
username = name;
password = pass;
}
Gradle
buildscript {
repositories {
mavenCentral()
}
dependencies {
classpath("org.springframework.boot:spring-boot-gradle-plugin:1.2.5.RELEASE")
classpath 'mysql:mysql-connector-java:5.1.34'
}
}
apply plugin: 'java'
apply plugin: 'eclipse'
apply plugin: 'idea'
apply plugin: 'spring-boot'
jar {
baseName = 'gs-rest-service'
version = '0.1.0'
}
repositories {
mavenCentral()
}
sourceCompatibility = 1.8
targetCompatibility = 1.8
dependencies {
compile("org.springframework.boot:spring-boot-starter-web")
compile("org.springframework.boot:spring-boot-starter-data-jpa")
compile("org.springframework.boot:spring-boot-starter-security")
compile("org.springframework.boot:spring-boot-starter-thymeleaf")
compile 'mysql:mysql-connector-java:5.1.31'
compile 'commons-dbcp:commons-dbcp:1.4'
testCompile("org.springframework:spring-test")
testCompile("junit:junit")
testCompile 'org.springframework.security:spring-security-test:4.0.1.RELEASE'
}
task wrapper(type: Wrapper) {
gradleVersion = '2.3'
}
Вот тут перечислены зависимости, которые Gradle автоматически подгрузит если их нет локально:
dependencies {
compile("org.springframework.boot:spring-boot-starter-web") - для работы Spring MVC
compile("org.springframework.boot:spring-boot-starter-data-jpa") - для работы Spring Jpa(работа с базой данных)
compile("org.springframework.boot:spring-boot-starter-security") - для работы Spring Security
compile("org.springframework.boot:spring-boot-starter-thymeleaf") - для работы шаблонов из resources/tempates
compile 'mysql:mysql-connector-java:5.1.31' - mysql to spring
compile 'commons-dbcp:commons-dbcp:1.4'
А тут перечислены зависимости, нужные для проведения интеграционных тестов (о них позже):
testCompile("org.springframework:spring-test")
testCompile("junit:junit")
testCompile 'org.springframework.security:spring-security-test:4.0.1.RELEASE'
UPD вторая часть: Spring без XML. Часть 2
Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.
Полезна ли статья?
10.96% Да. Дальше пилить не нужно.24
79% Да, пили дальше!173
10.05% Нет.22
Проголосовали 219 пользователей. Воздержались 70 пользователей.