Статьи

Добавление входа в социальные сети в веб-приложение Spring MVC: настройка

В старые добрые времена пользователи входили в систему, используя комбинацию имени пользователя и пароля. Хотя в настоящее время некоторые люди все еще предпочитают традиционный способ, все большее число пользователей хотят войти, используя свои учетные записи в социальных сетях.

Это то, что делает Spring Social (и его подпроекты) полезным дополнением к портфелю проектов Spring. Однако интеграция Spring Social с Spring Security была немного громоздкой.

Spring Social 1.1.0 меняет все это. Он обеспечивает бесшовную интеграцию с Spring Security, а поддержка конфигурации Java в Spring Security делает настройку похожей на прогулку в парке.

Вы не должны поверить на мое слово. Продолжайте читать, и вы узнаете, как это делается.

Требования нашего решения следующие:

  • Должно быть возможно создать учетную запись пользователя с помощью обычной регистрационной формы.
  • Должно быть возможно создать учетную запись пользователя, используя социальный вход.
  • Должно быть возможно войти в систему, используя имя пользователя и пароль.
  • Должен быть возможен вход в систему с использованием SaaS API-провайдера.
  • Приложение должно поддерживать Facebook и Twitter.
  • Приложение должно использовать «обычные» контроллеры Spring MVC (без REST).

Давайте начнем с рассмотрения предпосылок этого урока.

Предпосылки

В этом руководстве предполагается, что вы уже создали приложение Facebook и Twitter, используемое в примере приложения. Вы можете создать эти приложения, перейдя по следующим ссылкам:

Если вы не знаете, как это сделать, вы можете проверить следующие ссылки:

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

Получение необходимых зависимостей с Maven

Первое, что мы должны сделать, это получить необходимые зависимости от Maven. Мы можем сделать это, объявив следующие зависимости в нашем файле POM:

  • Spring Security (версия 3.2.0.RC1).
    • Основной модуль содержит основные компоненты аутентификации и контроля доступа.
    • Модуль config содержит код, используемый для анализа файлов конфигурации XML с использованием пространства имен Spring Security XML.
    • Модуль taglibs содержит библиотеки тегов JPS Spring Security.
    • Веб- модуль содержит фильтры и весь другой код, связанный с веб-безопасностью.
  • Apache HttpClient (версия 4.2.5). Apache HttpClient является необязательной (но рекомендуемой) зависимостью Spring Social. Если он присутствует, Spring Social будет использовать его в качестве HTTP-клиента. Если нет, Spring social будет использовать стандартные компоненты Java SE.
  • Spring Social (версия 1.1.0. BUILD-SNAPSHOT).
    • Основной модуль содержит инфраструктуру подключения и обеспечивает поддержку клиентов OAuth.
    • Модуль безопасности интегрирует Spring Security с Spring Social. Он делегирует вопросы проверки подлинности, обычно решаемые Spring Security, поставщикам услуг с помощью Spring Social.
    • Веб- модуль содержит компоненты, которые обрабатывают рукопожатие аутентификации между нашим веб-приложением и поставщиком услуг.
  • Spring Social Facebook (версия 1.1.0.BUILD-SNAPSHOT) является расширением Spring Social и обеспечивает интеграцию с Facebook.
  • Spring Social Twitter (версия 1.1.0.BUILD-SNAPSHOT) — это расширение для Social Social, которое обеспечивает интеграцию с Twitter.

Соответствующая часть файла pom.xml выглядит следующим образом:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
<!-- Spring Security -->
<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-core</artifactId>
    <version>3.2.0.RC1</version>
</dependency>
<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-config</artifactId>
    <version>3.2.0.RC1</version>
</dependency>
<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-taglibs</artifactId>
    <version>3.2.0.RC1</version>
</dependency>
<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-web</artifactId>
    <version>3.2.0.RC1</version>
</dependency>
 
<!-- Use Apache HttpClient as HTTP Client -->
<dependency>
    <groupId>org.apache.httpcomponents</groupId>
    <artifactId>httpclient</artifactId>
    <version>4.2.5</version>
</dependency>
 
<!-- Spring Social -->
<dependency>
    <groupId>org.springframework.social</groupId>
    <artifactId>spring-social-core</artifactId>
    <version>1.1.0.BUILD-SNAPSHOT</version>
</dependency>
<dependency>   
    <groupId>org.springframework.social</groupId>
    <artifactId>spring-social-security</artifactId>
    <version>1.1.0.BUILD-SNAPSHOT</version>
</dependency>
<dependency>
    <groupId>org.springframework.social</groupId>
    <artifactId>spring-social-web</artifactId>
    <version>1.1.0.BUILD-SNAPSHOT</version>
</dependency>
 
<!-- Spring Social Facebook -->
<dependency>
    <groupId>org.springframework.social</groupId>
    <artifactId>spring-social-facebook</artifactId>
    <version>1.1.0.BUILD-SNAPSHOT</version>
</dependency>
 
<!-- Spring Social Twitter -->
<dependency>
    <groupId>org.springframework.social</groupId>
    <artifactId>spring-social-twitter</artifactId>
    <version>1.1.0.BUILD-SNAPSHOT</version>
</dependency>

Примечание : наше приложение имеет и другие зависимости. Например, он использует Spring Framework 3.2.4.RELEASE, Spring Data JPA 1.3.4 и Hibernate 4.2.4.Final. Эти зависимости исключены из списка зависимостей для ясности. Вы можете получить полный список зависимостей от Github .

