SpringCloud一代组件+Nginx实现简单的登录注册
使用简单的登录注册功能串联起 验证码的生成与发送、邮件发送、IP防爆刷、用户统一认证等功能,实现所涉及到的技术全部基于 SpringCloud微服务架构: Nginx、Eureka、Feign(Ribbon、Hystrix)、Gateway、Config+bus等
软件依赖版本
依赖软件 | 版本号 |
---|---|
JDK | 11 |
SpringCloud | Greenwich.RELEASE |
SpringBoot | 2.1.6.RELEASE |
Nginx | windows版1.20.2 |
rabbitMq | windows版rabbitmq_server-3.10.0 |
configServerGit | gitee |
SpringCloud一代组件对应的实现步骤:
1.要求
2.实现步骤
1. 首先将项目骨架创建出来
lagou-liuyu-login-parent 项目(父工程,pom文件中打包方式为pom),然后添加SpringCloud相关组件的依赖
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.lagou.liuyu</groupId>
<artifactId>lagou-liuyu-login-parent</artifactId>
<version>1.0-SNAPSHOT</version>
<modules>
<module>lagou-service-user</module>
<module>lagou-service-code</module>
<module>lagou-service-email</module>
<module>lagou-cloud-configserver</module>
<module>lagou-cloud-eureka-server-8761</module>
<module>lagou-cloud-eureka-server-8762</module>
<module>lagou-service-common</module>
<module>lagou-cloud-business</module>
</modules>
<!--⽗⼯程打包⽅式为pom-->
<packaging>pom</packaging>
<!--spring boot 父启动器依赖-->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.6.RELEASE</version>
</parent>
<properties>
<maven.compiler.source>11</maven.compiler.source>
<maven.compiler.target>11</maven.compiler.target>
</properties>
<dependencyManagement>
<dependencies>
<!--spring cloud依赖管理,引入了Spring Cloud的版本-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Greenwich.RELEASE</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<!--web依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--日志依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
</dependency>
<!--测试依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!--lombok工具-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.4</version>
<scope>provided</scope>
</dependency>
<!-- Actuator可以帮助你监控和管理Spring Boot应用-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<!--热部署-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<optional>true</optional>
</dependency>
<!--eureka server 需要引入Jaxb,开始-->
<dependency>
<groupId>com.sun.xml.bind</groupId>
<artifactId>jaxb-core</artifactId>
<version>2.2.11</version>
</dependency>
<dependency>
<groupId>javax.xml.bind</groupId>
<artifactId>jaxb-api</artifactId>
</dependency>
<dependency>
<groupId>com.sun.xml.bind</groupId>
<artifactId>jaxb-impl</artifactId>
<version>2.2.11</version>
</dependency>
<dependency>
<groupId>org.glassfish.jaxb</groupId>
<artifactId>jaxb-runtime</artifactId>
<version>2.2.10-b140310.1920</version>
</dependency>
<dependency>
<groupId>javax.activation</groupId>
<artifactId>activation</artifactId>
<version>1.1.1</version>
</dependency>
<!--引入Jaxb,结束-->
</dependencies>
<build>
<plugins>
<!--编译插件-->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>11</source>
<target>11</target>
<encoding>utf-8</encoding>
</configuration>
</plugin>
<!--打包插件-->
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
2.然后将每个功能对应的子项目创建出来
其中包括:
a. 注册中心(集群)项目 lagou-cloud-eureka-server-8761、lagou-cloud-eureka-server-8762
b. 全局配置中心项目 lagou-cloud-configserver (9006端口)
c. 全局网关项目 lagou-cloud-gateway (网关项目为单独的工程,不做为子项目引入)(9002端口)
d. 验证码项目 lagou-service-code (8081端口)
e. 邮件项目 lagou-service-email (8082端口)
f. 用户项目 lagou-service-user (8080端口)
g. 公共服务项目 lagou-service-common
h. 统一业务层项目 lagou-cloud-business (8070端口)
3.导入对应的数据库和配置全局配置
-
添加项目中所需要的数据库表
-
gitee中添加仓库用于config的全局配置文件
4.配置每个项目基本依赖和配置文件
1.注册中心项目
添加eureka-server的依赖
<dependencies>
<!--Eureka server依赖-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>
</dependencies>
添加端口号和eureka注册地址相关的配置
server:
port: 8761
spring:
application:
name: lagou-cloud-eureka-server
eureka:
instance:
# 当前eureka实例的主机名称
hostname: LagouCloudEurekaServerA
client:
service-url:
# 客户端与Eureka server交互的地址,EurekaServer相对于其他server来说也是client
# 集群模式下,需要写其他server的地址,多个使用逗号拼接即可
defaultZone: http://LagouCloudEurekaServerB:8762/eureka/
# 是否注册到eureka中, 自己就是serve不需要注册自己,集群模式下设置为true
register-with-eureka: true
# 自己就是服务不需要从Eureka Server获取服务信息,默认为true, 这里设置为false
fetch-registry: true
启动类上添加开启eurekaserver的注解
/**
* @author LiuYu
* @date 2022/5/14 18:15
*/
@SpringBootApplication
@EnableEurekaServer
public class LagouCloudEurekaServer8761 {
public static void main(String[] args) {
SpringApplication.run(LagouCloudEurekaServer8761.class, args);
}
}
2.网关项目
添加相应的SpringCloud依赖
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<!--spring boot 父启动器依赖-->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.6.RELEASE</version>
</parent>
<artifactId>lagou-cloud-gateway</artifactId>
<groupId>com.lagou.liuyu</groupId>
<version>1.0-SNAPSHOT</version>
<properties>
<maven.compiler.source>11</maven.compiler.source>
<maven.compiler.target>11</maven.compiler.target>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-commons</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<!--GateWay 网关-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<!--引入webflux-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
<!--日志依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
</dependency>
<!--测试依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!--lombok工具-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.4</version>
<scope>provided</scope>
</dependency>
<!--引入Jaxb,开始-->
<dependency>
<groupId>com.sun.xml.bind</groupId>
<artifactId>jaxb-core</artifactId>
<version>2.2.11</version>
</dependency>
<dependency>
<groupId>javax.xml.bind</groupId>
<artifactId>jaxb-api</artifactId>
</dependency>
<dependency>
<groupId>com.sun.xml.bind</groupId>
<artifactId>jaxb-impl</artifactId>
<version>2.2.11</version>
</dependency>
<dependency>
<groupId>org.glassfish.jaxb</groupId>
<artifactId>jaxb-runtime</artifactId>
<version>2.2.10-b140310.1920</version>
</dependency>
<dependency>
<groupId>javax.activation</groupId>
<artifactId>activation</artifactId>
<version>1.1.1</version>
</dependency>
<!--引入Jaxb,结束-->
<!-- Actuator可以帮助你监控和管理Spring Boot应用-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<!--热部署-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<optional>true</optional>
</dependency>
<!--链路追踪-->
<!--<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-sleuth</artifactId>
</dependency>-->
</dependencies>
<dependencyManagement>
<!--spring cloud依赖版本管理-->
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Greenwich.RELEASE</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<plugins>
<!--编译插件-->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>11</source>
<target>11</target>
<encoding>utf-8</encoding>
</configuration>
</plugin>
<!--打包插件-->
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
添加相关的配置信息
eureka配置信息和网关gateway配置信息
server:
port: 9002
eureka:
client:
serviceUrl: # eureka server的路径
defaultZone: http://lagoucloudeurekaservera:8761/eureka/,http://lagoucloudeurekaserverb:8762/eureka/ #把 eureka 集群中的所有 url 都填写了进来,也可以只写一台,因为各个 eureka server 可以同步注册表
instance:
#使用ip注册,否则会使用主机名注册了(此处考虑到对老版本的兼容,新版本经过实验都是ip)
prefer-ip-address: true
#自定义实例显示格式,加上版本号,便于多版本管理,注意是ip-address,早期版本是ipAddress
instance-id: ${spring.cloud.client.ip-address}:${spring.application.name}:${server.port}:@project.version@
spring:
application:
name: lagou-cloud-gateway
cloud:
gateway:
routes: # 路由可以有多个
# 我们自定义的路由 ID,保持唯一
- id: lagou-service-code
#uri: http://127.0.0.1:8091 # 目标服务地址 自动投递微服务(部署多实例) 动态路由:uri配置的应该是一个服务名称,而不应该是一个具体的服务实例的地址
# gateway网关从服务注册中心获取实例信息然后负载后路由
uri: lb://lagou-service-code
# 断言:路由条件,Predicate 接受一个输入参数,返回一个布尔值结果。该接口包含多种默 认方法来将 Predicate 组合成其他复杂的逻辑(比如:与,或,非)。
predicates:
- Path=/api/code/**
# 我们自定义的路由 ID,保持唯一
- id: lagou-service-mail
#uri: http://127.0.0.1:8080 # 目标服务地址
uri: lb://lagou-service-mail
# 断言:路由条件,Predicate 接受一个输入参数,返回一个布尔值结果。该接口包含多种默 认方法来将 Predicate 组合成其他复杂的逻辑(比如:与,或,非)。
predicates:
- id: lagou-service-user
#uri: http://127.0.0.1:8080 # 目标服务地址
uri: lb://lagou-service-user
# 断言:路由条件,Predicate 接受一个输入参数,返回一个布尔值结果。该接口包含多种默 认方法来将 Predicate 组合成其他复杂的逻辑(比如:与,或,非)。
predicates:
- Path=/api/user/**
启动类中添加eureka注解
/**
* @author LiuYu
* @date 2022/5/14 20:31
*/
@SpringBootApplication
@EnableDiscoveryClient
public class GateWayApplication {
public static void main(String[] args) {
SpringApplication.run(GateWayApplication.class, args);
}
}
3.全局配置中心项目
添加eureka、config、bus相关的依赖信息
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-config-server</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-bus-amqp</artifactId>
</dependency>
</dependencies>
添加eureka相关的配置信息和config相关的配置信息和开启springboot的健康检查接口
server:
port: 9006
#注册到Eureka服务中心
eureka:
client:
service-url:
# 注册到集群,就把多个Eurekaserver地址使用逗号连接起来即可;注册到单实例(非集群模式),那就写一个就ok
defaultZone: http://LagouCloudEurekaServerA:8761/eureka,http://LagouCloudEurekaServerB:8762/eureka
instance:
prefer-ip-address: true #服务实例中显示ip,而不是显示主机名(兼容老的eureka版本)
# 实例名称: 192.168.1.103:lagou-service-resume:8080,我们可以自定义它
instance-id: ${spring.cloud.client.ip-address}:${spring.application.name}:${server.port}:@project.version@
spring:
application:
name: lagou-cloud-configserver
cloud:
config:
server:
git:
uri: https://gitee.com/struggle_ly/spring-cloud-config-repo.git #配置git服务地址
username: ****** #配置git用户名
password: ****** #配置git密码
search-paths:
- spring-cloud-config-repo
# 读取分支
label: lagou-login-config
#针对的被调用方微服务名称,不加就是全局生效
#lagou-service-resume:
# ribbon:
# NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RoundRobinRule #负载策略调整
启动类添加eureka和configServer的注解
/**
* @author LiuYu
* @date 2022/5/14 20:42
*/
@SpringBootApplication
@EnableDiscoveryClient
@EnableConfigServer
public class ConfigServerApplication {
public static void main(String[] args) {
SpringApplication.run(ConfigServerApplication.class, args);
}
}
4.公共服务common项目
添加对mysql和jpa的依赖
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
</dependencies>
添加对应的配置文件和启动类
设置pojo包,用于放置对应数据库表的实体类
5.用户服务项目
6.邮件服务项目
7.验证码服务项目
添加对应的依赖信息:service-common、eureka、config、bus、mail(邮件服务需要该依赖)
<dependencies>
<dependency>
<groupId>com.lagou.liuyu</groupId>
<artifactId>lagou-service-common</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-config-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-bus-amqp</artifactId>
</dependency>
<!-- 发邮件的依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-mail</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
</dependencies>
添加对应的配置文件信息:config信息、eureka信息、邮件信息(邮件服务需要)
server:
port: 8082
spring:
application:
name: lagou-service-email
cloud:
# config客户端配置和ConfigServer进行通信,并告知ConfigServer希望获取的配置信息在哪个文件中(lable分支下的name-profile.yml)
config:
# 获取的文件名称
name: lagou-config-common
# 获取的文件名称后缀
profile:
# 获取的仓库分支
label: lagou-login-config
# configServer的访问地址 这里不能设置eureka server的名称
uri: http://localhost:9006
# 邮件发送配置
mail:
# 邮箱服务器地址
host: smtp.189.cn
# 邮箱账号
username: ******@**.com
# 邮箱密码,如果POP3需要授权码,此处应填授权码
password: ******
default-encoding: UTF-8
# 注册到eureka server中
eureka:
client:
service-url:
# 注册到集群,就把多个eurekaServer地址使用逗号进行连接起来即可,注册到单实例(非集群模式), 那就写一个
defaultZone: http://LagouCloudEurekaServerA:8761/eureka/,http://LagouCloudEurekaServerB:8762/eureka/
instance:
# 服务实例中显示ip,而不是显示主机名(兼容老的eureka版本)
prefer-ip-address: true
# 实例名称 172.16.172.24:lagou-service-resume:8080, 这里使用instance-id进行自定义
instance-id: ${spring.cloud.client.ip-address}:${spring.application.name}:${server.port}:@project.version@
#针对的被调用方微服务名称,不加就是全局生效
ribbon:
#请求连接超时时间
ConnectTimeout: 2000
#请求处理超时时间
##########################################Feign超时时长设置
ReadTimeout: 15000
#对所有操作都进行重试
OkToRetryOnAllOperations: true
####根据如上配置,当访问到故障请求的时候,它会再尝试访问一次当前实例(次数由MaxAutoRetries配置),
####如果不行,就换一个实例进行访问,如果还不行,再换一次实例访问(更换次数由MaxAutoRetriesNextServer配置),
####如果依然不行,返回失败信息。
MaxAutoRetries: 0 #对当前选中实例重试次数,不包括第一次调用
MaxAutoRetriesNextServer: 0 #切换实例的重试次数
NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RoundRobinRule #负载策略调整
logging:
level:
# Feign 日志只会对日志级别为debug的做出响应
com.lagou.liuyu.edu.service.ResumeServiceFeignClient: debug
# 开启Feign的熔断功能
feign:
hystrix:
enabled: true
## Feign 支持对请求和响应进行GZIP压缩,以减少通信过程中的性能损耗
compression:
request:
# 开启请求压缩
enabled: true
# 设置压缩的数据类型,此处也是默认的数据
mime-types: text/html,application/xml,application/json
# 设置触发压缩的大小下限,此处也是默认的数据
min-request-size: 2048
response:
# 开启响应压缩
enabled: true
hystrix:
command:
default:
execution:
isolation:
thread:
##########################################Hystrix的超时时长设置
timeoutInMilliseconds: 5000
在对应的启动类上添加相应的注解:eureka、扫描实体的路径
/**
* @author LiuYu
* @date 2022/5/14 22:02
*/
@SpringBootApplication
@EnableDiscoveryClient
@EnableFeignClients
@EntityScan("com.lagou.liuyu")
public class EmailApplication {
public static void main(String[] args) {
SpringApplication.run(EmailApplication.class, args);
}
}
8.统一对外提供服务业务层
添加相应的依赖eureka、openFeign、config、bus
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-config-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-bus-amqp</artifactId>
</dependency>
</dependencies>
添加相应的配置信息eureka、config、ribbon、feign.hystrix
server:
port: 8070
spring:
application:
name: lagou-cloud-business
cloud:
# config客户端配置和ConfigServer进行通信,并告知ConfigServer希望获取的配置信息在哪个文件中(lable分支下的name-profile.yml)
config:
# 获取的文件名称
name: lagou-config-common
# 获取的文件名称后缀
profile:
# 获取的仓库分支
label: lagou-login-config
# configServer的访问地址 这里不能设置eureka server的名称
uri: http://localhost:9006
# 注册到eureka server中
eureka:
client:
service-url:
# 注册到集群,就把多个eurekaServer地址使用逗号进行连接起来即可,注册到单实例(非集群模式), 那就写一个
defaultZone: http://LagouCloudEurekaServerA:8761/eureka/,http://LagouCloudEurekaServerB:8762/eureka/
instance:
# 服务实例中显示ip,而不是显示主机名(兼容老的eureka版本)
prefer-ip-address: true
# 实例名称 172.16.172.24:lagou-service-resume:8080, 这里使用instance-id进行自定义
instance-id: ${spring.cloud.client.ip-address}:${spring.application.name}:${server.port}:@project.version@
# ribbon负载均衡策略调整, 指定项目使用的负载均衡策略,不写项目名默认所有项目
#lagou-service-resume:
# ribbon:
# NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RoundRobinRule # 负载均衡策略的调整
#针对的被调用方微服务名称,不加就是全局生效
ribbon:
#请求连接超时时间
ConnectTimeout: 2000
#请求处理超时时间
##########################################Feign超时时长设置
ReadTimeout: 15000
#对所有操作都进行重试
OkToRetryOnAllOperations: true
####根据如上配置,当访问到故障请求的时候,它会再尝试访问一次当前实例(次数由MaxAutoRetries配置),
####如果不行,就换一个实例进行访问,如果还不行,再换一次实例访问(更换次数由MaxAutoRetriesNextServer配置),
####如果依然不行,返回失败信息。
MaxAutoRetries: 0 #对当前选中实例重试次数,不包括第一次调用
MaxAutoRetriesNextServer: 0 #切换实例的重试次数
NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RoundRobinRule #负载策略调整
logging:
level:
# Feign 日志只会对日志级别为debug的做出响应
com.lagou.liuyu.edu.service.ResumeServiceFeignClient: debug
# 开启Feign的熔断功能
feign:
hystrix:
enabled: true
## Feign 支持对请求和响应进行GZIP压缩,以减少通信过程中的性能损耗
compression:
request:
# 开启请求压缩
enabled: true
# 设置压缩的数据类型,此处也是默认的数据
mime-types: text/html,application/xml,application/json
# 设置触发压缩的大小下限,此处也是默认的数据
min-request-size: 2048
response:
# 开启响应压缩
enabled: true
hystrix:
command:
default:
execution:
isolation:
thread:
##########################################Hystrix的超时时长设置
timeoutInMilliseconds: 5000
启动类添加相应的注解
/**
* @author LiuYu
* @date 2022/5/15 10:05
*/
@SpringBootApplication
@EnableDiscoveryClient
@EnableFeignClients
public class BusinessApplication {
public static void main(String[] args) {
SpringApplication.run(BusinessApplication.class, args);
}
}
5.具体业务实现
1.邮件服务
邮件服务实现一个发送邮件的工具类
/**
* 发送邮件工具类
*
* @author LiuYu
* @date 2022/5/14 14:27
*/
@Component
public class SendMailUtil{
@Autowired
private JavaMailSender javaMailSender;
@Value("${spring.mail.username}")
private String fromUserName;
/**
* 发送邮件工具类(普通的文本文件)
*
* @param toEmail 收件人邮箱(1********23@qq.com)
* @param title 邮件的标题
* @param msg 发送的内容
*/
public void sendSimpleMail(String toEmail,String title, String msg){
SimpleMailMessage simpleMailMessage = new SimpleMailMessage();
// 发件人
simpleMailMessage.setFrom(fromUserName);
// 收件人
simpleMailMessage.setTo(toEmail);
// 邮件标题
simpleMailMessage.setSubject(title);
// 邮件内容
simpleMailMessage.setText(msg);
javaMailSender.send(simpleMailMessage);
}
/**
* 发送邮件工具类(富文本邮件---带图片的邮件,支持多张图片)
* 发送富文本邮件需要使用MimeMessageHelper类,MimeMessageHelper支持发送复杂邮件模板,支持文本、附件、HTML、图片等。
* 如果需要发送多张图片,可以改变传参方式,使用集合添加多个<img src='cid:rscId'>和 helper.addInline(rscId, res);即可实现'
*
* @param toEmail 收件人
* @param title 邮件标题
* @param msg 邮件内容
* @param recPathMap 图片信息 : 图片路径
*/
public void sendInlineResourceMail(String toEmail, String title, String msg, Map<String, String> recPathMap) throws MessagingException {
MimeMessage mimeMessage = javaMailSender.createMimeMessage();
MimeMessageHelper mimeMessageHelper = new MimeMessageHelper(mimeMessage, true);
mimeMessageHelper.setFrom(fromUserName);
mimeMessageHelper.setTo(toEmail);
mimeMessageHelper.setSubject(title);
mimeMessageHelper.setText(msg, true);
if (!recPathMap.isEmpty()){
recPathMap.keySet().forEach(rsc -> {
FileSystemResource res = new FileSystemResource(new File(recPathMap.get(rsc)));
try {
mimeMessageHelper.addInline(rsc, res);
} catch (MessagingException e) {
e.printStackTrace();
}
});
}
javaMailSender.send(mimeMessage);
}
/**
* 发送邮件工具类(发送HTML邮件)
*
* @param toEmail 收件人
* @param title 邮件标题
* @param msg 邮件内容
*/
public void sendHtmlMail(String toEmail,String title, String msg) throws MessagingException {
MimeMessage mimeMessage = javaMailSender.createMimeMessage();
MimeMessageHelper mimeMessageHelper = new MimeMessageHelper(mimeMessage, true);
mimeMessageHelper.setFrom(fromUserName);
mimeMessageHelper.setTo(toEmail);
mimeMessageHelper.setSubject(title);
mimeMessageHelper.setText(msg, true);
javaMailSender.send(mimeMessage);
}
/**
* 发送邮件工具类(发送带附件的邮件)
* 如果有多个附件,同样可以改变传参方式,使用集合多次调用helper.addAttachment(fileName, file);如多个图片的实现方式
*
* @param toEmail 收件人
* @param title 标题
* @param msg 内容
* @param filePath 附件路径
*/
public void sendAttachmentsMail(String toEmail,String title, String msg, String filePath) throws MessagingException {
MimeMessage mimeMessage = javaMailSender.createMimeMessage();
MimeMessageHelper mimeMessageHelper = new MimeMessageHelper(mimeMessage, true);
mimeMessageHelper.setFrom(fromUserName);
mimeMessageHelper.setTo(toEmail);
mimeMessageHelper.setSubject(title);
mimeMessageHelper.setText(msg, true);
FileSystemResource file = new FileSystemResource(new File(filePath));
mimeMessageHelper.addAttachment(file.getFilename(), file);
javaMailSender.send(mimeMessage);
}
}
然后提供相关的发送邮件的接口
/**
* 发送邮件服务
*
* @author LiuYu
* @date 2022/5/15 10:38
*/
@RestController
@RequestMapping("/email")
public class EmailController {
@Autowired
private IEmailService emailService;
@PostMapping("/{toEmail}/{code}")
public ResultDTO<String> sendEmailByCode(@PathVariable("toEmail") String toEmail, @PathVariable("code") String code){
emailService.sendEmailByCode(toEmail, code);
return ResultUtils.SUCCESS();
}
}
/**
* @author LiuYu
* @date 2022/5/15 11:12
*/
@Service
public class EmailServiceImpl implements IEmailService {
@Autowired
private SendMailUtil sendMailUtil;
@Override
public void sendEmailByCode(String toEmail, String code) {
String title = "注册用户验证码";
String msg = "当前注册的验证码为:【" + code + "】,10分钟内有效,请不要泄露给其他人!";
sendMailUtil.sendSimpleMail(toEmail, title, msg);
}
}
测试邮件发送功能
/**
* @author LiuYu
* @date 2022/5/15 11:27
*/
@SpringBootTest(classes = {EmailApplication.class})
@RunWith(SpringJUnit4ClassRunner.class)
public class EmailServiceImplTest {
@Autowired
private IEmailService emailService;
@Test
public void sendEmailByCode() {
emailService.sendEmailByCode("****@qq.com", "123456");
}
}
正常接收到邮件
2.验证码服务
定义对应的controller层
/**
* @author LiuYu
* @date 2022/5/15 11:37
*/
@RestController
@RequestMapping("/code")
public class CodeController {
@Autowired
private ICodeService codeService;
@PostMapping("/create/{toEmail}")
public ResultDTO createCode(@PathVariable("toEmail") String toEmail){
try {
codeService.createCode(toEmail);
} catch (Exception e) {
e.printStackTrace();
return ResultUtils.ERROR(BaseEnum.CREATE_CODE_ERROR);
}
return ResultUtils.SUCCESS();
}
@PostMapping("/validate/{toEmail}/{code}")
public ResultDTO validateCode(@PathVariable("toEmail")String toEmail, @PathVariable("code")String code){
final int result;
try {
result = codeService.validateCode(toEmail, code);
} catch (Exception e) {
e.printStackTrace();
return ResultUtils.ERROR(BaseEnum.VALIDATE_CODE_ERROR);
}
return ResultUtils.SUCCESS(result);
}
}
具体的service层实现逻辑
@Service
public class CodeServiceImpl implements ICodeService {
@Autowired
private LagouAuthCodeDao lagouAuthCodeDao;
@Autowired
private IEmailService emailService;
@Override
public void createCode(String toEmail) {
// 随机生成一个6位数的验证码,10分钟内有效
Random random = new Random();
final int randomCode = random.nextInt(1000000);
LagouAuthCode lagouAuthCode = new LagouAuthCode();
lagouAuthCode.setEmail(toEmail);
lagouAuthCode.setCode(String.valueOf(randomCode));
lagouAuthCode.setCreateTime(new Date());
lagouAuthCode.setExpireTime(DateUtils.addMinutes(new Date(), 10));
lagouAuthCodeDao.save(lagouAuthCode);
// 发送验证码至邮件
emailService.sendEmailByCode(toEmail, String.valueOf(randomCode));
}
@Override
public int validateCode(String toEmail, String code) {
LagouAuthCode lagouAuthCode = new LagouAuthCode();
lagouAuthCode.setEmail(toEmail);
lagouAuthCode.setCode(code);
Example<LagouAuthCode> example = Example.of(lagouAuthCode);
Sort sort = Sort.by(Sort.Direction.DESC, "expireTime");
final List<LagouAuthCode> all = lagouAuthCodeDao.findAll(example, sort);
if(CollectionUtils.isEmpty(all)){
// 当前email没有查询到对应的code,则为验证码错误
return 1;
}
final LagouAuthCode lac = all.get(0);
final Date expireTime = lac.getExpireTime();
if(new Date().after(expireTime)){
// 验证码超时,超过了10分钟
return 2;
}
return 0;
}
}
service内部使用的dao层和email服务的接口
/**
* @author LiuYu
* @date 2022/5/15 11:51
*/
public interface LagouAuthCodeDao extends JpaRepository<LagouAuthCode, Integer> {
}
通过feign请求的方式
/**
* @author LiuYu
* @date 2022/5/15 15:52
*/
@FeignClient(value = "lagou-service-email", fallback = EmailServiceFallBack.class, path = "/email")
public interface IEmailService {
@PostMapping("/{toEmail}/{code}")
ResultDTO sendEmailByCode(@PathVariable("toEmail") String toEmail, @PathVariable("code") String code);
}
对应的EmailServiceFallBack(服务降级)方法
/**
* @author LiuYu
* @date 2022/5/15 15:54
*/
@Component
public class EmailServiceFallBack implements IEmailService {
@Override
public ResultDTO sendEmailByCode(String toEmail, String code) {
return ResultUtils.SUCCESS();
}
}
测试生成验证码和校验验证码的功能
/**
* @author LiuYu
* @date 2022/5/15 16:27
*/
@SpringBootTest(classes = {CodeApplication.class})
@RunWith(SpringJUnit4ClassRunner.class)
public class CodeServiceImplTest {
@Autowired
private ICodeService codeService;
@Test
public void createCode() {
codeService.createCode("******@qq.com");
}
@Test
public void validateCode() {
final int result = codeService.validateCode("******@qq.com", "804959");
System.out.println("result = " + result);
}
}
正常收到邮件
验证码校验通过,正常返回数据
3.用户服务
定义对应的controller层
/**
* @author LiuYu
* @date 2022/5/15 09:50
*/
@RestController
@RequestMapping("/user")
public class UserController {
@Autowired
private IUserService userService;
@PostMapping("/register/{email}/{passWord}/{code}")
public ResultDTO register(@PathVariable("email") String email,
@PathVariable("passWord") String passWord,
@PathVariable("code") String code,
HttpServletResponse response){
return userService.register(email, passWord, code, response);
}
@GetMapping("/isRegistered/{email}")
public ResultDTO isRegistered(@PathVariable("email") String email){
return ResultUtils.SUCCESS(userService.isRegistered(email));
}
@PostMapping("/user/login/{email}/{passWord}")
public ResultDTO login(@PathVariable("email") String email, @PathVariable("passWord") String passWord,
HttpServletResponse response){
return ResultUtils.SUCCESS(userService.login(email, passWord, response));
}
@GetMapping("/user/info/{token}")
public ResultDTO userInfo(@PathVariable("token") String token){
return ResultUtils.SUCCESS(userService.userInfo(token));
}
}
定义对应的service层
/**
* @author LiuYu
* @date 2022/5/15 22:04
*/
@Service
public class UserServiceImpl implements IUserService {
@Autowired
private ICodeService codeService;
@Autowired
private LagouTokenDao lagouTokenDao;
/**
* 注册接⼝,true成功,false失败
*
* @param email
* @param passWord
* @param code
*/
@Override
public ResultDTO register(String email, String passWord, String code, HttpServletResponse response) {
// 验证验证码是否正确
final ResultDTO resultDTO = codeService.validateCode(email, code);
// 验证码服务异常
if(!Objects.equals(BaseEnum.SUCCESS.code(), resultDTO.getCode())){
return ResultUtils.ERROR(BaseEnum.REGISTER_ERROR);
}
// 验证码错误
if(Objects.equals(resultDTO.getData(), 1)){
return ResultUtils.ERROR(BaseEnum.VALIDATE_CODE_ERROR);
}
// 验证码超时,超过10分钟
if(Objects.equals(resultDTO.getData(), 2)){
return ResultUtils.ERROR(BaseEnum.VALIDATE_CODE_TIMEOUT);
}
// 验证码正确,开始注册账户,生成token令牌
final String token = DigestUtils.md5Hex(email+passWord);
LagouToken lagouToken = new LagouToken();
lagouToken.setToken(token);
lagouToken.setEmail(email);
lagouTokenDao.save(lagouToken);
Cookie cookie = new Cookie("user_token", token);
response.addCookie(cookie);
return ResultUtils.SUCCESS();
}
/**
* 是否已注册,根据邮箱判断,true代表已经注册过,
* false代表尚未注册
*
* @param email
* @return
*/
@Override
public boolean isRegistered(String email) {
LagouToken lagouToken = new LagouToken();
lagouToken.setEmail(email);
final Optional<LagouToken> one = lagouTokenDao.findOne(Example.of(lagouToken));
return one.isPresent();
}
/**
* 登录接⼝,验证⽤户名密码合法性,根据⽤户名和
* 密码⽣成token,token存⼊数据库,并写⼊cookie
* 中,登录成功返回邮箱地址,重定向到欢迎⻚
*
* @param email
* @param passWord
* @return
*/
@Override
public String login(String email, String passWord, HttpServletResponse response) {
final String token = DigestUtils.md5Hex(email+passWord);
LagouToken lagouToken = new LagouToken();
lagouToken.setToken(token);
lagouToken.setEmail(email);
final Optional<LagouToken> one = lagouTokenDao.findOne(Example.of(lagouToken));
if(one.isPresent()){
Cookie cookie = new Cookie("user_token", token);
response.addCookie(cookie);
return email;
}
return null;
}
/**
* 根据token查询⽤户登录邮箱接⼝
*
* @param token
* @return
*/
@Override
public String userInfo(String token) {
LagouToken lagouToken = new LagouToken();
lagouToken.setToken(token);
final Optional<LagouToken> one = lagouTokenDao.findOne(Example.of(lagouToken));
return one.orElse(new LagouToken()).getEmail();
}
}
对应的dao层
/**
* @author LiuYu
* @date 2022/5/16 09:27
*/
public interface LagouTokenDao extends JpaRepository<LagouToken, Integer> {
}
通过feign请求的方式请求code服务接口
/**
* @author LiuYu
* @date 2022/5/15 22:15
*/
@FeignClient(value = "lagou-service-code", fallback = CodeServiceFallBack.class, path = "/code")
public interface ICodeService {
@PostMapping("/validate/{toEmail}/{code}")
ResultDTO validateCode(@PathVariable("toEmail")String toEmail, @PathVariable("code")String code);
}
对应的服务降级方法为
/**
* @author LiuYu
* @date 2022/5/15 22:17
*/
@Component
public class CodeServiceFallBack implements ICodeService {
@Override
public ResultDTO validateCode(String toEmail, String code) {
return ResultUtils.SUCCESS(0);
}
}
测试对应的接口实现
/**
* @author LiuYu
* @date 2022/5/16 21:25
*/
@SpringBootTest(classes = {UserApplication.class})
@RunWith(SpringJUnit4ClassRunner.class)
public class UserServiceImplTest {
@Autowired
private IUserService userService;
@Autowired
private HttpServletResponse response;
private static final String EMAIL = "****@qq.com";
private static final String PASS_WORD = "123456";
@Test
public void register() {
final ResultDTO register = userService.register(EMAIL, PASS_WORD, "804959", response);
System.out.println("register = " + register);
}
@Test
public void isRegistered() {
final boolean registered = userService.isRegistered(EMAIL);
System.out.println("registered = " + registered);
}
@Test
public void login() {
final String login = userService.login(EMAIL, PASS_WORD, response);
System.out.println("login = " + login);
}
@Test
public void userInfo() {
final String userInfo = userService.userInfo("e5f2a8136ece06ad8fddc2760443d666");
System.out.println("userInfo = " + userInfo);
}
}
-
注册接口,成功返回对应的数据并插入数据库
- 是否注册过接口, 使用已经注册过的email进行测试,正常返回结果
-
登录接口,使用注册的email和密码进行登录测试,正常登录成功返回email信息
-
用户信息接口,使用注册生成token进行用户信息的查询,结果正常返回email
6.gateway网关相关逻辑实现
-
gateway实现ip防爆刷限制
/** * @author LiuYu * @date 2022/5/16 22:27 */ @Slf4j @Component public class IpFilter implements GlobalFilter, Ordered { @Value("${minute:10}") private Integer minute; @Value("${number:3}") private Integer number; private final Map<String, List<LocalDateTime>> IP_MAP = new HashMap<>(); @Override public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) { // 获取对应的客户端IP地址 final ServerHttpRequest request = exchange.getRequest(); final ServerHttpResponse response = exchange.getResponse(); final String hostString = request.getRemoteAddress().getHostString(); final String path = request.getURI().getPath(); if(path.contains("register")){ final List<LocalDateTime> dates = IP_MAP.get(hostString); // 如果当前ip第一次请求,则进行记录然后放行 if(CollectionUtils.isEmpty(dates)){ log.error("当前ip:【{}】第一次请求注册接口允许放行.", hostString); List<LocalDateTime> nowDates = new ArrayList<>(); nowDates.add(LocalDateTime.now()); IP_MAP.put(hostString, nowDates); return chain.filter(exchange); } // 如果这里注册的次数小于配置的次数,则直接放行 if(dates.size() < number){ log.error("当前ip:【{}】,请求第【{}】次注册接口,小于限制【{}】次,允许放行.", hostString, dates.size(), number); return chain.filter(exchange); } final LocalDateTime localDateTime = dates.get(number); if(LocalDateTime.now().minusMinutes(minute).isBefore(localDateTime)){ // 如果当前ip在minute分钟内注册超过number次,则不允许当前注册请求 response.setStatusCode(HttpStatus.SEE_OTHER); log.error("当前ip:【{}】在【{}】分钟内注册超过【{}】次,不允许放行.", hostString, minute, number); String data = "您频繁进行注册,请求已被拒绝"; final DataBuffer wrap = response.bufferFactory().wrap(data.getBytes()); return response.writeWith(Mono.just(wrap)); } dates.add(LocalDateTime.now()); } return chain.filter(exchange); } @Override public int getOrder() { return 0; } }
7. 配置nginx反向代理服务器
-
由于都是基于windows进行测试和开发,所以这里下载了windows版的nginx进行配置使用
下载链接:http://nginx.org/en/download.html, 选择对应的版本进行下载即可
-
下载解压之后打开conf文件夹中nginx.conf文件进行本次需要的配置
-
首先配置server_name为www.test.com, 设置允许代理的时候携带header信息(cookie等) proxy_set_header Host $host;
-
设置拦截/api开头的请求转发到 gateway网关项目
-
设置拦截/static/开头的静态资源请求到本地的目录
upstream login-gateway{ server 127.0.0.1:9002; } server { listen 80; server_name www.test.com; #charset koi8-r; #access_log logs/host.access.log main; proxy_set_header Host $host; #location / { # root html; # index index.html index.htm; #} # 拦截动态请求 location /api { proxy_pass http://login-gateway; } #拦截静态请求 location /static/ { root C:/Users/liu.yu/Desktop/html; } }
3. 项目部署和启动
-
部署nginx服务
-
由于使用到了springcloud config bus 需要部署rabbitmq
-
启动服务需要按照顺序启动
- 启动 eureka server
- 启动config server
- 启动 gateway
- 启动 email、code、user服务
-
最终的项目结构是如下这个样子的
4. 具体的演示和使用
- 登录页面
- 注册页面
- 验证码过期:手动将验证码的过期时间调整为已过期
- 验证码错误:
- 用户已注册:
- Ip防爆刷:由于当前在configServer中配置的是1分钟内最多允许注册15次,一旦超过就会在网关gateWay进行请求拦截
- 欢迎页面: 登录成功显示登录邮箱和token
- 演示视频
https://gitee.com/struggle_ly/lagou-cloud-first-generation/blob/master/%E6%A8%A1%E5%9D%9710springCloud%E7%AC%AC%E4%B8%80%E4%BB%A3%E7%BB%84%E4%BB%B6.mp4
5. 踩坑
-
配置nginx时,指定的server_name为www.test.com域名,在使用chrome进行测试的时候打不开http://www.test.com/static/login.html 静态资源页面
解决: 尝试使用postman或者更换浏览器进行尝试,或者重启nginx,其次就是配置的nginx路径是否能找到,/static/对应的路径为root C:/Users/liu.yu/Desktop/html, 其进行查找的时候会去C:/Users/liu.yu/Desktop/html/static/login.html对应的路径进行查找,验证路径是否正确,
最后还有一点,如果本机使用了代理/VPN,也会导致nginx不能正常访问
-
登录成功token写入cookie失败
解决:首先在nginx中添加proxy_set_header Host $host; 允许nginx在转发的时候携带cookie等信息,其次是在后端进行添加cookie的时候设置对应cookie的path为/
Cookie cookie = new Cookie("token", token); cookie.setPath("/"); cookie.setMaxAge(36000); response.addCookie(cookie);
-
springcloud config对应的client无法正常获取server端的配置信息
解决:client端的配置文件中server的uri不能设置eureka server的名称,需要设置对应的ip+port, 对应的使用配置的类上添加@RefreshScope注解