vlambda博客
学习文章列表

读书笔记《spring-security-third-edition》使用 Spring 数据进行身份验证

使用 Spring 数据进行身份验证

在上一章中,我们介绍了如何利用 Spring Security 的内置 JDBC 支持。在本章中,我们将研究 Spring Data 项目,以及如何利用 JPA 对关系数据库执行身份验证。我们还将探讨如何使用 MongoDB 对文档数据库执行身份验证。本章的示例代码基于 Chapter 4 中的 Spring Security 设置, 基于 JDBC 的身份验证,并且已更新以重构对 SQL 的需求并使用 ORM 进行所有数据库交互。

在本章的课程中,我们将讨论以下主题:

  • Some of the basic concepts related to the Spring Data project
  • Utilizing Spring Data JPA to authenticate against a relational database
  • Utilizing Spring Data MongoDB to authenticate against a document database
  • How to customize Spring Security for more flexibility when dealing with Spring Data integration
  • Understanding the Spring Data project

Spring Data 项目的使命是为数据访问提供熟悉且一致的基于 Spring 的编程模型,同时仍保留底层数据提供者的特殊特征。

以下只是这个 Spring Data 项目中的一些强大功能:

  • Powerful repository and custom object-mapping abstractions
  • Dynamic query derivation from repository method names
  • Implementation of domain base classes, providing basic properties
  • Support for transparent auditing (created and last changed)
  • The ability to integrate custom repository code
  • Easy Spring integration via Java-based configuration and custom XML namespaces
  • Advanced integration with Spring MVC controllers
  • Experimental support for cross-store persistence

该项目简化了数据访问技术、关系和非关系数据库、map-reduce 框架和基于云的数据服务的使用。这个总括项目包含许多特定于给定数据库的子项目。这些项目是通过与许多支持这些令人兴奋的技术的公司和开发人员合作开发的。还有许多社区维护的模块和其他相关模块,包括 JDBC Support 和 Apache Hadoop。

下表描述了构成 Spring Data 项目的主要模块:

模块

说明

Spring Data Commons

将核心 Spring 概念应用于所有 Spring Data 项目

春季数据宝石火

提供从 Spring 应用程序对 Gemfire 的轻松配置和访问

春季数据 JPA

使实现基于 JPA 的存储库变得容易

Spring 数据键值

基于地图的存储库和 SPI,可以轻松构建用于键值存储的 Spring Data 模块

春季数据 LDAP

为 Spring LDAP 提供 Spring Data repository 支持

春季数据 MongoDB

MongoDB 的基于 Spring 的对象文档支持和存储库

春季数据休息

将 Spring Data 存储库导出为超媒体驱动的 RESTful 资源

Spring Data Redis

提供从 Spring 应用程序对 Redis 的轻松配置和访问

Apache Cassandra 的 Spring 数据

Apache Cassandra 的 Spring Data 模块

Apache Solr 的 Spring 数据

Apache Solr 的 Spring Data 模块

春季数据 JPA

Spring Data JPA 项目旨在通过减少实际需要的工作量来显着改进数据访问层的 ORM 实现。开发人员只需要编写存储库接口,包括自定义查找器方法,Spring 会自动提供实现。

以下只是 Spring Data JPA 项目特有的一些强大功能:

  • Sophisticated support for building repositories based on Spring and JPA
  • Support for Querydsl predicates, and thus, type-safe JPA queries
  • Transparent auditing of domain classes
  • Pagination support, dynamic query execution, and the ability to integrate custom data access code
  • Validation of @Query annotated queries at bootstrap time
  • Support for XML based entity mapping
  • The JavaConfig based repository configuration by introducing @EnableJpaRepositories

更新我们的依赖

我们已经包含了本章所需的所有依赖项,因此您无需对 build.gradle 文件进行任何更新。但是,如果您只是在自己的应用程序中添加 Spring Data JPA 支持,则需要在 build.gradlespring-boot-starter-data-jpa 作为依赖项> 文件,如下:

    //build.gradle

dependencies {
...
// REMOVE: compile('org.springframework.boot:spring-boot-starter-jdbc')
compile('org.springframework.boot:spring-boot-starter-data-jpa')
...
}

请注意,我们删除了 spring-boot-starter-jdbc 依赖项。 spring-boot-starter-data-jpa 依赖项将包含将域对象连接到使用 JPA 的嵌入式数据库所需的所有依赖项。

更新 JBCP 日历以使用 Spring Data JPA

为了熟悉 Spring Data,我们将首先使用 Spring Data JPA starter 将 JBCP 日历 SQL 转换为利用 ORM。