Вы также можете прочитать следующие документы, в которых содержится дополнительная информация о зависимостях платформ, обсуждаемых в этом сообщении в блоге (Spring Security и Spring Social):

Далее мы должны создать файл свойств для свойств конфигурации нашего приложения. Давайте выясним, как это делается.

Создание файла свойств

Мы можем создать файл свойств, выполнив следующие действия:

  1. Создайте файл с именем application.properties и убедитесь, что он найден из пути к классам.
  2. Настройте соединение с базой данных.
  3. Настройте Hibernate.
  4. Добавьте идентификатор приложения Facebook и секрет приложения в файл свойств.
  5. Добавьте ключ потребителя Twitter и секрет потребителя в файл свойств.

Содержимое файла application.properties выглядит следующим образом:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
#Database Configuration
db.driver=com.mysql.jdbc.Driver
db.url=jdbc:mysql://localhost:3306/socialtwitter
db.username=socialtwitter
db.password=password
 
#Hibernate Configuration
hibernate.dialect=org.hibernate.dialect.MySQL5InnoDBDialect
hibernate.format_sql=true
hibernate.hbm2ddl.auto=validate
hibernate.ejb.naming_strategy=org.hibernate.cfg.ImprovedNamingStrategy
hibernate.show_sql=false
 
#Facebook
facebook.app.id=foo
facebook.app.secret=bar
 
#Twitter
twitter.consumer.key=foo
twitter.consumer.secret=bar

Прежде чем мы сможем настроить наше приложение, мы должны создать несколько общих компонентов. Давайте выясним, что это за компоненты, и как мы можем их создать.

Создание общих компонентов

Мы должны создать три компонента, которые используются в процессе аутентификации. Эти компоненты:

  • Мы создали класс, который содержит пользовательские данные аутентифицированного пользователя.
  • Мы должны создать класс, который реализует интерфейс UserDetailsService . Этот класс используется для загрузки информации о пользователе, когда пользователь использует форму входа в систему.
  • Мы должны создать класс, который реализует интерфейс SocialUserDetailsService . Этот класс используется для загрузки информации о пользователе, когда пользователь использует социальный вход.

Давайте продолжим и выясним, как мы можем реализовать эти классы.

Создание класса сведений о пользователе

Мы должны принять во внимание следующие требования при создании класса, который содержит пользовательские данные аутентифицированного пользователя:

  • Класс, в котором хранятся данные пользователя, который использует форму входа в систему, должен реализовывать интерфейс UserDetails .
  • Класс, в котором хранятся данные пользователя, который использует социальный вход, должен реализовывать интерфейс SocialUserDetails .

Spring Social имеет класс SocialUser, который удовлетворяет обоим этим требованиям. Однако часто мы хотим добавить информацию о приложении в наш класс пользовательских данных.

Мы можем сделать это, выполнив следующие действия:

  1. Создайте класс пользовательских данных.
  2. Расширьте класс SocialUser .
  3. Добавьте специфичные для приложения поля в созданный класс. Специфическими полями нашего примера приложения являются: id , firstName , lastName , role и socialSignInProvider .
  4. Создайте конструктор, который принимает имя пользователя, пароль и набор предоставленных прав доступа в качестве параметров. Передайте эти параметры в конструктор класса SocialUser .
  5. Создайте геттеры для конкретных областей применения.
  6. Добавьте внутренний класс построителя, который используется для создания новых объектов ExampleUserDetails .

Исходный код нашего класса пользовательских данных выглядит следующим образом:

001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
023
024
025
026
027
028
029
030
031
032
033
034
035
036
037
038
039
040
041
042
043
044
045
046
047
048
049
050
051
052
053
054
055
056
057
058
059
060
061
062
063
064
065
066
067
068
069
070
071
072
073
074
075
076
077
078
079
080
081
082
083
084
085
086
087
088
089
090
091
092
093
094
095
096
097
098
099
100
101
102
103
104
105
import org.apache.commons.lang3.builder.ToStringBuilder;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.social.security.SocialUser;
 
import java.util.Collection;
import java.util.HashSet;
import java.util.Set;
 
public class ExampleUserDetails extends SocialUser {
 
    private Long id;
 
    private String firstName;
 
    private String lastName;
 
    private Role role;
 
    private SocialMediaService socialSignInProvider;
 
    public ExampleUserDetails(String username, String password, Collection<? extends GrantedAuthority> authorities) {
        super(username, password, authorities);
    }
 
    //Getters are omitted for the sake of clarity.
 
    public static class Builder {
 
        private Long id;
 
        private String username;
 
        private String firstName;
 
        private String lastName;
 
        private String password;
 
        private Role role;
 
        private SocialMediaService socialSignInProvider;
 
        private Set<GrantedAuthority> authorities;
 
        public Builder() {
            this.authorities = new HashSet<>();
        }
 
        public Builder firstName(String firstName) {
            this.firstName = firstName;
            return this;
        }
 
        public Builder id(Long id) {
            this.id = id;
            return this;
        }
 
        public Builder lastName(String lastName) {
            this.lastName = lastName;
            return this;
        }
 
        public Builder password(String password) {
            if (password == null) {
                password = "SocialUser";
            }
 
            this.password = password;
            return this;
        }
 
        public Builder role(Role role) {
            this.role = role;
 
            SimpleGrantedAuthority authority = new SimpleGrantedAuthority(role.toString());
            this.authorities.add(authority);
 
            return this;
        }
 
        public Builder socialSignInProvider(SocialMediaService socialSignInProvider) {
            this.socialSignInProvider = socialSignInProvider;
            return this;
        }
 
