Spring Boot + Spring Security + JWT + MySQL + React Full Stack Polling App

Hello and Welcome to the first part of an exciting series of blog posts where you will learn how to build an end-to-end full stack polling app similar to twitter polls.

We’ll build the backend server using Spring Boot where we’ll use Spring Security along with JWT authentication. We’ll use MySQL database for storage.

将使用Spring Boot构建后端服务器,并在其中使用Spring Security和JWT身份验证。将使用MySQL数据库进行存储

The front-end application will be built using React. We’ll also use Ant Design for designing our user interface.

前端应用程序将使用React构建。还将使用Ant Design来设计用户界面

In the end of this tutorial series, you’ll have built a fully-fledged polling application from scratch like a boss.

从头开始构建一个成熟的轮询应用程序

The complete source code of the project is hosted on Github. You can refer that anytime if you get stuck at something.

Following is the screenshot of the final version of our application -

以下是我们应用程序最终版本的屏幕截图-

Spring Boot + Spring Security + JWT + MySQL + React Full Stack Polling App

Looks great, isn’t it? Well, then let’s start building it from scratch…

In this article, We’ll set up the backend project using Spring Boot and define the basic domain models and repositories.

在本文中,我们将使用Spring Boot设置后端项目,并定义基本的域模型和存储库

Creating the Backend Application using Spring Boot

Let’s bootstrap the project using Spring Initialzr web tool -

  1. Open http://start.spring.io
  2. Enter polls in Artifact field.
  3. Add Web, JPA, MySQL and Security dependencies from the Dependencies section.
  4. Click Generate to generate and download the project.

Spring Boot + Spring Security + JWT + MySQL + React Full Stack Polling App

Once the project is downloaded, unzip it and import it into your favorite IDE. The directory structure of the project will look like this-

下载项目后,将其解压缩并将其导入您喜欢的IDE。该项目的目录结构如下所示:

Spring Boot + Spring Security + JWT + MySQL + React Full Stack Polling App

Adding additional dependencies

We’ll need to add few additional dependencies to our project. Open pom.xml file from the root directory of your generated project and add the following to the <dependencies> section -

我们将需要为项目添加一些其他依赖项。从生成的项目的根目录中打开pom.xml文件,并将以下内容添加到部分中-

<!-- For Working with Json Web Tokens (JWT) -->
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt</artifactId>
    <version>0.9.0</version>
</dependency>

<!-- For Java 8 Date/Time Support -->
<dependency>
    <groupId>com.fasterxml.jackson.datatype</groupId>
    <artifactId>jackson-datatype-jsr310</artifactId>
</dependency>

Configuring the Server, Database, Hibernate and Jackson

Let’s now configure the server, database, hibernate, and jackson by adding the following properties to the src/main/resources/application.properties file -

现在,通过将以下属性添加到src/main/resources/application.properties文件中,配置服务器,数据库

## Server Properties
server.port= 5000

## Spring DATASOURCE (DataSourceAutoConfiguration & DataSourceProperties)
spring.datasource.url= jdbc:mysql://localhost:3306/polling_app?useSSL=false&serverTimezone=UTC&useLegacyDatetimeCode=false
spring.datasource.username= root
spring.datasource.password= MyNewPass4!

## Hibernate Properties

# The SQL dialect makes Hibernate generate better SQL for the chosen database
spring.jpa.properties.hibernate.dialect = org.hibernate.dialect.MySQL8Dialect
spring.jpa.hibernate.ddl-auto = update

## Hibernate Logging
logging.level.org.hibernate.SQL= DEBUG

# Initialize the datasource with available DDL and DML scripts
spring.datasource.initialization-mode=always

## Jackson Properties
spring.jackson.serialization.WRITE_DATES_AS_TIMESTAMPS= false
spring.jackson.time-zone= UTC

All the above properties are self-explanatory. I’ve set hibernate’s ddl-auto property to update. This will automatically create/update the tables in the database according to the entities in our application.

以上所有属性都是不言自明的。我已将休眠的ddl-auto属性设置为要更新。这将根据我们应用程序中的实体自动创建/更新数据库中的表