创建和维护 SQL 可能非常乏味。在前面的章节中,当我们想在数据库中创建一个新的 CalendarUser 表时,我们必须创建相当多的样板代码,如下所示:

    //src/main/java/com/packtpub/springsecurity/
dataaccess/JdbcCalendarUserDao.java

public int createUser(final CalendarUser userToAdd) {
if (userToAdd == null) {
throw new IllegalArgumentException("userToAdd cannot be null");
}
if (userToAdd.getId() != null) {
throw new IllegalArgumentException("userToAdd.getId() must be
null when creating a
"+CalendarUser.class.getName());
}
KeyHoldener keyHolder = new GeratedKeyHolder();
this.jdbcOperations.update(new PreparedStatementCreator() {
public PreparedStatement createPreparedStatement
(Connection connection)
throws SQLException {
PreparedStatement ps = connection.prepareStatement("insert into
calendar_users (email, password, first_name, last_name)
values (?, ?, ?, ?)", new String[] {
"id" });
ps.setString(1, userToAdd.getEmail());
ps.setString(2, userToAdd.getPassword());
ps.setString(3, userToAdd.getFirstName());
ps.setString(4, userToAdd.getLastName());
return ps;
}
}, keyHolder);
return keyHolder.getKey().intValue();
}

要创建这个对象,我们在技术上需要 12 行代码来执行操作。

现在,使用 Spring Data JPA,可以将相同的实现简化为以下代码片段:

    //src/main/java/com/packtpub/springsecurity/dataaccess/JpaCalendarUserDao.java