        public Builder username(String username) {
            this.username = username;
            return this;
        }
 
        public ExampleUserDetails build() {
            ExampleUserDetails user = new ExampleUserDetails(username, password, authorities);
 
            user.id = id;
            user.firstName = firstName;
            user.lastName = lastName;
            user.role = role;
            user.socialSignInProvider = socialSignInProvider;
 
            return user;
        }
    }
}

Роль — это простое перечисление, которое определяет «легальные» пользовательские роли нашего примера приложения. Его исходный код выглядит следующим образом:

1
2
3
public enum Role {
    ROLE_USER
}

SocialMediaService — это перечисление, которое идентифицирует поставщика API SaaS, который использовался, когда пользователь создал учетную запись пользователя в нашем примере приложения. Его исходный код выглядит следующим образом:

1
2
3
4
public enum SocialMediaService {
    FACEBOOK,
    TWITTER
}

Реализация интерфейса UserDetailsService

Мы можем создать нашу собственную реализацию интерфейса UserDetailsService , выполнив следующие действия:

  1. Создайте класс, который реализует интерфейс UserDetailsService .
  2. Добавьте поле UserRepository в созданный класс.
  3. Создайте конструктор, который принимает UserRepository в качестве аргумента конструктора, и аннотируйте конструктор аннотацией @Autowired .
  4. Реализуйте метод loadUserByUsername (String username) интерфейса UserDetailsService . Реализация этого метода состоит из следующих этапов:
    1. Получите пользователя, вызвав метод findByEmail () интерфейса UserRepository . Этот метод возвращает пользователя, чья электронная почта совпадает с именем пользователя, указанным в качестве параметра метода.
    2. Если пользователь не найден, создайте новое исключение UsernameNotFoundException .
    3. Создайте новый объект ExampleUserDetails .
    4. Вернуть созданный объект.

Исходный код класса RepositoryUserDetailsService выглядит следующим образом:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
 
public class RepositoryUserDetailsService implements UserDetailsService {
 
    private UserRepository repository;
 
    @Autowired
    public RepositoryUserDetailsService(UserRepository repository) {
        this.repository = repository;
    }
 
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        User user = repository.findByEmail(username);
 
        if (user == null) {
            throw new UsernameNotFoundException("No user found with username: " + username);
        }
 
        ExampleUserDetails principal = ExampleUserDetails.getBuilder()
                .firstName(user.getFirstName())
                .id(user.getId())
                .lastName(user.getLastName())
                .password(user.getPassword())
                .role(user.getRole())
                .socialSignInProvider(user.getSignInProvider())
                .username(user.getEmail())
                .build();
 
        return principal;
    }
}

UserRepository — это простой репозиторий Spring Data JPA, и его исходный код выглядит следующим образом:

1
2
3
4
5
6
import org.springframework.data.jpa.repository.JpaRepository;
 
public interface UserRepository extends JpaRepository<User, Long> {
 
    public User findByEmail(String email);
}

Пользователь является единственным объектом нашего примера приложения, и он содержит информацию о пользователе, который создал учетную запись пользователя для нашего примера приложения. Соответствующая часть его исходного кода выглядит следующим образом:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
import javax.persistence.*;
 
@Entity
@Table(name = "users")
public class User extends BaseEntity<Long> {
 
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;
 
    @Column(name = "email", length = 100, nullable = false, unique = true)
    private String email;
 
    @Column(name = "first_name", length = 100,nullable = false)
    private String firstName;
 
    @Column(name = "last_name", length = 100, nullable = false)
    private String lastName;
 
    @Column(name = "password", length = 255)
    private String password;
 
    @Enumerated(EnumType.STRING)
    @Column(name = "role", length = 20, nullable = false)
    private Role role;
 
    @Enumerated(EnumType.STRING)
    @Column(name = "sign_in_provider", length = 20)
    private SocialMediaService signInProvider;
 
    public User() {
 
    }
 
    //Getters and other methods are omitted for the sake of clarity.
}

Реализация интерфейса SocialUserDetailsService

Мы можем реализовать интерфейс SocialUserDetailsService , выполнив следующие действия:

  1. Создайте класс, который реализует SocialUserDetailsService .
  2. Добавьте поле UserDetailsService в созданный класс.
  3. Создайте конструктор, который принимает объект UserDetailsService в качестве параметра конструктора, и аннотируйте конструктор аннотацией @Autowired .
  4. Реализуйте метод loadUserByUserId (String userId) объекта SocialUserDetailsInterface .
  5. Получите правильный объект UserDetails , вызвав метод loadUserByUsername (), и передайте идентификатор пользователя в качестве параметра метода. Мы можем сделать это, потому что наше приложение использует имя пользователя в качестве идентификатора пользователя.
  6. Приведите возвращенный объект к объекту SocialUserDetails и верните его.

Исходный код класса SimpleSocialUserDetailsService выглядит следующим образом:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
import org.springframework.dao.DataAccessException;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.social.security.SocialUser;
import org.springframework.social.security.SocialUserDetails;
import org.springframework.social.security.SocialUserDetailsService;
 
public class SimpleSocialUserDetailsService implements SocialUserDetailsService {
 
    private UserDetailsService userDetailsService;
 
    public SimpleSocialUserDetailsService(UserDetailsService userDetailsService) {
        this.userDetailsService = userDetailsService;
    }
 
    @Override
    public SocialUserDetails loadUserByUserId(String userId) throws UsernameNotFoundException, DataAccessException {
        UserDetails userDetails = userDetailsService.loadUserByUsername(userId);
        return (SocialUserDetails) userDetails;
    }
}

