(译)Spring Boot + Spring Security
原文链接: https://www.callicoder.com/spring-boot-spring-security-jwt-mysql-react-app-part-1/
欢迎来到令人激动的系列博客第一部分,在此你将学会如何构建一个端到端的全栈投票APP,有点类似于Twitter Polls。
我们将使用Spring Boot来构建服务端接口,结合Spring Security与JWT来进行认证,使用MySQL数据库用来存储数据。
使用React构建前端程序,使用 Ant Design 来设计我们的用户界面。
在本系列教程的最后,你将从0到1构建一个功能完备的投票应用。
本项目的完整源码托管在Github,如果你碰到困难,可随时参考。
下面是我们应用最终版本的截图
spring-boot-spring-security-jwt-mysql-react-full-stack-polling-app.jpg
看起来不错吧,那让我们从零开始构建吧。
在本文中,我们将使用Spring Boot来构建后端项目,随后定义基础实体与数据仓库。
使用Spring Boot创建后端应用
让我们使用Spring Initialzr web tool来创建项目
-
输入polls在Artifact栏
-
在依赖块添加Web,JPA,MySQL和Security的依赖
-
点击Generate Project来生成并下载项目
spring-boot-spring-security-mysql-jwt-react-polling-ap.jpg
等项目下载完成后解压,导入至你喜爱的IDE中,项目结构看起来是这样的-
spring-security-mysql-jwt-polling-app-directory-structure.jpg
增加额外的依赖
我们的项目还需要一些额外的依赖。从根目录打开 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>
配置Server,数据库,Hibernate和Jackson
让我们开始配置Server,数据库,Hibernate和Jackson,增加以下配置到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= callicoder
## Hibernate Properties
# The SQL dialect makes Hibernate generate better SQL for the chosen database
spring.jpa.properties.hibernate.dialect = org.hibernate.dialect.MySQL5InnoDBDialect
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
以上配置都是看名称就可以大概知晓含义的。我把hibernate的ddl-auto
属性设为了update
,将会根据 实体 自动在数据库中创建/更新表的结构。
Jackson的WRITE_DATES_AS_TIMESTAMPS
属性是用来使Date/Time的值序列化为 ISO date/time 字符串格式,而不是Java 8 Date/Time的格式。
在做下一步之前,请先在MySQL中创建polling_app数据库并把spring.datasource.username
和spring.datasource.password
的值替换成你自己的。
配置Java8 Date/Time - UTC转换器
我们将在领域模型中使用Java 8 Date/Time,需要注册 JPA2.1 转换器,以便将领域模型中的所有Java 8 Date / Time字段持久化为SQL类型后再将它们保存在数据库中。
此外,我们还需要在我们的应用中设置默认时区为 UTC。
打开PollsApplication.java
,添加一下修改-
package com.example.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);
}
}
创建领域模型
我们的应用允许用户注册,登入。每个用户有一个或多个角色。用户有什么角色决定了他能访问那些特定的资源与否。
在这一节,我们创建User
和Role
对象,所有的领域模型都放在名为model
的包下,包名为 com.example.polls
。
1. User
用户模型包含以下几个项 -
1.id
: 主键
-
username
: 一个唯一的名字 -
email
: 一个唯一的邮箱 -
password
: 加密后被存储的密码 -
roles
:一个角色集(与Role
是多对多关系)
package com.example.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;
}
}
User
类继承了DateAudit
类,我们稍后再定义。DateAudit
类有 createdAt
和updateAt
这2个字段 主要用于记录创建时间和更新时间。
2. Role
Role
类包含了id
和name
字段。name
字段是一个枚举。我们有一组预定义的角色,所以我们把角色定义为枚举。
package com.example.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
package com.example.polls.model;
public enum RoleName {
ROLE_USER,
ROLE_ADMIN
}
我定义了2个角色为ROLE_USER
和ROLE_ADMIN
,你可以根据项目需求自我增删角色。
3. DateAudit
OK,让我们开始定义DateAudit
。他有createdAt
和updatedAt
字段。其他的类需要这2个字段可以很简单的通过继承来获得。
当我们持久化实体时,JPA的AuditingEntityListener
可以自动往createdAt
和updatedAt
字段填充值。
以下是一个完整的DateAudit
类,我们在model
包下再创建一个audit
包来存放相关的模型。
package com.example.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;
}
}
为了开始JPA Auditing
,我们需要增加@EnableJpaAuditing
注解在我们的主类上或其他任何配置类上。
让我们创建一个AuditingConfig
配置类并在类头上加上@EnableJpaAuditing
注解。
我们单独创建这个类是因为我们后续会增加更多的Auditing相关的配置类。
我们把所有配置相关的类放在config包下,现在就让我们去com.example.polls
下创建config包并创建AuditingConfig
吧
package com.example.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.
}
创建可访问User
和Role
的 Repositories
现在我们已经定义好了领域模型,下面就可以定义Repositories,Repositories的作用是持久化数据和查询数据。
所有的Repositories应该在名为repository
的包下。现在就让我们去com.example.polls
下创建 repository
包吧。
1. UserRepository
以下就是UserRepository
的完整的代码,他继承了Spring Data JPA的JpaRepository
接口。
package com.example.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
以下就是RoleRepository
的接口,他包含了一个方法:通过RoleName查询Role。
package com.example.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);
}
检测当前配置并启动程序
在创建以上模型,仓库和配置后,我们的项目结构看起来应该如下
spring-boot-spring-security-jwt-user-role-directory-structure-part-1.jpg
你可以在项目根目录下通过键入以下命令来启动项目
mvn spring-boot:run
检查日志并确保服务已成功启动
2018-02-24 22:40:44.998 INFO 33708 --- Tomcat started on port(s): 5000 (http)
2018-02-24 22:40:45.008 INFO 33708 --- Started PollsApplication in 7.804 seconds (JVM running for 27.193)
创建默认角色
我们得有一些预定义的角色,才得以在用户注册完赋予它ROLE_USER
的角色。
我们得在MySQL中执行以下sql来创建这些初始的角色。
INSERT INTO roles(name) VALUES('ROLE_USER');
INSERT INTO roles(name) VALUES('ROLE_ADMIN');
下一步是什么?
在本系列文章的下一章,我们将学习在项目中如何配置Spring Security并加一些功能,比如用户注册与用户登录。