Как стать автором
Обновить

Spring без XML. Часть 1

Время на прочтение 23 мин
Количество просмотров 49K
Привет, хабрахабр!

На данный момент тут уже довольно много гайдов по такой связке, но они на мой взгляд во-первых немного устаревшие, во вторых — я считаю что должен быть гайд как сделать что-то осязаемое но простое, чтобы показать что и такое возможно.

Итак, если вам хочется попробовать Spring MVC с сохранением в базе и 0(нулем) файлов xml-конфигураций, прошу под кат!



Конечно хотелось бы сразу запустить приложение, но сначала немного подготовимся.

1. Подготовка к запуску


1.1 IDE


Вся разработка будет вестись на Intellij IDEA, но не думаю что реализация в другой IDE будет сильно сложнее.

1.2 Структура папок


Сначала создадим папку проекта, назовем ее ForHabrahabr
Для нашего проекта в корне нужно создать вот такое дерево папок:

directories tree
(можно же просто сделать по инструкции для остальных в следующем разделе)

1.3 Gradle & Git


Для самостоятельных
Итак, каркас приложения мы получили.
Теперь добавим в него контроль версий и сборщик.
Для этого в 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'
}


После чего в консольке в той же директории где 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).

image

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

В итоге должен получиться такой проект:

image

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/ для шаблонизатора.

Папка ресурсов будет выглядеть вот так:

image

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

Готово, можете впервые запустить ваше приложение без падения.

Для запуска нужно запустить таску bootRun (двойной клик по ней):

image

Если нет такой панельки, идем в 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
    image
  • Нету панельки Gradle? Открываем ее тут
    image
  • Запускаем bootRun
    image
  • На этом шаге уже должно все работать


Многое пришлось опустить/не объяснять чтобы не запутать окончательно, но если считаете необходимым добавить что-то уже сейчас — пишите в лс.

Осталось материала еще минимум на одну часть, если, конечно, тема актуальна. (Controllers, EntityToEntity(ManyToOne OneToOne etc), User Roles, Testing etc)

Комментарии к первой части


В самой статье пришлось некоторые момоменты пропустить, постараюсь про максимальное их количество написать здесь. Эта часть не нужна для запуска приложения, но может пригодиться в выяснении непонятных моментов.
Читать...

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 пользователей.
Теги:
Хабы:
+6
Комментарии 33
Комментарии Комментарии 33

Публикации

Истории

Работа

Java разработчик
359 вакансий

Ближайшие события

Московский туристический хакатон
Дата 23 марта – 7 апреля
Место
Москва Онлайн
Геймтон «DatsEdenSpace» от DatsTeam
Дата 5 – 6 апреля
Время 17:00 – 20:00
Место
Онлайн