Вот и все. Теперь мы готовы настроить контекст приложения нашего приложения. Давайте выясним, как мы можем это сделать.

Настройка контекста приложения

В этом разделе описывается, как мы можем настроить контекст приложения нашего примера приложения с помощью конфигурации Java. Конфигурация контекста приложения была разделена на несколько классов конфигурации, следуя этим рекомендациям:

  1. Каждый класс конфигурации содержит конфигурацию, которая связана с определенной частью нашего примера приложения. Это облегчает поиск соответствующей конфигурации, если нам нужно что-то проверить или изменить что-то через несколько месяцев (или лет) после создания начальной конфигурации.
  2. Конфигурация была разделена таким образом, чтобы с помощью Spring Test MVC было легко писать модульные тесты для веб-слоя. Мы поговорим об этом подробнее в третьей части этого руководства, где мы напишем модульные тесты для веб-слоя нашего приложения.
  3. Конфигурация позволяет легко удалять зависимости от внешних ресурсов, когда мы пишем интеграционные тесты для нашего приложения. Мы поговорим об этом подробнее в четвертой части этого руководства, в которой описано, как мы можем написать интеграционные тесты для нашего приложения.

Примечание . Если вы хотите использовать конфигурацию XML, вы можете взглянуть на пример приложения из этого блога, который также имеет рабочую конфигурацию XML (хотя web.xml отсутствует).

Давайте начнем с настройки постоянного уровня нашего приложения.

Настройка уровня сохраняемости

Постоянный слой нашего приложения хранит информацию об учетной записи пользователя и предоставляет способ доступа к этой информации. Это важно по двум причинам:

  • Мы можем предоставить способ входа, используя имя пользователя и пароль.
  • Мы можем хранить информацию, относящуюся к приложению, и связывать эту информацию с пользователем, который использует социальный вход.

Давайте выясним, как мы можем настроить его, используя оба класса конфигурации Java.

Примечание . Уровень персистентности примера приложения использует Spring Data JPA 1.3.4. Я буду держать этот раздел настолько тонким, насколько это возможно. Если вы хотите узнать больше о Spring Data JPA, вы можете прочитать мое руководство по Spring Data JPA . Я также написал книгу о Spring Data, которая поможет вам быстро начать работу.

Мы можем настроить наш уровень персистентности, выполнив следующие действия:

  1. Создайте класс конфигурации и аннотируйте созданный класс аннотацией @Configuration .
  2. Пометьте класс аннотацией @EnableJpaRepositories и установите базовый пакет наших репозиториев Spring Data JPA.
  3. Включите управление транзакциями Spring, аннотируя класс конфигурации аннотацией @EnableTransactionManagement .
  4. Добавьте поле Environment в класс и добавьте аннотацию @Autowired . Нам не нужно настраивать файл свойств с помощью аннотации @PropertySource, поскольку он уже настроен в классе конфигурации контекста «родительского» приложения.
  5. Настройте компонент источника данных. Этот компонент обеспечивает соединения с базой данных для менеджера сущностей, но у него есть и другая цель. Он используется Spring Social, когда он сохраняет соединения с базой данных и загружает их из базы данных.
  6. Настройте компонент управления транзакциями.
  7. Сконфигурируйте фабричный компонент диспетчера сущностей.

Исходный код класса PersistenceContext выглядит следующим образом:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
import com.jolbox.bonecp.BoneCPDataSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.env.Environment;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import org.springframework.orm.jpa.JpaTransactionManager;
import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter;
import org.springframework.transaction.annotation.EnableTransactionManagement;
 
import javax.annotation.Resource;
import javax.sql.DataSource;
import java.util.Properties;
 
@Configuration
@EnableJpaRepositories(basePackages = {
        "net.petrikainulainen.spring.social.signinmvc.user.repository"
})
@EnableTransactionManagement
public class PersistenceContext {
 
    @Resource
    private Environment env;
 
    @Bean
    public DataSource dataSource() {
        BoneCPDataSource dataSource = new BoneCPDataSource();
 
        dataSource.setDriverClass(env.getRequiredProperty("db.driver"));
        dataSource.setJdbcUrl(env.getRequiredProperty("db.url"));
        dataSource.setUsername(env.getRequiredProperty("db.username"));
        dataSource.setPassword(env.getRequiredProperty("db.password"));
 
        return dataSource;
    }
 
    @Bean
    public JpaTransactionManager transactionManager() {
        JpaTransactionManager transactionManager = new JpaTransactionManager();
        transactionManager.setEntityManagerFactory(entityManagerFactory().getObject());
        return transactionManager;
    }
 
    @Bean
    public LocalContainerEntityManagerFactoryBean entityManagerFactory() {
        LocalContainerEntityManagerFactoryBean entityManagerFactoryBean = new LocalContainerEntityManagerFactoryBean();
 
        entityManagerFactoryBean.setDataSource(dataSource());
        entityManagerFactoryBean.setJpaVendorAdapter(new HibernateJpaVendorAdapter());
        entityManagerFactoryBean.setPackagesToScan({
                "net.petrikainulainen.spring.social.signinmvc.common.model",
                "net.petrikainulainen.spring.social.signinmvc.user.model"
        });
 
        Properties jpaProperties = new Properties();
        jpaProperties.put("hibernate.dialect", env.getRequiredProperty("hibernate.dialect"));
        jpaProperties.put("hibernate.format_sql", env.getRequiredProperty("hibernate.format_sql"));
        jpaProperties.put("hibernate.hbm2ddl.auto", env.getRequiredProperty("hibernate.hbm2ddl.auto"));
        jpaProperties.put("hibernate.ejb.naming_strategy", env.getRequiredProperty("hibernate.ejb.naming_strategy"));
        jpaProperties.put("hibernate.show_sql", env.getRequiredProperty("hibernate.show_sql"));
 
        entityManagerFactoryBean.setJpaProperties(jpaProperties);
 
        return entityManagerFactoryBean;
    }
}