The Jackson’s WRITE_DATES_AS_TIMESTAMPS property is used to disable serializing Java 8 Data/Time values as timestamps. All the Date/Time values will be serialized to ISO date/time string.

Jackson的WRITE_DATES_AS_TIMESTAMPS属性用于禁用序列化Java 8 Data/Time值作为时间戳。所有日期/时间值将被序列化为ISO日期/时间字符串

Before proceeding further, please create a database named polling_app in MySQL and change the spring.datasource.username and spring.datasource.password properties as per your MySQL installation.

在继续之前,请在MySQL中创建一个名为polling_app的数据库,并根据您的MySQL安装更改spring.datasource.username和spring.datasource.password属性

Configuring Spring Boot to use Java 8 Date/Time converters and UTC Timezone

配置Spring Boot以使用Java 8日期/时间转换器和UTC时区

We’ll be using Java 8 Data/Time classes in our domain models. We’ll need to register JPA 2.1 converters so that all the Java 8 Date/Time fields in the domain models automatically get converted to SQL types when we persist them in the database.

我们将在域模型中使用Java 8 Data/Time类。我们需要注册JPA 2.1转换器,以便在将域模型中的所有Java 8日期/时间字段持久化到数据库中时,它们都可以自动转换为SQL类型

Moreover, We’ll set the default timezone for our application to UTC.

此外,我们会将应用程序的默认时区设置为UTC

Open the main class PollsApplication.java and make the following modifications to it-

打开主类PollsApplication.java并对其进行以下修改-

package com.callicoder.polls;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.domain.EntityScan;
import org.springframework.data.jpa.convert.threeten.Jsr310JpaConverters;

import javax.annotation.PostConstruct;
import java.util.TimeZone;

@SpringBootApplication
@EntityScan(basePackageClasses = { 
		PollsApplication.class,
		Jsr310JpaConverters.class 
})
public class PollsApplication {

	@PostConstruct
	void init() {
		TimeZone.setDefault(TimeZone.getTimeZone("UTC"));
	}

	public static void main(String[] args) {
		SpringApplication.run(PollsApplication.class, args);
	}
}

Creating the domain models

Our application will allow new users to register and login to our application. Every User will have one or more roles. The roles associated with a user will be used in future to decide whether the user is authorized to access a particular resource on our server or not.

应用程序将允许新用户注册并登录我们的应用程序。每个用户将具有一个或多个角色。与用户相关的角色将在将来用于确定该用户是否被授权访问我们服务器上的特定资源

In this section, We’ll create the User and Role domain models. All the domain models will be stored in a package named model inside com.callicoder.polls.

在本节中,我们将创建用户和角色域模型。所有域模型都将存储在com.callicoder.polls中名为model的包中

1. User model

The User model contains the following fields -

  1. id: Primary Key

    主键

  2. username: A unique username

    唯一的用户名

  3. email: A unique email

    唯一的邮箱

  4. password: A password which will be stored in encrypted format.

    密码将以加密格式存储

  5. roles: A set of roles. (Many-To-Many relationship with Role entity)

    一组角色(与角色实体的多对多关系)

Here is the complete User class -

package com.callicoder.polls.model;

import com.example.polls.model.audit.DateAudit;
import org.hibernate.annotations.NaturalId;
import javax.persistence.*;
import javax.validation.constraints.Email;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.Size;
import java.util.HashSet;
import java.util.Set;

@Entity
@Table(name = "users", uniqueConstraints = {
    @UniqueConstraint(columnNames = {
        "username"
    }),
    @UniqueConstraint(columnNames = {
        "email"
    })
})
public class User extends DateAudit {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @NotBlank
    @Size(max = 40)
    private String name;

    @NotBlank
    @Size(max = 15)
    private String username;

    @NaturalId
    @NotBlank
    @Size(max = 40)
    @Email
    private String email;

    @NotBlank
    @Size(max = 100)
    private String password;

    @ManyToMany(fetch = FetchType.LAZY)
    @JoinTable(name = "user_roles",
            joinColumns = @JoinColumn(name = "user_id"),
            inverseJoinColumns = @JoinColumn(name = "role_id"))
    private Set<Role> roles = new HashSet<>();

