Привет, хабрахабр!
На данный момент тут уже довольно много гайдов по такой связке, но они на мой взгляд во-первых немного устаревшие, во вторых — я считаю что должен быть гайд как сделать что-то осязаемое но простое, чтобы показать что и такое возможно.
Итак, если вам хочется попробовать 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 пользователей.