Давайте продолжим и выясним, как мы можем создать конфигурацию безопасности для нашего приложения.

Настройка Spring Security

Spring Security предоставляет механизм аутентификации для пользователей, которые используют либо форму входа в систему, либо социальный вход, а также отвечает за авторизацию.

Мы можем настроить Spring Security, выполнив следующие действия:

  1. Создайте класс конфигурации и аннотируйте созданный класс аннотацией @Configuration .
  2. Аннотируйте класс с помощью аннотации @EnableWebSecurity . Это позволяет настроить Spring Security с помощью интерфейса WebSecurityConfigurer .
  3. Убедитесь, что наш класс конфигурации расширяет класс WebSecurityConfigurerAdapter, который является базовым классом для создания экземпляров WebSecurityConfigurer . После того, как мы это сделали, мы можем настроить конфигурацию безопасности путем переопределения методов.
  4. Добавьте поле ApplicationContext в класс конфигурации и аннотируйте поле аннотацией @Autowired .
  5. Добавьте поле UserRepository в конфигурацию и добавьте аннотацию @Autowired .
  6. Переопределите метод configure (WebSecurity web) метода класса WebSecurityConfigurerAdapter . Убедитесь, что Spring Security игнорирует запросы к статическим ресурсам, таким как файлы CSS и Javascript.
  7. Переопределите метод configure (HttpSecurity http) класса WebSecurityConfigurerAdapter и реализуйте его, выполнив следующие действия:
    1. Настройте форму входа в систему, выполнив следующие действия:
      1. Установите URL страницы входа в систему ‘/ login’.
      2. Установите URL, который обрабатывает отправку формы входа в систему, на «/ login / authenticate».
      3. Установите URL ошибки входа в систему ‘/ login? Error = bad_credentials’.
    2. Настройте функцию выхода из системы, выполнив следующие действия:
      1. Убедитесь, что cookie с именем JSESSIONID удаляется после выхода из системы.
      2. Установите URL для выхода из системы ‘/ logout’.
      3. Установите URL успеха выхода из системы ‘/ login’.
    3. Настройте авторизацию на основе URL. Основная цель этого этапа — обеспечить, чтобы анонимные пользователи имели доступ ко всем URL-адресам, связанным с процессом входа / регистрации, и защитить остальную часть нашего приложения от анонимных пользователей.
    4. Добавьте SocialAuthenticationFilter в цепочку фильтров Spring Security. Мы можем сделать это, создав новый объект SpringSocialConfigurer и убедившись, что этот объект используется при настройке Spring Security.
    5. Задайте значение поля ApplicationContext как объекта, который используется всеми экземплярами SecurityConfigurer .
  8. Настройте bean-компонент PasswordEncoder, который используется для хеширования пароля пользователя (если пользователь использует форму регистрации и входа в систему). Мы можем сделать это, создав новый объект BCryptPasswordEncoder и вернув созданный объект.
  9. Настройте компонент UserDetailsService . Мы можем сделать это путем создания нового объекта RepositoryUserDetailsService и передачи UserRepository в качестве аргумента конструктора.
  10. Переопределите метод registerAuthentication (AuthenticationManagerBuilder auth) класса WebSecurityConfigurerAdapter. Мы используем этот метод для настройки запросов аутентификации, если пользователь использует форму входа в систему. Реализуйте этот метод, выполнив следующие действия:
    1. Передайте компонент UserDetailsService в объект AuthenticationManagerBuilder, указанный в качестве параметра метода.
    2. Передайте компонент PasswordEncoder объекту AuthenticationManagerBuilder, указанному в качестве параметра метода.
  11. Настройте bean- компонент SocialUserDetailsService . Мы можем сделать это, создав новый объект SimpleSocialUserDetailsService и передав объект EJB UserDetailsService в качестве аргумента конструктора. Этот компонент загружает пользовательские данные, когда используется социальный вход.

Исходный код нашего класса конфигурации контекста приложения выглядит следующим образом:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.social.security.SocialUserDetailsService;
import org.springframework.social.security.SpringSocialConfigurer;
 
@Configuration
@EnableWebSecurity
public class SecurityContext extends WebSecurityConfigurerAdapter {
 
    @Autowired
    private ApplicationContext context;
 
    @Autowired
    private UserRepository userRepository;
 