    public User() {

    }

    public User(String name, String username, String email, String password) {
        this.name = name;
        this.username = username;
        this.email = email;
        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;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getEmail() {
        return email;
    }

    public void setEmail(String email) {
        this.email = email;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    public Set<Role> getRoles() {
        return roles;
    }

    public void setRoles(Set<Role> roles) {
        this.roles = roles;
    }
}

The User class extends the DateAudit class that we’ll define shortly. The DateAudit class will have createdAt and updatedAt fields that will be used for auditing purposes.

User类扩展了我们即将定义的DateAudit类。DateAudit类将具有createdAt和updatedAt字段,这些字段将用于审核

2. Role model

The Role class contains an id and a name field. The name field is an enum. We’ll have a fixed set of pre-defined roles. So it makes sense to make the role name as enum.

角色类包含一个ID和一个NAME字段。NAME字段是一个枚举。我们将有一组固定的预定义角色。因此,将角色名称设置为枚举是有意义的

Here is the complete code for Role class -

package com.callicoder.polls.model;

import org.hibernate.annotations.NaturalId;
import javax.persistence.*;

@Entity
@Table(name = "roles")
public class Role {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Enumerated(EnumType.STRING)
    @NaturalId
    @Column(length = 60)
    private RoleName name;

    public Role() {

    }

    public Role(RoleName name) {
        this.name = name;
    }

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public RoleName getName() {
        return name;
    }

    public void setName(RoleName name) {
        this.name = name;
    }
}

RoleName enum

Following is the RoleName enum -

package com.callicoder.polls.model;

public enum  RoleName {
    ROLE_USER,
    ROLE_ADMIN
}

I have defined two roles namely ROLE_USER and ROLE_ADMIN. You’re free to add more roles as per your project requirements.

定义了两个角色,即ROLE_USER和ROLE_ADMIN。您可以根据项目要求随意添加更多角色

3. DateAudit model

All right! Let’s now define the DateAudit model. It will have a createdAt and an updatedAt field. Other domain models that need these auditing fields will simply extend this class.

现在让我们定义DateAudit模型。它将具有createdAt和updatedAt字段。需要这些审核字段的其它域模型将简单地扩展此类

We’ll use JPA’s AuditingEntityListener to automatically populate createdAt and updatedAt values when we persist an entity.

当我们保留实体时,我们将使用JPA的AuditingEntityListener来自动填充createdAt和updatedAt值

Here is the Complete DateAudit class (I’ve created a package named audit inside com.example.polls.model package to store all the auditing related models) -

这是Complete DateAudit类(在com.callicoder.polls.model包中创建了一个名为audit的包,用于存储所有与审核相关的模型)-

package com.callicoder.polls.model.audit;

import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;
import javax.persistence.Column;
import javax.persistence.EntityListeners;
import javax.persistence.MappedSuperclass;
import java.io.Serializable;
import java.time.Instant;

@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
@JsonIgnoreProperties(
    value = {"createdAt", "updatedAt"},
    allowGetters = true
)
public abstract class DateAudit implements Serializable {

    @CreatedDate
    @Column(nullable = false, updatable = false)
    private Instant createdAt;

    @LastModifiedDate
    @Column(nullable = false)
    private Instant updatedAt;

    public Instant getCreatedAt() {
        return createdAt;
    }

    public void setCreatedAt(Instant createdAt) {
        this.createdAt = createdAt;
    }

    public Instant getUpdatedAt() {
        return updatedAt;
    }

    public void setUpdatedAt(Instant updatedAt) {
        this.updatedAt = updatedAt;
    }
}

To enable JPA Auditing, we’ll need to add @EnableJpaAuditing annotation to our main class or any other configuration classes.

要启用JPA审核,我们需要在主类或任何其它配置类中添加@EnableJpaAuditing注解

Let’s create an AuditingConfig configuration class and add the @EnableJpaAuditing annotation to it.

让我们创建一个AuditingConfig配置类,并向其添加@EnableJpaAuditing注解

We’re creating a separate class because we’ll be adding more auditing related configurations later. So it’s better to have a separate class.

我们将创建一个单独的类,因为稍后将添加更多与审核相关的配置。因此,最好有一个单独的类

We’ll keep all the configuration classes inside a package named config. Go ahead and create the config package inside com.example.polls, and then create the AuditingConfig class inside config package -

我们会将所有配置类保留在名为config的程序包中。继续在com.callicoder.polls中创建配置包,然后在配置包中创建AuditingConfig类-

package com.callicoder.polls.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;

@Configuration
@EnableJpaAuditing
public class AuditingConfig {
    // That's all here for now. We'll add more auditing configurations later.
}

Creating the Repositories for accessing User and Role data

创建用于访问用户和角色数据的存储库

Now that we have defined the domain models, Let’s create the repositories for persisting these domain models to the database and retrieving them.

现在我们已经定义了域模型,让我们创建存储库,以将这些域模型持久化到数据库中并检索它们

All the repositories will go inside a package named repository. So let’s first create the repository package inside com.example.polls.

所有存储库都将放在名为repository的包中。因此,让我们首先在com.callicoder.polls中创建repository包

1. UserRepository

Following is the complete code for UserRepository interface. It extends Spring Data JPA’s JpaRepository interface.

以下是UserRepository接口的完整代码。它扩展了Spring Data JPA的JpaRepository接口

package com.callicoder.polls.repository;

import com.example.polls.model.User;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.List;
import java.util.Optional;

@Repository
public interface UserRepository extends JpaRepository<User, Long> {
    Optional<User> findByEmail(String email);

    Optional<User> findByUsernameOrEmail(String username, String email);

    List<User> findByIdIn(List<Long> userIds);

    Optional<User> findByUsername(String username);

    Boolean existsByUsername(String username);

    Boolean existsByEmail(String email);
}

2. RoleRepository

Following is the RoleRepository interface. It contains a single method to retrieve a Role from the RoleName-

以下是RoleRepository接口。它包含一个从RoleName-检索Role的方法

package com.callicoder.polls.repository;

import com.example.polls.model.Role;
import com.example.polls.model.RoleName;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.Optional;

@Repository
public interface RoleRepository extends JpaRepository<Role, Long> {
    Optional<Role> findByName(RoleName roleName);
}

Exploring the current setup and Running the Application

探索当前设置并运行应用程序

After creating all the above models, repositories and configurations, our current project should look like this -

创建完所有上述模型,存储库和配置后,我们当前的项目应如下所示-

Spring Boot + Spring Security + JWT + MySQL + React Full Stack Polling App

You can run the application by typing the following command from the root directory of your project -

您可以通过从项目的根目录中键入以下命令来运行应用程序-

mvn spring-boot:run

Check out the logs and make sure that the server starts successfully.

检查日志并确保服务器成功启动

2020-07-08 22:40:44.998  INFO 33708 --- Tomcat started on port(s): 5000 (http)
2020-07-08 22:40:45.008  INFO 33708 --- Started PollsApplication in 7.804 seconds (JVM running for 27.193)

Write to me in the comment section, if the server doesn’t start successfully for you. I’ll help you out.

Creating Default Roles

We’ll have a fixed set of predefined roles in our application. Whenever a user logs in, we’ll assign ROLE_USER to it by default.

应用程序中将有一组固定的预定义角色。每当用户登录时,默认情况下,都会为其分配ROLE_USER

For assigning the roles, they have to be present in the database. So let’s create the two default roles in the database by executing the following insert statements -

为了分配角色,它们必须存在于数据库中。因此,让我们通过执行以下插入语句在数据库中创建两个默认角色-

INSERT INTO roles(name) VALUES('ROLE_USER');
INSERT INTO roles(name) VALUES('ROLE_ADMIN');

What’s Next?

In the next chapter of this series, we’ll learn how to configure Spring Security in our project and add functionalities to register a new user and log them in.

在本系列的下一章中,将学习如何在项目中配置Spring Security并添加功能以注册新用户并登录

Read Next:

Full Stack Polling App with Spring Boot, Spring Security, JWT, MySQL and React - Part 2