public int createUser(final CalendarUser userToAdd) {
if (userToAdd == null) {
throw new IllegalArgumentException("userToAdd cannot be null");
}
if (userToAdd.getId() != null) {
throw new IllegalArgumentException("userToAdd.getId()
must be null when creating a "+CalendarUser.class.getName());
}
Set<Role> roles = new HashSet<>();
roles.add(roleRepository.findOne(0));
userToAdd.setRoles(roles);
CalendarUser result = repository.save(userToAdd);
repository.flush();
return result.getId();
}

现在,要使用 JPA 创建这个对象,从技术上讲,我们需要五行代码来执行操作。我们现在需要不到一半的代码来执行相同的操作。

重新配置数据库配置

首先,我们将转换当前的 JBCP 日历项目。让我们从重新配置数据库开始。

我们可以从删除 DataSourceConfig.java 文件开始,因为我们将利用 Spring Boot 对嵌入式 H2 数据库的内置支持。我们还需要删除 JavaConfig.java 文件中对 DataSourceConfig.java 的引用,因为当前存在对 JavaConfig.java 的引用在 @Import 注释内。

初始化数据库

我们现在可以删除 src/main/resources/database 目录和该目录中的所有内容。该目录包含几个.sql文件,我们将它们合并移动到下一步:

现在,我们需要创建一个包含种子数据的 data.sql 文件,如下所示:

    //src/main/resources/data.sql:
  • Take a look at the following SQL statement, depicting the password for user1:
        insert into calendar_users(id,username,email,password,
first_name,last_name)
values(0,'[email protected]','[email protected]',
'$2a$04$qr7RWyqOnWWC1nwotUW1nOe1RD5.
mKJVHK16WZy6v49pymu1WDHmi','User','1');
  • Take a look at the following SQL statement, depicting the password for admin1 :
        insert into calendar_users(id,username,email,password,
first_name,last_name)
values (1,'[email protected]','[email protected]',
'$2a$04$0CF/Gsquxlel3fWq5Ic/ZOGDCaXbMfXYiXsviTNMQofWRXhvJH3IK',
'Admin','1');
  • Take a look at the following SQL statement, depicting the password for user2:
        insert into calendar_users(id,username,email,password,first_name,
last_name)
values (2,'[email protected]','[email protected]',
'$2a$04$PiVhNPAxunf0Q4IMbVeNIuH4M4ecySWHihyrclxW..PLArjLbg8CC',
'User2','2');
  • Take a look at the following SQL statement, depicting the user roles:
        insert into role(id, name) values (0, 'ROLE_USER');
insert into role(id, name) values (1, 'ROLE_ADMIN');
  • Here, user1 has one role:
        insert into user_role(user_id,role_id) values (0, 0);
  • Here, admin1 has two roles:
        insert into user_role(user_id,role_id) values (1, 0);
insert into user_role(user_id,role_id) values (1, 1);
  • Take a look at the following SQL statement, depicting events:
        insert into events (id,when,summary,description,owner,attendee)
values (100,'2017-07-03 20:30:00','Birthday Party',
'This is going to be a great birthday',0,1);
insert into events (id,when,summary,description,owner,attendee)
values (101,'2017-12-23 13:00:00','Conference Call','Call with
the client',2,0);
insert into events (id,when,summary,description,owner,attendee)
values (102,'2017-09-14 11:30:00','Vacation',
'Paragliding in Greece',1,2);

现在,我们可以更新应用程序属性以在 src/main/resources/application.yml 文件中定义我们的嵌入式数据库属性,如下所示:

    # Embedded Database
datasource:
url: jdbc:h2:mem:dataSource;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE
driverClassName: org.h2.Driver
username: sa
password:
continue-on-error: true
jpa:
database-platform: org.hibernate.dialect.H2Dialect
show-sql: true
hibernate:
ddl-auto: create-drop

至此,我们已经移除了旧的数据库配置并添加了新的配置。应用程序此时将无法工作,但在我们继续进行下一步转换之前,这仍然可以被视为一个标记点​​。

您的代码现在应该类似于 calendar05.01-calendar

从 SQL 重构为 ORM

从 SQL 重构为 ORM 实现比您想象的要简单。大多数重构都涉及以 SQL 的形式删除多余的代码。在下一节中,我们将把 SQL 实现重构为 JPA 实现。

为了让 JPA 将我们的域对象映射到我们的数据库,我们需要对我们的域对象执行一些映射。

使用 JPA 映射域对象

查看以下步骤以了解映射域对象:

  1. Let's begin by mapping our Event.java file so all the domain objects will use JPA, as follows:
//src/main/java/com/packtpub/springsecurity/domain/Event.java

import javax.persistence.*;
@Entity
@Table(name = "events")
public class Event implements Serializable{
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Integer id;
@NotEmpty(message = "Summary is required")
private String summary;
@NotEmpty(message = "Description is required")
private String description;
@NotNull(message = "When is required")
private Calendar when;
@NotNull(message = "Owner is required")
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name="owner", referencedColumnName="id")
private CalendarUser owner;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name="attendee", referencedColumnName="id")
private CalendarUser attendee;
  1. We need to create a Role.java file with the following contents:
//src/main/java/com/packtpub/springsecurity/domain/Role.java

import javax.persistence.*;
@Entity
@Table(name = "role")
public class Role implements Serializable {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Integer id;
private String name;
@ManyToMany(fetch = FetchType.EAGER, mappedBy = "roles")
private Set<CalendarUser> users;
  1. The Role object will be used to map authorities to our CalendarUser table. Let's map our CalendarUser.java file, now that we have a Role.java file:
//src/main/java/com/packtpub/springsecurity/domain/CalendarUser.java

import javax.persistence.*;
import java.io.Serializable;
import java.util.Set;
@Entity
@Table(name = "calendar_users")
public class CalendarUser implements Serializable {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Integer id;
private String firstName;
private String lastName;
private String email;
private String password;
@ManyToMany(fetch = FetchType.EAGER)
@JoinTable(name = "user_role",
joinColumns = @JoinColumn(name = "user_id"),
inverseJoinColumns = @JoinColumn(name = "role_id"))
private Set<Role> roles;

至此,我们已经将我们的域对象映射到所需的 JPA 注释,包括 @Entity@Table 来定义 RDBMS 位置,以及结构、引用和关联映射注释。

应用程序此时将无法工作,但在我们继续进行下一步转换之前,这仍然可以被视为一个标记点​​。

您应该从 chapter05.02-calendar 的源代码开始。

Spring 数据存储库

我们现在将添加 Spring Data 所需的接口,通过执行以下步骤将我们所需的 CRUD 操作映射到我们的嵌入式数据库:

  1. We begin by adding a new interface in a new package, which will be com.packtpub.springsecurity.repository. The new file will be called CalendarUserRepository.java, as follows:
        //com/packtpub/springsecurity/repository/CalendarUserRepository.java

package com.packtpub.springsecurity.repository;
import com.packtpub.springsecurity.domain.CalendarUser;
import org.springframework.data.jpa.repository.JpaRepository;

public interface CalendarUserRepository
extends JpaRepository<CalendarUser, Integer> {
CalendarUser findByEmail(String email);
}

这将允许在我们的 CalendarUser find()save()delete() kbd> 对象。

  1. We can now continue by adding a new interface in the same repository package, which will be com.packtpub.springsecurity.repository, and the new file will be called EventRepository.java:
            //com/packtpub/springsecurity/repository/EventRepository.java

package com.packtpub.springsecurity.repository;
import com.packtpub.springsecurity.domain.Event;
import org.springframework.data.jpa.repository.JpaRepository;

public interface EventRepository extends JpaRepository<Event,
Integer> {}

这将允许对我们的 Event 进行标准 CRUD 操作,例如 find()save()delete() kbd> 对象。

  1. Finally, we will be adding a new interface in the same repository package, which will be com.packtpub.springsecurity.repository, and the new file will be called RoleRepository.java. This CrudRepository interface will be used to manage the Role object for our security roles associated with a given CalendarUser:
            //com/packtpub/springsecurity/repository/

package com.packtpub.springsecurity.repository;
import com.packtpub.springsecurity.domain.Event;
import org.springframework.data.jpa.repository.JpaRepository;

public interface RoleRepository extends JpaRepository<Role,
Integer> {}

这将允许在我们的 Role find()save()delete() kbd> 对象。

数据访问对象

我们需要用新名称重构 JdbcEventDao.java 文件,JpaEventDao.java,因此我们可以用新的 Spring Data 代码替换 JDBC SQL 代码。让我们看一下以下步骤:

  1. Specifically, we need to add the new EventRepository interface, and replace the SQL code with the new ORM repository, as shown in the following code:
        //com/packtpub/springsecurity/dataaccess/JpaEventDao.java

package com.packtpub.springsecurity.dataaccess;
import com.packtpub.springsecurity.domain.CalendarUser;
import com.packtpub.springsecurity.domain.Event;
import com.packtpub.springsecurity.repository.EventRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Example;
import org.springframework.stereotype.Repository;
import org.springframework.transaction.annotation.Transactional;
...
@Repository
public class JpaEventDao implements EventDao {
private EventRepository repository;
@Autowired
public JpaEventDao(EventRepository repository) {
if (repository == null) {
throw new IllegalArgumentException("repository
cannot be null");
}
this.repository = repository;
}
@Override
@Transactional(readOnly = true)
public Event getEvent(int eventId) {
return repository.findOne(eventId);
}
@Override
public int createEvent(final Event event) {
...
final Calendar when = event.getWhen();
if(when == null) {
throw new IllegalArgumentException("event.getWhen()
cannot be null");
}
Event newEvent = repository.save(event);
...
}
@Override
@Transactional(readOnly = true)
public List<Event> findForUser(final int userId) {
Event example = new Event();
CalendarUser cu = new CalendarUser();
cu.setId(userId);
example.setOwner(cu);
return repository.findAll(Example.of(example));
}
@Override
@Transactional(readOnly = true)
public List<Event> getEvents() {
return repository.findAll();
}
}
  1. At this point, we need to refactor the DAO classes to support the new CrudRepository interfaces we have created. Let's begin with refactoring the JdbcCalendarUserDao.java file. First, we can rename the file to JpaCalendarUserDao.java to indicate that this is using JPA, and not standard JDBC:
        //com/packtpub/springsecurity/dataaccess/JpaCalendarUserDao.java

package com.packtpub.springsecurity.dataaccess;
... omitted for brevity ...
@Repository
public class JpaCalendarUserDao
implements CalendarUserDao {
private CalendarUserRepository userRepository;
private RoleRepository roleRepository;
@Autowired
public JpaCalendarUserDao(CalendarUserRepository repository,
RoleRepository roleRepository) {
if (repository == null) {
throw new IllegalArgumentException("repository
cannot be null");
}
if (roleRepository == null) {
throw new IllegalArgumentException("roleRepository
cannot be null");
}
this. userRepository = repository;
this.roleRepository = roleRepository;
}
@Override
@Transactional(readOnly = true)
public CalendarUser getUser(final int id) {
return userRepository.findOne(id);
}
@Override
@Transactional(readOnly = true)
public CalendarUser findUserByEmail(final String email) {
if (email == null) {
throw new IllegalArgumentException
("email cannot be null");
}
try {
return userRepository.findByEmail(email);
} catch (EmptyResultDataAccessException notFound) {
return null;
}
}
@Override
@Transactional(readOnly = true)
public List<CalendarUser> findUsersByEmail(final String email) {
if (email == null) {
throw new IllegalArgumentException("email
cannot be null");
}
if ("".equals(email)) {
throw new IllegalArgumentException("email
cannot be empty string");
}
return userRepository.findAll();
}
@Override
public int createUser(final CalendarUser userToAdd) {
if (userToAdd == null) {
throw new IllegalArgumentException("userToAdd
cannot be null");
}
if (userToAdd.getId() != null) {
throw new IllegalArgumentException("userToAdd.getId()
must be null when creating a "+
CalendarUser.class.getName());
}
Set<Role> roles = new HashSet<>();
roles.add(roleRepository.findOne(0));
userToAdd.setRoles(roles);
CalendarUser result = userRepository.save(userToAdd);
userRepository.flush();
return result.getId();
}
}

正如您在前面的代码中看到的,利用 JPA 所需数量的更新片段比 JDBC 所需的代码少得多。这意味着我们可以专注于业务逻辑而不用担心管道。

  1. We continue by refactoring the JdbcEventDao.java file. First, we can rename the file to JpaEventDao.java, to indicate that this is using JPA and not standard JDBC, as follows:
//com/packtpub/springsecurity/dataaccess/JpaEventDao.java

package com.packtpub.springsecurity.dataaccess;
... omitted for brevity ...
@Repository
public class JpaEventDao implements EventDao {
private EventRepository repository;
@Autowired
public JpaEventDao(EventRepository repository) {
if (repository == null) {
throw new IllegalArgumentException("repository
cannot be null");
}
this.repository = repository;
}
@Override
@Transactional(readOnly = true)
public Event getEvent(int eventId) {
return repository.findOne(eventId);
}
@Override
public int createEvent(final Event event) {
if (event == null) {
throw new IllegalArgumentException("event cannot be null");
}
if (event.getId() != null) {
throw new IllegalArgumentException
("event.getId() must be null when creating a new Message");
}
final CalendarUser owner = event.getOwner();
if (owner == null) {
throw new IllegalArgumentException("event.getOwner()
cannot be null");
}
final CalendarUser attendee = event.getAttendee();
if (attendee == null) {
throw new IllegalArgumentException("attendee.getOwner()
cannot be null");
}
final Calendar when = event.getWhen();
if(when == null) {
throw new IllegalArgumentException
("event.getWhen()cannot be null");
}
Event newEvent = repository.save(event);
return newEvent.getId();
}
@Override
@Transactional(readOnly = true)
public List<Event> findForUser(final int userId) {
Event example = new Event();
CalendarUser cu = new CalendarUser();
cu.setId(userId);
example.setOwner(cu);
return repository.findAll(Example.of(example));
}
@Override
@Transactional(readOnly = true)
public List<Event> getEvents() {
return repository.findAll();
}
}

在前面的代码中,利用 JPA 存储库的更新片段以粗体显示,因此现在 EventCalendarUser 对象映射到我们的底层 RDBMS。

应用程序此时将无法工作,但在我们继续进行下一步转换之前,这仍然可以被视为一个标记点​​。

此时,您的源代码应该看起来与 chapter05.03-日历

应用服务

剩下要做的就是配置 Spring Security 以使用新的工件。

我们需要编辑 DefaultCalendarService.java 文件,只删除用于将 USER_ROLE 添加到任何新创建的 User 对象的剩余代码如下:

    //com/packtpub/springsecurity/service/DefaultCalendarService.java

package com.packtpub.springsecurity.service;
... omitted for brevity ...
@Repository
public class DefaultCalendarService implements CalendarService {
@Override
public int createUser(CalendarUser user) {
String encodedPassword = passwordEncoder.encode(user.getPassword());
user.setPassword(encodedPassword);
int userId = userDao.createUser(user);
//jdbcOperations.update("insert into
calendar_user_authorities(calendar_user,authority)
values (?,?)", userId,
//"ROLE_USER");
return userId;
}
}

UserDetailsS​​ervice 对象

让我们看一下添加 UserDetailsS​​ervice 对象的以下步骤:

  1. Now, we need to add a new implementation of the UserDetailsService object, we will use our CalendarUserRepository interface to authenticate and authorize users again, with the same underlying RDBMS, but using our new JPA implementation as follows:
        //com/packtpub/springsecurity/service/UserDetailsServiceImpl.java

package com.packtpub.springsecurity.service;
... omitted for brevity ...
@Service
public class UserDetailsServiceImpl
implements UserDetailsService {
@Autowired
private CalendarUserRepository userRepository;
@Override
@Transactional(readOnly = true)
public UserDetails loadUserByUsername(final String username)
throws UsernameNotFoundException {
CalendarUser user = userRepository.findByEmail(username);
Set<GrantedAuthority> grantedAuthorities = new HashSet<>();
for (Role role : user.getRoles()){
grantedAuthorities.add(new SimpleGrantedAuthority
(role.getName()));
}
return new org.springframework.security.core.userdetails.User(
user.getEmail(), user.getPassword(), grantedAuthorities);
}
}

  1. Now, we have to configure Spring Security to use our custom UserDetailsService object, as follows:
       //com/packtpub/springsecurity/configuration/SecurityConfig.java

package com.packtpub.springsecurity.configuration;
... omitted for brevity ...
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {\
@Autowired
private UserDetailsService userDetailsService;
@Override
public void configure(AuthenticationManagerBuilder auth)
throws Exception {
auth
.userDetailsService(userDetailsService)
.passwordEncoder(passwordEncoder());
}
@Bean
@Override
public UserDetailsService userDetailsService() {
return new UserDetailsServiceImpl();
}
...
}
  1. Start the application and try logging in to the application. Any of the configured users can now log in and create new events. You can also create a new user and will be able to log in as the new user immediately.
您的代码现在应该看起来像 calendar05.04-日历

从 RDBMS 重构为文档数据库

幸运的是,有了 Spring Data 项目,一旦我们有了 Spring Data 实现,我们就完成了大部分困难的工作。现在,只有少数特定于实现的更改需要重构。

使用 MongoDB 实现文档数据库

现在,我们将使用 MongoDB 作为我们的底层数据库提供者,将我们的 RDBMS 实现(使用 JPA 作为我们的 ORM 提供者)重构为文档数据库实现。 MongoDB(来自humonous)是一个免费和开源的跨平台面向文档的数据库程序。 MongoDB 被归类为 NoSQL 数据库程序,它使用带有模式的类似 JSON 的文档。 MongoDB 由 MongoDB Inc. 开发,位于 https://github.com/mongodb/mongo

更新我们的依赖

我们已经包含了本章所需的所有依赖项,因此您无需对 build.gradle 文件进行任何更新。但是,如果您只是为自己的应用程序添加 Spring Data JPA 支持,则需要在 build.gradle spring-boot-starter-data-jpa 作为依赖项kbd> 文件,如下:

    //build.gradle
// JPA / ORM / Hibernate:
//compile('org.springframework.boot:spring-boot-starter-data-jpa')
// H2 RDBMS
//runtime('com.h2database:h2')
// MongoDB:

compile('org.springframework.boot:spring-boot-starter-data-mongodb')
compile('de.flapdoodle.embed:de.flapdoodle.embed.mongo')

请注意,我们删除了 spring-boot-starter-jpa 依赖项。 spring-boot-starter-data-mongodb 依赖项将包含将域对象连接到嵌入式 MongoDB 数据库所需的所有依赖项,其中包含 Spring 和 MongoDB 注释。

我们还添加了 Flapdoodle 嵌入式 MongoDB 数据库,但这仅用于测试和演示目的。嵌入式 MongoDB 将为在单元测试中运行 MongoDB 提供一种平台中立的方式。这个嵌入式数据库位于 https://github.com/flapdoodle-oss/de .flapdoodle.embed.mongo

在 MongoDB 中重新配置数据库配置

首先,我们将开始转换当前的 JBCP 日历项目。让我们首先重新配置数据库以使用 Flapdoodle 嵌入式 MongoDB 数据库。以前,当我们更新这个项目的依赖项时,我们添加了一个 Flapdoodle 依赖项,它为项目提供了一个嵌入式 MongoDB 数据库,我们可以自动使用它而不是安装完整版本的 MongoDB 安装。为了与 JBCP 应用程序保持一致,我们需要更改数据库的名称。使用 Spring Data,我们可以使用 YAML 配置更改 MongoDB 配置,如下所示:

    //src/main/resources/application.yml

spring
# MongoDB
data:
mongodb:
host: localhost
database: dataSource

对于我们当前的需求,最重要的配置是将数据库名称更改为 dataSource,这与我们在本书中一直使用的名称相同。

初始化 MongoDB 数据库

在 JPA 实现中,我们使用 data.sql 文件来初始化数据库中的数据。对于 MongoDB 实现,我们可以删除 data.sql 文件并将其替换为 Java 配置文件,我们将其称为 MongoDataInitializer.java

    //src/main/java/com/packtpub/springsecurity/configuration/
MongoDataInitializer.java


@Configuration
public class MongoDataInitializer {
@Autowired
private RoleRepository roleRepository;
@Autowired
private CalendarUserRepository calendarUserRepository;
@Autowired
private EventRepository eventRepository;
@PostConstruct
public void setUp() {
calendarUserRepository.deleteAll();
roleRepository.deleteAll();
eventRepository.deleteAll();
seedRoles();
seedCalendarUsers();
seedEvents();
}
CalendarUser user1, admin, user2;
{
user1 = new CalendarUser(0, "[email protected]",
"$2a$04$qr7RWyqOnWWC1nwotUW1nOe1RD5.mKJVHK16WZy6v49pymu1WDHmi",
"User","1");
admin = new CalendarUser(1,"[email protected]",
"$2a$04$0CF/Gsquxlel3fWq5Ic/ZOGDCaXbMfXYiXsviTNMQofWRXhvJH3IK",
"Admin","1");
user2 = new CalendarUser(2,"[email protected]",
"$2a$04$PiVhNPAxunf0Q4IMbVeNIuH4M4ecySWHihyrclxW..PLArjLbg8CC",
"User2","2");
}
Role user_role, admin_role;
private void seedRoles(){
user_role = new Role(0, "ROLE_USER");
admin_role = new Role(1, "ROLE_ADMIN");
user_role = roleRepository.save(user_role);
admin_role = roleRepository.save(admin_role);
}
private void seedEvents(){
// Event 1
Event event1 = new Event(100, "Birthday Party", "This is
going to be a great birthday", new
GregorianCalendar(2017,6,3,6,36,00), user, admin);
// Event 2
Event event2 = new Event(101, "Conference Call",
"Call with the client",new
GregorianCalendar(2017,11,23,13,00,00),user2, user);
// Event 3
Event event3 = new Event(102, "Vacation",
"Paragliding in Greece",new GregorianCalendar(2017,8,14,11,30,00),
admin, user2);
// Save Events
eventRepository.save(event1);
eventRepository.save(event2);
eventRepository.save(event3);
}
private void seedCalendarUsers(){
// user1
user1.addRole(user_role);
// admin2
admin.addRole(user_role);
admin.addRole(admin_role);
// user2
user2.addRole(user_role);
calendarUserRepository.save(user1);
calendarUserRepository.save(admin);
calendarUserRepository.save(user2);
}
}

这将在加载时执行,并将与我们对 H2 数据库所做的相同的数据播种到我们的 MongoDB 中。

使用 MongoDB 映射域对象

让我们首先映射我们的 Event.java 文件,以便将每个域对象作为文档保存在我们的 MongoDB 数据库中。这可以通过执行以下步骤来完成:

  1. With a document database, domain object mapping is a little different, but the same ORM concepts hold true. Let's begin with the Event JPA implementation, then take a look how we can transform our Entity to document mapping:
        //src/main/java/com/packtpub/springsecurity/domain/Event.java

...
import javax.persistence.*;
@Entity
@Table(name = "events")
public class Event implements Serializable{
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Integer id;
private String summary;
private String description;
private Calendar when;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name="owner", referencedColumnName="id")
private CalendarUser owner;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name="attendee", referencedColumnName="id")
private CalendarUser attendee;
  1. In Entity-based JPA mapping, we needed to use six different annotations to create the required mapping. Now, with document-based MongoDB mapping, we need to change all the previous mapping annotations. Here is a fully refactored example of our Event.java file:
        //src/main/java/com/packtpub/springsecurity/domain/Event.java

import org.springframework.data.annotation.Id;
import org.springframework.data.annotation.PersistenceConstructor;
import org.springframework.data.domain.Persistable;
import org.springframework.data.mongodb.core.mapping.DBRef;
import org.springframework.data.mongodb.core.mapping.Document;
...
@Document(collection="events")
public class Event implements Persistable<Integer>, Serializable{
@Id
private Integer id;
private String summary;
private String description;
private Calendar when;
@DBRef
private CalendarUser owner;
@DBRef
private CalendarUser attendee;
@PersistenceConstructor
public Event(Integer id,
String summary,
String description,
Calendar when,
CalendarUser owner,
CalendarUser attendee) {
...
}

在前面的代码中,我们可以看到以下几个值得注意的变化:

  1. First, we declare the class to be of type @o.s.d.mongodb.core.mapping.Document, and provide a collection name for these documents.
  2. Next, the Event class must implement the o.s.d.domain.Persistable interface, providing the primary key type (Integer) for our document.
  3. Now, we change the annotation for our domain ID to @o.s.d.annotation.Id, to define the domain primary key.
  4. Previously, we had to map our owner and attendee CalendarUser object to two different mapping annotations.
  5. Now, we only have to define the two types to be of type @o.s.d.mongodb.core.mapping.DBRef, and allow Spring Data to take care of the underlying references.
  6. The final annotation we have to add defines a specific constructor to be used for new documents to be added to our document, by using the @o.s.d.annotation.PersistenceConstructor annotation.
  7. Now that we have reviewed the changes needed to refactor from JPA to MongoDB, let's refactor the other domain object starting with the Role.java file, as follows:
        //src/main/java/com/packtpub/springsecurity/domain/Role.java

...
import org.springframework.data.annotation.Id;
import org.springframework.data.annotation.PersistenceConstructor;
import org.springframework.data.domain.Persistable;
import org.springframework.data.mongodb.core.mapping.Document;
@Document(collection="role")
public class Role implements Persistable<Integer>, Serializable {
@Id
private Integer id;
private String name;
public Role(){}
@PersistenceConstructor
public Role(Integer id, String name) {
this.id = id;
this.name = name;
}
  1. The final domain object that we need to refactor is our CalendarUser.java file. After all, this is the most complex domain object we have in this application:
        //src/main/java/com/packtpub/springsecurity/domain/CalendarUser.java

...
import org.springframework.data.annotation.Id;
import org.springframework.data.annotation.PersistenceConstructor;
import org.springframework.data.domain.Persistable;
import org.springframework.data.mongodb.core.mapping.DBRef;
import org.springframework.data.mongodb.core.mapping.Document;
@Document(collection="calendar_users")
public class CalendarUser implements Persistable<Integer>,
Serializable {
@Id
private Integer id;
private String firstName;
private String lastName;
private String email;
private String password;
@DBRef(lazy = false)
private Set<Role> roles = new HashSet<>(5);
public CalendarUser() {}
@PersistenceConstructor
public CalendarUser(Integer id,String email, String password,
String firstName,String lastName) {
this.id = id;
this.firstName = firstName;
this.lastName = lastName;
this.email = email;
this.password = password;
}

如您所见,将我们的域对象从 JPA 重构为 MongoDB 的工作相当简单,并且比 JPA 配置需要更少的注解配置。

MongoDB 的 Spring 数据仓库

我们现在只需进行一些更改即可从 JPA 实现重构为 MongoDB 实现。我们将首先通过更改我们的存储库扩展的接口来重构我们的 CalendarUserRepository.java 文件,如下所示:

    //com/packtpub/springsecurity/repository/CalendarUserRepository.java

...
import org.springframework.data.mongodb.repository.MongoRepository;
public interface CalendarUserRepository extends MongoRepository
<CalendarUser, Integer> {
...

同样的更改需要相应地应用于 EventRepository.java 文件和 RoleRepository.java 文件。

如果您在任何这些更改方面需要帮助,请记住 chapter05.05 将提供完整的代码供您参考。

MongoDB 中的数据访问对象

在我们的 EventDao 接口中,我们需要创建一个新的 Event 对象。使用 JPA,我们可以自动生成对象 ID。在 MongoDB 中,有多种方法可以分配主键标识符,但为了演示,我们将使用原子计数器,如下所示:

    //src/main/java/com/packtpub/springsecurity/dataaccess/MongoEventDao.java

...
import java.util.concurrent.atomic.AtomicInteger;
@Repository
public class MongoEventDao implements EventDao {
// Simple Primary Key Generator
private AtomicInteger eventPK = new AtomicInteger(102);
...
@Override
public int createEvent(Event event) {
...
// Get the next PK instance
event.setId(eventPK.incrementAndGet());
Event newEvent = repository.save(event);
return newEvent.getId();
}
...

我们的 CalendarUserDao 对象在技术上没有变化,但为了本书的一致性,我们重命名了实现文件以表示使用 Mongo

    @Repository
public class MongoCalendarUserDao implements CalendarUserDao {

此重构示例不需要其他 数据访问对象 (DAO) 更改。

继续并启动应用程序,它会像以前一样运行。尝试以 user1admin1 身份登录,并对其进行测试以确保两个用户都可以向系统添加新事件,以确保整个应用程序的映射正确。

你应该从源头开始 chapter05.05-日历

概括

我们研究了 Spring Data 项目的强大功能和灵活性,并探讨了与应用程序开发相关的几个方面,以及它与 Spring Security 的集成。在本章中,我们介绍了 Spring Data 项目及其一些功能。我们还看到了从使用 SQL 的遗留 JDBC 代码转换为使用 JPA 的 ORM,以及从使用 Spring Data 的 JPA 实现转换为使用 Spring Data 的 MongoDB 实现的重构过程。我们还介绍了配置 Spring Security 以在关系数据库和文档数据库中利用 ORM Entity

在下一章中,我们将探索 Spring Security 对基于 LDAP 的身份验证的内置支持。