    @Override
    public void configure(WebSecurity web) throws Exception {
        web
                //Spring Security ignores request to static resources such as CSS or JS files.
                .ignoring()
                    .antMatchers("/static/**");
    }
 
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                //Configures form login
                .formLogin()
                    .loginPage("/login")
                    .loginProcessingUrl("/login/authenticate")
                    .failureUrl("/login?error=bad_credentials")
                //Configures the logout function
                .and()
                    .logout()
                        .deleteCookies("JSESSIONID")
                        .logoutUrl("/logout")
                        .logoutSuccessUrl("/login")
                //Configures url based authorization
                .and()
                    .authorizeRequests()
                        //Anyone can access the urls
                        .antMatchers(
                                "/auth/**",
                                "/login",
                                "/signin/**",
                                "/signup/**",
                                "/user/register/**"
                        ).permitAll()
                        //The rest of the our application is protected.
                        .antMatchers("/**").hasRole("USER")
                //Adds the SocialAuthenticationFilter to Spring Security's filter chain.
                .and()
                    .apply(new SpringSocialConfigurer())
                .and()
                    .setSharedObject(ApplicationContext.class, context);
    }
 
    @Override
    protected void registerAuthentication(AuthenticationManagerBuilder auth) throws Exception {
        auth
                .userDetailsService(userDetailsService())
                .passwordEncoder(passwordEncoder());
    }
 
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder(10);
    }
 
    @Bean
    public SocialUserDetailsService socialUserDetailsService() {
        return new SimpleSocialUserDetailsService(userDetailsService());
    }
 
    @Bean
    public UserDetailsService userDetailsService() {
        return new RepositoryUserDetailsService(userRepository);
    }
}

Давайте продолжим и узнаем, как мы можем настроить Spring Social.

Настройка Spring Social

Spring Social обеспечивает интеграцию с провайдерами SaaS API, такими как Facebook и Twitter. Мы можем настроить Spring Social, выполнив следующие действия:

  1. Создайте класс конфигурации контекста приложения, который реализует интерфейс SocialConfigurer, и аннотируйте созданный класс аннотацией @Configuration . Интерфейс SocialConfigurer объявляет методы обратного вызова, которые можно использовать для настройки Spring Social.
  2. Аннотируйте класс с помощью аннотации @EnableSocial . Это включает Spring Social и импортирует класс конфигурации SocialConfiguration .
  3. Пометьте класс аннотацией @Profile и установите строку application в качестве значения. Это гарантирует, что этот класс конфигурации контекста приложения используется только тогда, когда активным профилем Spring является «приложение». Это важно, потому что это дает нам возможность писать интеграционные тесты для нашего приложения, не полагаясь на Facebook или Twitter.
  4. Добавьте поле DataSource в класс конфигурации и добавьте аннотацию @Autowired .
  5. Добавьте метод addConnectionFactories () интерфейса SocialConfigurer в созданный класс конфигурации. Этот метод принимает два параметра метода, которые описаны ниже:
    1. Первый параметр — это объект ConnectionFactoryConfigurer, который можно использовать для регистрации фабрик соединений.
    2. Второй параметр — это объект Environment, представляющий среду, в которой работает наше примерное приложение.
  6. Реализуйте метод addConnectionFactories () , выполнив следующие действия:
    1. Создайте новый объект TwitterConnectionFactory и передайте ключ потребителя и секрет потребителя в качестве аргументов конструктора.
    2. Зарегистрируйте созданный объект TwitterConnectionFactory , вызвав метод addConnectionFactory () интерфейса ConnectionFactoryConfigurer . Передайте созданный объект TwitterConnectionFactory в качестве параметра метода.
    3. Создайте новый объект FacebookConnectionFactory и передайте идентификатор приложения и секрет приложения в качестве аргументов конструктора.
    4. Зарегистрируйте созданный объект FacebookConnectionFactory , вызвав метод addConnectionFactory интерфейса ConnectionFactoryConfigurer . Передайте созданный объект FacebookConnectionFactory в качестве параметра метода.
  7. Добавьте метод getUserIdSource () интерфейса SocialConfigurer к созданному классу. Объект UserIdSource, возвращаемый этим методом, отвечает за определение правильного идентификатора учетной записи пользователя. Поскольку наше примерное приложение использует имя пользователя в качестве идентификатора учетной записи, мы должны реализовать этот метод, возвращая новый объект AuthenticationNameUserIdSource .
  8. Добавьте метод getUsersConnectionRepository () интерфейса SocialConfigurer к созданному классу. Этот метод принимает объект ConnectionFactoryLocator в качестве параметра метода и возвращает объект UsersConnectionRepository .
  9. Реализуйте метод getUsersConnectionRepository () , выполнив следующие действия:
    1. Создайте новый объект JdbcUsersConnectionRepository и передайте следующие объекты в качестве аргументов конструктора:
      1. Первый аргумент — это объект DataSource . Мы передаем значение поля dataSource в качестве первого параметра метода.
      2. Второй аргумент — это объект ConnectionFactoryLocator . Мы передаем значение параметра метода connectionFactoryLocator в качестве второго параметра метода.
      3. Третий параметр — это объект TextEncryptor, который шифрует данные авторизации соединения, установленного между провайдером API SaaS и нашим приложением. Мы создаем этот объект, вызывая метод noOpText () класса Encryptors . Это означает, что наше примерное приложение хранит эти данные в виде открытого текста. Это удобно на этапе разработки, но мы не должны использовать его в производстве .
    2. Вернуть созданный объект.
  10. Настройте компонент ConnectController . Метод, который настраивает этот компонент, имеет два параметра. Первый параметр — это bean-компонент ConnectionFactoryLocator . Второй параметр — это используемый компонент ConnectionRepository . Передайте эти параметры в качестве аргументов конструктора при создании нового объекта ConnectController .

Исходный код нашего класса конфигурации выглядит следующим образом:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.*;
import org.springframework.core.env.Environment;
import org.springframework.security.crypto.encrypt.Encryptors;
import org.springframework.social.UserIdSource;
import org.springframework.social.config.annotation.ConnectionFactoryConfigurer;
import org.springframework.social.config.annotation.EnableSocial;
import org.springframework.social.config.annotation.SocialConfigurer;
import org.springframework.social.connect.ConnectionFactoryLocator;
import org.springframework.social.connect.ConnectionRepository;
import org.springframework.social.connect.UsersConnectionRepository;
import org.springframework.social.connect.jdbc.JdbcUsersConnectionRepository;
import org.springframework.social.connect.web.ConnectController;
import org.springframework.social.facebook.connect.FacebookConnectionFactory;
import org.springframework.social.security.AuthenticationNameUserIdSource;
import org.springframework.social.twitter.connect.TwitterConnectionFactory;
 
import javax.sql.DataSource;
 
@Configuration
@EnableSocial
@Profile("application")
public class SocialContext implements SocialConfigurer {
 
    @Autowired
    private DataSource dataSource;
 
    @Override
    public void addConnectionFactories(ConnectionFactoryConfigurer cfConfig, Environment env) {
        cfConfig.addConnectionFactory(new TwitterConnectionFactory(
                env.getProperty("twitter.consumer.key"),
                env.getProperty("twitter.consumer.secret")
        ));
        cfConfig.addConnectionFactory(new FacebookConnectionFactory(
                env.getProperty("facebook.app.id"),
                env.getProperty("facebook.app.secret")
        ));
    }
 
    @Override
    public UserIdSource getUserIdSource() {
        return new AuthenticationNameUserIdSource();
    }
 
    @Override
    public UsersConnectionRepository getUsersConnectionRepository(ConnectionFactoryLocator connectionFactoryLocator) {
        return new JdbcUsersConnectionRepository(
                dataSource,
                connectionFactoryLocator,
                Encryptors.noOpText()
        );
    }
 
    @Bean
    public ConnectController connectController(ConnectionFactoryLocator connectionFactoryLocator, ConnectionRepository connectionRepository) {
        return new ConnectController(connectionFactoryLocator, connectionRepository);
    }
}

Наш следующий шаг — настроить веб-слой нашего приложения. Давай приступим к работе.

Настройка веб-слоя

Мы можем настроить веб-слой нашего приложения, выполнив следующие действия:

  1. Создайте класс конфигурации, выполнив следующие действия:
    1. Расширьте класс WebMvcConfigurerAdapter .
    2. Аннотируйте созданный класс с помощью аннотации @Configuration .
  2. Убедитесь, что все классы контроллеров найдены, аннотируя класс аннотацией @ComponentScan и устанавливая базовые пакеты наших контроллеров.
  3. Включите управляемый аннотациями веб-mvc, аннотируя класс аннотацией @EnableWebMvc .
  4. Убедитесь, что статические ресурсы обслуживаются сервлетом контейнера по умолчанию.
    1. Настройте статические ресурсы, переопределив метод addResourceHandlers () класса WebMvcConfigurerAdapter .
    2. Убедитесь, что запросы к статическим ресурсам передаются в сервлет по умолчанию контейнера. Это делается путем переопределения метода configureDefaultServletHandling () класса WebMvcConfigurerAdapter .
  5. Настройте компонент разрешения исключений .
  6. Настройте bean- компонент ViewResolver .

Исходный код класса WebAppContext выглядит следующим образом:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.ViewResolver;
import org.springframework.web.servlet.config.annotation.DefaultServletHandlerConfigurer;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
import org.springframework.web.servlet.handler.SimpleMappingExceptionResolver;
import org.springframework.web.servlet.view.InternalResourceViewResolver;
import org.springframework.web.servlet.view.JstlView;
 
import java.util.Properties;
 
@Configuration
@ComponentScan(basePackages = {
        "net.petrikainulainen.spring.social.signinmvc.common.controller",
        "net.petrikainulainen.spring.social.signinmvc.security.controller",
        "net.petrikainulainen.spring.social.signinmvc.user.controller"
})
@EnableWebMvc
public class WebAppContext extends WebMvcConfigurerAdapter {
 
    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        registry.addResourceHandler("/static/**").addResourceLocations("/static/");
    }
 
    @Override
    public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) {
        configurer.enable();
    }
 
    @Bean
    public SimpleMappingExceptionResolver exceptionResolver() {
        SimpleMappingExceptionResolver exceptionResolver = new SimpleMappingExceptionResolver();
 
        Properties exceptionMappings = new Properties();
 
        exceptionMappings.put("java.lang.Exception", "error/error");
        exceptionMappings.put("java.lang.RuntimeException", "error/error");
 
        exceptionResolver.setExceptionMappings(exceptionMappings);
 
        Properties statusCodes = new Properties();
 
        statusCodes.put("error/404", "404");
        statusCodes.put("error/error", "500");
 
        exceptionResolver.setStatusCodes(statusCodes);
 
        return exceptionResolver;
    }
 
    @Bean
    public ViewResolver viewResolver() {
        InternalResourceViewResolver viewResolver = new InternalResourceViewResolver();
 
        viewResolver.setViewClass(JstlView.class);
        viewResolver.setPrefix("/WEB-INF/jsp/");
        viewResolver.setSuffix(".jsp");
 
        return viewResolver;
    }
}

Давайте выясним, как мы можем связать все это вместе и создадим класс конфигурации родительского контекста приложения для нашего приложения.

Все вместе

Последний класс конфигурации контекста приложения имеет три обязанности:

  1. Он настраивает основные компоненты, используемые в нашем примере приложения.
  2. Это гарантирует, что классы обслуживания нашего приложения найдены во время сканирования пути к классам.
  3. Это корневой класс конфигурации приложения.

Мы можем создать этот класс конфигурации, выполнив следующие действия:

  1. Создайте класс конфигурации и аннотируйте созданный класс аннотацией @Configuration .
  2. Убедитесь, что наши классы обслуживания обнаружены во время сканирования компонента, пометив класс аннотацией @ComponentScan и установив базовый пакет наших услуг.
  3. Импортируйте другие классы конфигурации контекста приложения, пометив класс аннотацией @Import .
  4. Пометьте класс аннотацией @PropertySource и настройте его для поиска файла свойств с именем application.properties из пути к классам. Это обеспечивает доступ к свойствам конфигурации в импортированных классах конфигурации контекста приложения.
  5. Настройте компонент MessageSource .
  6. Настройте bean-компонент PropertyPlaceHolderConfigurer .

Исходный код класса ExampleApplicationContext выглядит следующим образом:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import org.springframework.context.MessageSource;
import org.springframework.context.annotation.*;
import org.springframework.context.support.PropertySourcesPlaceholderConfigurer;
import org.springframework.context.support.ResourceBundleMessageSource;
 
@Configuration
@ComponentScan(basePackages = {
        "net.petrikainulainen.spring.social.signinmvc.user.service"
})
@Import({WebAppContext.class, PersistenceContext.class, SecurityContext.class, SocialContext.class})
@PropertySource("classpath:application.properties")
public class ExampleApplicationContext {
 
    @Bean
    public MessageSource messageSource() {
        ResourceBundleMessageSource messageSource = new ResourceBundleMessageSource();
 
        messageSource.setBasename("i18n/messages");
        messageSource.setUseCodeAsDefaultMessage(true);
 
        return messageSource;
    }
 
    @Bean
    public PropertySourcesPlaceholderConfigurer propertyPlaceHolderConfigurer() {
        return new PropertySourcesPlaceholderConfigurer();
    }
}

Теперь мы настроили контекст приложения нашего примера приложения. Однако нам все еще нужно настроить наше веб-приложение. Давайте посмотрим, как мы можем сделать это с помощью конфигурации Java.

Настройка веб-приложения

Наш последний шаг — настройка нашего примера приложения. Мы можем сделать это без web.xml, если наше приложение развернуто в контейнере, совместимом с сервлетом 3.0.

Мы можем настроить веб-приложение, выполнив следующие действия:

  1. Создайте класс, который реализует интерфейс WebApplicationInitializer .
  2. Настройка нашего приложения путем переопределения OnStartup () метод WebApplicationInitializer интерфейса. Мы можем реализовать этот метод, выполнив следующие действия:
      1. Создайте корневой контекст приложения и зарегистрируйте класс ExampleApplicationContext в созданном корневом контексте.
      2. Настройте диспетчерский сервлет .

    C) Настройте фильтр кодировки символов .
    D) Настройте цепочку фильтров Spring Security .
    E) Настройте Sitemesh .
    F) Добавьте прослушиватель загрузчика контекста в контекст сервлета.

Исходный код класса ExampleApplicationConfig выглядит следующим образом:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
import org.sitemesh.config.ConfigurableSiteMeshFilter;
import org.springframework.web.WebApplicationInitializer;
import org.springframework.web.context.ContextLoaderListener;
import org.springframework.web.context.support.AnnotationConfigWebApplicationContext;
import org.springframework.web.context.support.XmlWebApplicationContext;
import org.springframework.web.filter.CharacterEncodingFilter;
import org.springframework.web.filter.DelegatingFilterProxy;
import org.springframework.web.servlet.DispatcherServlet;
 
import javax.servlet.*;
import java.util.EnumSet;
 
public class ExampleApplicationConfig implements WebApplicationInitializer {
 
    @Override
    public void onStartup(ServletContext servletContext) throws ServletException {
        AnnotationConfigWebApplicationContext rootContext = new AnnotationConfigWebApplicationContext();
        rootContext.register(ExampleApplicationContext.class);
 
        ServletRegistration.Dynamic dispatcher = servletContext.addServlet("dispatcher", new DispatcherServlet(rootContext));
        dispatcher.setLoadOnStartup(1);
        dispatcher.addMapping("/");
 
        EnumSet<DispatcherType> dispatcherTypes = EnumSet.of(DispatcherType.REQUEST, DispatcherType.FORWARD);
 
        CharacterEncodingFilter characterEncodingFilter = new CharacterEncodingFilter();
        characterEncodingFilter.setEncoding("UTF-8");
        characterEncodingFilter.setForceEncoding(true);
 
        FilterRegistration.Dynamic characterEncoding = servletContext.addFilter("characterEncoding", characterEncodingFilter);
        characterEncoding.addMappingForUrlPatterns(dispatcherTypes, true, "/*");
 
        FilterRegistration.Dynamic security = servletContext.addFilter("springSecurityFilterChain", new DelegatingFilterProxy());
        security.addMappingForUrlPatterns(dispatcherTypes, true, "/*");
 
        FilterRegistration.Dynamic sitemesh = servletContext.addFilter("sitemesh", new ConfigurableSiteMeshFilter());
        sitemesh.addMappingForUrlPatterns(dispatcherTypes, true, "*.jsp");
 
        servletContext.addListener(new ContextLoaderListener(rootContext));
    }
}

Что дальше?

Теперь мы успешно настроили наш пример приложения с помощью конфигурации Java. Этот урок научил нас двум вещам:

  • Мы узнали, как мы можем реализовать компоненты, необходимые для Spring Security и Spring Social.
  • Мы научились интегрировать Spring Security и Spring Social с помощью конфигурации Java.

Следующая часть этого руководства описывает, как мы можем добавить функции регистрации и аутентификации в нашем примере приложения.

PS Как всегда, пример приложения этого поста доступен на Github .