【文章内容输出来源:拉勾教育Java高薪训练营】
--- 所有脑图均本人制作,未经允许请勿滥用 ---
需要深刻把握各组件的职责和使用方式,多写多练
夫学须静也,才须学也,非学无以广才,非志无以成学
一、初代 Spring Cloud 核心组件(II)
Part 5 - GateWay 网关组件
「GateWay 简述」
Spring Cloud GateWay 是 Spring Cloud 的⼀个全新项目,目标是取代 Netflix Zuul,它基于 Spring5.0+SpringBoot2.0+WebFlux(基于高性能的 Reactor模式 响应式通信框架 Netty,异步非阻塞模型)等技术开发,性能高于Zuul,官方测试,GateWay 是 Zuul 的1.6倍,旨在为微服务架构提供⼀种简单有效的统⼀的API路由管理方式
Spring Cloud GateWay不仅提供统⼀的路由方式(反向代理)并且基于 Filter(定义过滤器对请求过滤,完成⼀些功能) 链的方式提供了网关基本的功能,例如:鉴权、流量控制、熔断、路径重写、日志监控等。
网关在微服务架构中的位置
【核心概念】
- 路由(route)
由 一个ID + 一个目标URL(最终路由地址) + 一系列断言(匹配条件) + 过滤器(精细化控制) 组成;
断言为 true 时,路由匹配 √ - 断言(predicates)
匹配 Http 请求中的所有内容(包括请求头、请求参数等) - 过滤器(filter)
在请求 前/后 执行业务逻辑
【核心逻辑】 规则匹配 + 路由转发 + 执行过滤器链
【常用业务】
- 限流
- 日志
- 黑名单
「GateWay 应用」
!GateWay 不要使用 web 模块,它引入的是WebFlux(类似于SpringMVC)
所以我们直接建立一个新的模块,而不是在 父工程下建立
新建 Module cloud-hystrix-turbine-9003
- 导入依赖 (除了webflux,其他依赖都和父工程一样)
<!--spring boot 父启动器依赖-->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.6.RELEASE</version>
</parent>
<dependencies>
<!-- spring cloud commons 依赖引入 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-commons</artifactId>
</dependency>
<!-- Eureka Client 客户端依赖引入-->
<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>
<!-- 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>
</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>8</source>
<target>8</target>
<encoding>utf-8</encoding>
</configuration>
</plugin>
<!--打包插件-->
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
- yml 配置
spring:
application:
name: lagou-cloud-gateway
cloud:
gateway:
routes: # 路由可以有多个
- id: service-autodeliver-router # 我们自定义的路由 ID,保持唯一
# uri: http://127.0.0.1:8096 # 目标服务地址 自动投递微服务(部署多实例)
# 但是 -> 动态路由:uri配置的应该是一个服务名称,而不应该是一个具体的服务实例的地址
uri: lb://lagou-service-autodeliver # gateway网关从服务注册中心获取实例信息然后负载后路由
predicates: # 断言:路由条件,Predicate 接受一个输入参数,返回一个布尔值结果。该接口包含多种默 认方法来将 Predicate 组合成其他复杂的逻辑(比如:与,或,非)。
- Path=/autodeliver/**
- id: service-resume-router # 我们自定义的路由 ID,保持唯一
# uri: http://127.0.0.1:8081 # 目标服务地址
# 但是 -> 动态路由:uri配置的应该是一个服务名称,而不应该是一个具体的服务实例的地址
uri: lb://lagou-service-resume
predicates: # 断言:路由条件,Predicate 接受一个输入参数,返回一个布尔值结果。该接口包含多种默 认方法来将 Predicate 组合成其他复杂的逻辑(比如:与,或,非)。
- Path=/resume/**
filters:
- StripPrefix=1
【说明】 其中使用的 uri 是动态路由。lb 代表 => 从注册中心获取服务;后面加上 目标服务名称
上面这段配置的意思是,配置了⼀个 id 为 service-autodeliver-router 的路由规则,当向网关发起请求 http://localhost:9003/autodeliver/checkState/1545132 请求会被分发路由到对应的微服务上
「GateWay 路由」
-
路由匹配规则
- DateTime 时间断言
- Cookie 断言
- Header 请求头断言
- Host 请求主机断言
- Method 请求方法断言
- Path 请求路径断言
- QueryParam 请求参数断言
- RemoteAddr 远程地址断言
-
简单使用
- 时间点后匹配
spring:
cloud:
gateway:
routes:
- id: after_route
uri: https://example.org
predicates:
- After=2021-01-20T17:42:47.789-07:00[America/Denver]
- 时间点前匹配
spring:
cloud:
gateway:
routes:
- id: before_route
uri: https://example.org
predicates:
- Before=2021-01-20T17:42:47.789-07:00[America/Denver]
- 时间区间匹配
spring:
cloud:
gateway:
routes:
- id: between_route
uri: https://example.org
predicates:
- Between=2021-01-20T17:42:47.789-07:00[America/Denver],2021-01-21T17:42:47.789-07:00[America/Denver]
- 指定正则Cookie匹配
spring:
cloud:
gateway:
routes:
- id: cookie_route
uri: https://example.org
predicates:
- Cookie=chocolate, ch.p
- 指定Header匹配
spring:
cloud:
gateway:
routes:
- id: header_route
uri: https://example.org
predicates:
- Header=X-Request-Id, \d+
- 请求Host匹配
spring:
cloud:
gateway:
routes:
- id: host_route
uri: https://example.org
predicates:
- Host=**.somehost.org,**.anotherhost.org
- 请求Method匹配
spring:
cloud:
gateway:
routes:
- id: method_route
uri: https://example.org
predicates:
- Method=GET,POST
- 请求路径正则匹配
spring:
cloud:
gateway:
routes:
- id: path_route
uri: https://example.org
predicates:
- Path=/red/{segment},/blue/{segment}
- 请求包含参数匹配
spring:
cloud:
gateway:
routes:
- id: query_route
uri: https://example.org
predicates:
- Query=green
- 请求包含参数并且参数值匹配正则
spring:
cloud:
gateway:
routes:
- id: query_route
uri: https://example.org
predicates:
- Query=red, gree.
- 远程地址匹配
spring:
cloud:
gateway:
routes:
- id: remoteaddr__route
uri: https://example.org
predicates:
- RemoteAddr=192.168.1.1/24
「GateWay 过滤器」
【影响时机分类】 两个 pre 和 post
- pre过滤器:在请求被路由之前调用
- 身份验证
- 集群选择微服务
- 记录调试信息
- post过滤器:微服务执行完调用
- 为响应 添加标注你的 HTTP Header
- 收集统计信息和指标
- 将响应从微服务分发到客户端
【类型分类】
- GateWayFilter:影响单个路由
- GlobalFilter:影响所有路由
以下语句的功能就属于GateWay Filter,可以去掉url中的占位后转发路由
predicates:
- Path=/resume/**
filters:
- StripPrefix=1
「GateWay 过滤器实例(黑名单)」
我们常用的都是 GlobalFilter,下面就以黑名单功能为例实现功能
@Component
public class BlackListFilter implements GlobalFilter, Ordered {
// 模拟黑名单(实际可以去数据库或者redis中查询)
private static List<String> blackList = new ArrayList<>();
static {
blackList.add("0:0:0:0:0:0:0:1"); // 模拟本机地址
}
/**
* 过滤器核心方法
* @param exchange 封装了request和response对象的上下文
* @param chain 网关过滤器链(包含全局过滤器和单路由过滤器)
* @return
*/
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
// 1. 获取客户端ip,判断是否在黑名单中
// yes --> 拒绝访问
// no --> 放行
ServerHttpRequest request = exchange.getRequest();
ServerHttpResponse response = exchange.getResponse();
String clientIP = request.getRemoteAddress().getHostString(); // 客户端ip拿到
if (blackList.contains(clientIP)) {
// 拒绝访问
response.setStatusCode(HttpStatus.UNAUTHORIZED); // 状态码拿到
String data = "Request be denied !";
DataBuffer wrap = response.bufferFactory().wrap(data.getBytes());
return response.writeWith(Mono.just(wrap));
}
// 放行,执行后续过滤器
return chain.filter(exchange);
}
/**
* 返回值表示 当前过滤器的 顺序(优先级),数值越小,优先级越高
* @return
*/
@Override
public int getOrder() {
return 0;
}
}
「GateWay 高可用」
网关如果挂掉了,后续的所有功能都会失效;所以!网关必须 高可用
可以启动多个GateWay实例来实现高可用,在GateWay的上游使用 Nginx等负载均衡设备 进行负载转发以达到高可用的目的
nginx.conf
#配置多个GateWay实例
upstream gateway {
server 127.0.0.1:9002;
server 127.0.0.1:9003;
}
location / {
proxy_pass http://gateway;
}
Part 6 - Spring Cloud Config 分布式配置中心
「应用场景」
- ⼀个微服务架构中可能有成百上千个微服务,所以集中配置管理是很重要的
- 不同环境不同配置,比如数据源配置在不同环境(开发dev, 测试test, 生产prod)
- 运行期间可动态调整。例如,可根据各个微服务的负载情况,动态调整数据源连接池大小等配置修改后可自动更新
- 如配置内容发生变化,微服务可以自动更新配置
「Spring Cloud Config 综述」
- Server端:
提供配置文件的存储、以接口的形式将配置文件的内容提供出去,使用 @EnableConfigServer 注解嵌入SpringBoot - Client端:
通过接口获取配置数据并初始化自己的应用
「实现」
现在,我们对 “简历微服务” 的application.yml进行配置管理(开发环境、测试环境、生成环境)
- 在自己的git或者码云中创建项目
- 上传yml配置文件,命名规则如下:
- {application}-{profile}.yml
- {application}-{profile}.properties
- 说明 --> application:应用名称 / profile:环境
- pom 依赖引入
<dependency>
<artifactId>service-common</artifactId>
<groupId>com.archie</groupId>
<version>1.0-SNAPSHOT</version>
</dependency>
<!-- Eureka Client 客户端依赖引入-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<!-- config配置中心服务端 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-config-server</artifactId>
</dependency>
- 启动类用注解声明
@SpringBootApplication
@EnableDiscoveryClient
@EnableConfigServer // 开启配置管理
public class ConfigApplication9006 {
public static void main(String[] args) {
SpringApplication.run(ConfigApplication9006.class, args);
}
}
- yml 配置管理
spring:
cloud:
config:
server:
git:
uri: https://github.com/5173098004/lagou-config-repo.git #配置git服务地址
username: 517309804@qq.com #配置git用户名
password: '******' #配置git密码
search-paths:
- lagou-config-repo
name: config-server #配置文件名称
uri: http://localhost:9006 #ConfigServer配置中心地址
label: master #分支名称
profile: dev #后缀名称
- 访问文件测试
http://127.0.0.1:9006/master/lagou-service-resume-dev.yml
- 在简历微服务中添加依赖坐标
<!-- Config 客户端 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-config-client</artifactId>
</dependency>
- application.yml 修改为 bootstrap.yml 配置文件并添加 config坐标
bootstrap.yml是系统级别的,优先级比 application.yml 高,应用启动时会检查这个配置文件,在这个配置文件中指定配置中心的服务地址,会自动拉取所有应用配置并且启用
spring:
cloud:
# config客户端配置,和ConfigServer通信,并告知ConfigServer希望获取的配置信息在哪个文件中
config:
name: lagou-service-resume #配置文件名称
profile: dev #后缀名称
label: master #分支名称
uri: http://localhost:9006 #ConfigServer配置中心地址
「Config 配置手动刷新」
- Client客户端添加依赖 springboot-starter-actuator
<!-- Actuator可以帮助你监控和管理Spring Boot应用-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
- Client客户端 bootstrap.yml 中添加配置(暴露通信端点)
# 暴露所有的端⼝
management:
endpoints:
web:
exposure:
include: "*" # 也可以指定暴露断点,e.g. include: refresh
- Client客户端使用到配置信息的类上添加
@RefreshScope
@RestController
@RequestMapping("/config")
@RefreshScope
public class ConfigController {
// 和取本地配置信息一样
@Value("${lagou.message}")
private String lagouMessage;
@Value("${mysql.url}")
private String mysqlUrl;
// 内存级别的配置信息
// 数据库,redis配置信息
@GetMapping("/viewconfig")
public String viewconfig() {
return "lagouMessage==>" + lagouMessage + " mysqlUrl=>" + mysqlUrl;
}
}
- 手动向 Client客户端 发起 POST请求 http://localhost:8080/actuator/refresh
「Config 配置自动更新」
RabbitMQ 的安装请 Learning online
⼀次通知处处生效
结合消息总线(Bus)实现分布式配置的自动更新(Spring Cloud Config + Spring Cloud Bus)
所谓消息总线Bus,即我们经常会使用 MQ消息代理 构建⼀个共用的Topic,通过这个 Topic连接各个微服务实例,MQ 广播的消息会被所有在注册中心的微服务实例监听和消费。
换言之就是通过⼀个主题连接各个微服务,打通脉络。
Spring Cloud Bus(基于MQ的,支持 RabbitMq/Kafka) 是Spring Cloud中的消息总线方案
- Config Server服务端 添加消息总线支持
<!-- 消息总线 bus ⽀持 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-bus-amqp</artifactId>
</dependency>
- ConfigServer 添加配置
rabbitmq:
host: 127.0.0.1
port: 5672
username: guest
password: guest
- 微服务暴露端口
# 暴露所有的端⼝
management:
endpoints:
web:
exposure:
include: "*" # 也可以指定暴露断点,e.g. include: refresh
- 重启各个服务,更改配置之后,向配置中心服务端发送 post请求 http://localhost:9003/actuator/bus-refresh 各个客户端配置即可自动刷新
若想定向更新而不是全部更新:
在发起刷新请求的时候 http://localhost:9006/actuator/bus-refresh/lagou-service-resume:8081 即为最后面跟上要定向刷新的实例的 服务名:端口号即可
Part 7 - Spring Cloud Stream 消息驱动组件
Spring Cloud Stream 消息驱动组件帮助我们更快速,更方便,更友好的去构建消息驱动微服务的。
- MQ:消息队列/消息中间件/消息代理,产品有很多,ActiveMQ RabbitMQ RocketMQ Kafka
「Stream 解决的痛点问题」
- 不同的MQ消息中间件内部机制包括使用方式都会有所不同,比如RabbitMQ中有
Exchange(交换机/交换器)这⼀概念,kafka 有Topic、Partition分区这些概念, - MQ消息中间件的差异性不利于我们上层的开发应用,当我们的系统希望从原有的 RabbitMQ 切换到 Kafka 时,我们会发现比较困难,很多要操作可能重来(因为应用程序和具体的某⼀款MQ消息中间件耦合在⼀起了)
- Spring Cloud Stream 进行了很好的上层抽象,可以让我们与具体消息中间件解耦合,屏蔽掉了底层具体MQ消息中间件的细节差异,就像Hibernate 屏蔽掉了具体数据库(Mysql/Oracle⼀样)
如此⼀来,我们学习、开发、维护MQ都会变得轻松。目前 Spring Cloud Stream 支持 RabbitMQ 和 Kafka
【本质】:
屏蔽掉了底层不同 MQ消息中间件之间的差异,统⼀了MQ的编程模型,降低了学习、开发、维护MQ的成本
「Stream 重要概念」
【SC Cloud 定义】
是⼀个构建消息驱动微服务的框架
通过 inputs / outputs 来与 SC Cloud 中的 binder对象 交互(binder对象用于屏蔽底层MQ细节)
- 每个input关联一个 消费者
- 每个output关联一个 生产者
- Stream中提供了不同的Binder,当我们需要切换MQ的时候,我们只切换Binder即可
- "Middleware" ≈ RabbitMQ / Kafka
「传统 MQ模型 与 Stream消息驱动模型」
「Stream消息通信方式 及 编程模型」
Stream中的消息通信方式遵循了 发布—订阅模式
当⼀条消息被投递到消息中间件之后,它会通过共享的 Topic 主题进行广播,消息消费者在订阅的主题中收到它并触发自身的业务逻辑处理。
【"Topic"】:
是Spring Cloud Stream中的⼀个抽象概念,用来代表发布共享消息给消费者的地方
- RabbitMQ中的"Topic" ==>
Exchange- Kafka中的"Topic" ==>
Topic
【Stream中的相关注解】:
@Input标识输入通道,接受到的消息 ==输入通道==> 应用程序@Output标识输出通道,应用程序 ==输出通道==> 发布的消息@StreamListner监听队列@EnableBinding绑定 Channel 和 Exchange
以上注解无非在做⼀件事:
把我们结构图中那些组成部分上下关联起来,打通通道(这样的话⽣产者的 message数据才能进⼊mq,mq中数据才能进⼊消费者工程)
「Stream 使用事例」
建立三个module
- cloud-stream-producer-9090, 作为生产者端发消息
- cloud-stream-consumer-9091,作为消费者端接收消息
- cloud-stream-consumer-9092,作为消费者端接收消息
生产者端
- pom 引入依赖
<!--eureka client 客户端依赖引入-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<!--spring cloud stream 依赖(rabbit)-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-stream-rabbit</artifactId>
</dependency>
- application.yml 配置
server:
port: 9090
spring:
application:
name: cloud-stream-producer
cloud:
stream:
binders: # 绑定MQ服务信息(此处我们是RabbitMQ)
lagouRabbitBinder: # 给Binder定义的名称,用于后面的关联
type: rabbit # MQ类型,如果是Kafka的话,此处配置kafka
environment: # MQ环境配置(用户名、密码等)
spring:
rabbitmq:
host: localhost
port: 5672
username: guest
password: guest
bindings: # 关联整合通道和binder对象
output: # output是我们定义的通道名称,此处不能乱改
destination: lagouExchange # 要使用的Exchange名称(消息队列主题名称)
content-type: text/plain # application/json # 消息类型设置,比如json
binder: lagouRabbitBinder # 关联MQ服务
# 注册到 Eureka 服务中心
eureka:
client:
service-url:
# 注册到集群,用 [,] 分隔
defaultZone: http://myEurekaA:8761/eureka/,http://myEurekaB:8762/eureka/
instance:
# 服务实例中显示ip,而非主机名(为了兼容老版本)
prefer-ip-address: true
# 可以对实例名称自定义
instance-id: ${spring.cloud.client.ip-address}:${spring.application.name}:${server.port}:@project.version@
- 启动入口
@SpringBootApplication
@EnableDiscoveryClient
public class StreamProducerApplication9090 {
public static void main(String[] args) {
SpringApplication.run(StreamProducerApplication9090.class,args);
}
}
- 业务类开发
public interface IMessageProducer {
public void sendMessage(String content);
}
// Source.class里面就是对输出通道的定义(这是Spring Cloud Stream内置的通道封装)
@EnableBinding(Source.class)
public class MessageProducerImpl implements IMessageProducer {
// 将MessageChannel的封装对象Source注入到这里使用
@Autowired
private Source source;
@Override
public void sendMessage(String content) {
// 向mq中发送消息(并不是直接操作mq,应该操作的是spring cloud stream)
// 使用通道向外发出消息(指的是Source里面的output通道)
source.output().send(MessageBuilder.withPayload(content).build());
}
}
消费者端
- 同生产者 pom
(略)
- application.yml 配置
唯二不同处:
- server.port 改动
- spring.cloud.stream.bindings.input 改 spring.cloud.stream.bindings.output
- 启动入口
(略)
- 消息消费者监听
@EnableBinding(Sink.class)
public class MessageConsumerService {
@StreamListener(Sink.INPUT)
public void recevieMessages(Message<String> message) {
System.out.println("=========接收到的消息:" + message);
}
}
「Stream 高级之 自定义消息通道」
Stream 内置了两种接口 Source 和 Sink 。分别定义了 binding 为 “input” 的输入流和“output” 的输出流,我们也可以自定义各种输入输出流(通道)
但实际我们会在我们的服务中使用多个binder、多个输入通道和输出通道,现在仅默认⼀个input的输入通道和⼀个output的输出通道,怎么办?
自消息通道! 学着Source和Sink的样子,给你的通道定义个自己的名字,多个输入通道和输出通道是可以写在⼀个类中的。
- 定义接口
interface CustomChannel {
String INPUT_LOG = "inputLog";
String OUTPUT_LOG = "outputLog";
@Input(INPUT_LOG)
SubscribableChannel inputLog();
@Output(OUTPUT_LOG)
MessageChannel outputLog();
}
- 使用
- 在 @EnableBinding 注解中,绑定自定义的接口
- 使用 @StreamListener 做监听的时候,需要指定 CustomChannel.INPUT_LOG
bindings:
inputLog:
destination: AExchange
outputLog:
destination: BExchange
二、Spring Cloud 高级实战
Part 1 - 微服务控制之 Turbine 聚合监控
参考 上篇 的 Hytrix 章节
Part 2 - 微服务监控之 分布式链路追踪技术 Sleuth + Zipkin
微服务架构中,一个功能往往涉及到多个乃至十几个微服务的调用,于是产生了以下问题:
- 如何动态展示服务的链路?
- 如何分析服务链路的瓶颈并进行调优?
- 如何快速迅速发现服务链路中的问题?
「分布式链路追踪」
一个请求的各个链路节点中,都记录日志,并将这些日志集中可视化
「市场上的分布式链路最终方案」
- Spring Cloud Sleuth + Twitter Zipkin
- 阿里 の “鹰眼”
- 大众点评 の “CAT”
- 美团 の “Mtrace”
- 京东 の “Hydra”
- 新浪 の “Watchman”
- Apache Skywalking
「核心思想」
【本质】:记录日志
- 所有的分布式链路系统都基于一个理念,这个理论来自Google的一篇论文——
《Dapper, a Large-Scale Distributed Systems Tracing Infrastructure》,其中的核心概念:- Trace : 以网关为边界,从request进入网关,直到response离开网关,这样一个回合,称之为 Trace;
- Trace ID :对每一个Trace进行追踪的标识。
- Span :封装日志数据的一个“跨度”,存在 开始 & 结束 节点;使用时间戳计算出 时间延迟,还有一些元数据等;
- Trace ID :对每一个Span进行追踪的标识。
- 一个 Trace 由多个 Span 组成;
- Span 往往单位就是每一个微服务;
- 每个 Span 中还存在 一个 ParentId,指向上一个 Span,表明父子关系。
【Sleuth】:
它能够记录一个服务请求经过了 哪些服务、时长多少等信息;然后我们根据这些信息,辨析各个微服务之间的调用关系并根据需求分析问题。
1. 耗时分析:使用采样请求,分析服务性能问题;
2. 链路优化:对于一些反复调用的服务进行性能优化。
Sleuth 将信息发送给 Zipkin 进行聚合,然后 Zipkin 存储并展示数据;
「Sleuth + Zipkin 应用」
- 每个要被追踪的微服务 引入 依赖坐标
<!--链路追踪-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-sleuth</artifactId>
</dependency>
- 每个要被追踪的微服务 修改 application.yml ,添加日志级别
logging:
level:
org.springframework.web.servlet.DispatcherServlet: debug
org.springframework.cloud.sleuth: debug
- Zipkin Server 模块构建
- 依赖坐标
<!--zipkin-server的依赖坐标-->
<dependency>
<groupId>io.zipkin.java</groupId>
<artifactId>zipkin-server</artifactId>
<version>2.12.3</version>
<exclusions>
<!--排除掉log4j2的传递依赖,避免和springboot依赖的日志组件冲突-->
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-log4j2</artifactId>
</exclusion>
</exclusions>
</dependency>
<!--zipkin-server ui界面依赖坐标-->
<dependency>
<groupId>io.zipkin.java</groupId>
<artifactId>zipkin-autoconfigure-ui</artifactId>
<version>2.12.3</version>
</dependency>
- 入口创建
@SpringBootApplication
@EnableZipkinServer // 开启 Zipkin 服务器功能
public class ZipkinApplication9411 {
public static void main(String[] args) {
SpringApplication.run(ZipkinApplication9411.class, args);
}
}
- yml配置
server:
port: 9411
spring:
application:
name: cloud-zipkin-server
management:
metrics:
web:
server:
auto-time-requests: false # 关闭自动检测
- 访问 zipkin server 界面
- Zipkin Client 实现
- 每个要被追踪的微服务 引入 client 依赖坐标
<!--链路追踪 Zipkin-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-zipkin</artifactId>
</dependency>
- 每个要被追踪的微服务的 yml 加入配置
spring:
zipkin:
base-url: http://127.0.0.1:9411 # zipkin server 的地址
sender:
# web 客户端将踪迹日志数据通过网络请求的方式传送到服务端,另外还有配置
# kafka / rabbit 客户端将踪迹日志数据传递到mq进行中转
type: web
sleuth:
sampler:
# 采样率 1 代表100%全部采集 ,默认0.1 代表10% 的请求踪迹数据会被采集
# 生产环境下,请求量非常大,没有必要所有请求的踪迹数据都采集分析,对于网络包括server端压力都是比较大的,可以配置采样率采集一定比例的请求的踪迹数据进行分析即可
probability: 1
「Zipkin server 的持久化」
一旦zipkin的服务下线了,之前的链路都将消失,这样会很不方便,所以可以使用持久化。(可以持久化到mysql等)
- 创建数据库,命名zipkin
- 创建数据库
执行语句后:
- 引入 pom 依赖
<!--zipkin针对mysql持久化的依赖-->
<dependency>
<groupId>io.zipkin.java</groupId>
<artifactId>zipkin-autoconfigure-storage-mysql</artifactId>
<version>2.12.3</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.1.10</version>
</dependency>
<!--操作数据库需要事务控制-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-tx</artifactId>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
</dependency>
- yml 配置增加
spring:
# 数据库连接信息
datasource:
username: root
password: root
url: jdbc:mysql://localhost:3306/zipkin?useUnicode=true&characterEncoding=utf-8&serverTimezone=GMT
driver-class-name: com.mysql.cj.jdbc.Driver
druid:
initial-size: 10
min-idle: 10
max-active: 30
max-wait: 50000
# 指定zipkin持久化介质为mysql
zipkin:
storage:
type: mysql
- 启动类 注入 配置管理器bean
@SpringBootApplication
@EnableZipkinServer // 开启 Zipkin 服务器功能
public class ZipkinApplication9411 {
public static void main(String[] args) {
SpringApplication.run(ZipkinApplication9411.class, args);
}
/**
* 注入事务控制器
* @param dataSource
* @return
*/
@Bean
public PlatformTransactionManager transactionManager(DataSource dataSource) {
return new DataSourceTransactionManager(dataSource);
}
}
- 重启并观察 zipkin 的三张表
Part 3 - 微服务统⼀认证方案 Spring Cloud OAuth2 + JWT
认证:只有当用户身份被验证合法时,才允许后续逻辑操作.
「微服务架构下的认证」
两种思路:
- 基于Session的认证
- 分布式中,每个应用服务都需要在自己的session中存储用户信息,通过负载均衡将本地的请求分配到另一个应用时,需要将session信息带过去,避免重复登录;
- 也可以使用session共享或者session粘贴;
- 缺点:移动端无法有效使用.
- 基于token的认证
- 服务端不用存储认证数据,而是由客户端将token令牌存储在任意地方,并且可以实现web和app的统一认证机制;
- 缺点一:token属于 自包含信息,数据量比较大,而且每次存储都需要携带,占用带宽较高;
- 缺点二:token的签名验证操作也给CPU带来了较大负担.
「OAuth2 开放授权协议/标准」
OAuth2 是一个开放协议,允许用户授权第三方访问其他服务提供者去获取部分基本内容;
e.g. 我们通过QQ登录未注册过的拉勾
- QQ -> 其他服务者 / 拉勾 -> 第三方;
- QQ平台颁发一些
参数给拉勾,供后续上线登录授权用;- 拉勾(相当于客户端)会拿到一个 client_id 当做临时账号
- 同时拉勾还会拿到一个 secret 当做临时密码
【OAuth2使用场景】
- 第三方授权;
- 单点登录;
「OAuth2 颁发token的授权方式」
- 授权码(authorization-code)☆☆
- 密码式(password)☆☆☆
- 隐藏式(implicit)
- 客户端凭证(client credentials)
授权码模式使用到了回调地址,比较麻烦;
接下来我们着重 说明接口对接中最常使用的 password 密码模式(用户名+密码 换取 token)
「Spring Cloud OAuth2 简介」
Spring Cloud OAuth2 是 Spring Cloud 体系对OAuth2协议的实现
通过向 OAuth2服务(统⼀认证授权服务)发送某个类型的 grant_type 进行集中认证和授权,从而获得 access_token(访问令牌),这个令牌是受其他微服务信任的
【OAuth2本质】: 引入了⼀个认证授权层,认证授权层连接了资源的拥有者,在授权层里面,资源的拥有者 可以给第三方应用授权去访问我们的某些受保护资源
「Spring Cloud OAuth2 应用实现」
【Demo 实现思路】:
搭建认证Module : cloud-oauth-server-9999
- 引入 pom 依赖
<dependency>
<artifactId>service-common</artifactId>
<groupId>com.archie</groupId>
<version>1.0-SNAPSHOT</version>
</dependency>
<!-- Eureka Client 客户端依赖引入-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<!--导入spring cloud oauth2依赖-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-oauth2</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.security.oauth.boot</groupId>
<artifactId>spring-security-oauth2-autoconfigure</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.security.oauth.boot</groupId>
<artifactId>spring-security-oauth2-autoconfigure</artifactId>
<version>2.1.11.RELEASE</version>
</dependency>
<!--引入security对oauth2的支持-->
<dependency>
<groupId>org.springframework.security.oauth</groupId>
<artifactId>spring-security-oauth2</artifactId>
<version>2.3.4.RELEASE</version>
</dependency>
- yml 配置
server:
port: 9999
spring:
application:
name: cloud-oauth-server
# 注册到 Eureka 服务中心
# ... (同其他模块)...
- 入口
@SpringBootApplication
@EnableDiscoveryClient
public class OauthApplication {
public static void main(String[] args) {
SpringApplication.run(OauthApplication.class, args);
}
}
- 认证服务器配置类
继承 AuthorizationServerConfigurerAdapter 并重写 三个config()
- configure(ClientDetailsServiceConfigurer clients)
配置客户端详情服务,可以将客户端信息存储在数据库中 - configure(AuthorizationServerEndpointsConfigurer endpoints)
配置令牌的访问端点和服务 - configure(AuthorizationServerSecurityConfigurer oauthServer)
配置令牌访问端点的安全约束
@Configuration
@EnableAuthorizationServer // 开启认证服务器功能
public class OauthServerConfigurer extends AuthorizationServerConfigurerAdapter {
@Autowired(required = false)
private AuthenticationManager authenticationManager;
/**
* ==> 接口的访问权限
* 认证服务器最终是以api接口的方式对外提供服务(校验合法性并生成令牌、校验令牌等)
* 那么,以api接口方式对外的话,就涉及到 接口的访问权限,我们需要在这里进行必要的配置
* @param security
* @throws Exception
*/
@Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
super.configure(security);
// 相当于打开endpoints 访问接口的开关,这样的话后期我们能够访问该接口
security
// 允许客户端表单认证
.allowFormAuthenticationForClients()
// 开启端口/oauth/token_key的访问权限(允许)
.tokenKeyAccess("permitAll()")
// 开启端口/oauth/check_token的访问权限(允许)
.checkTokenAccess("permitAll()");
}
/**
* ==> 客户端详情配置,
* 当前这个服务就如同 QQ平台,拉勾网作为客户端需要qq平台进行登录授权认证等,需要提前在QQ平台注册,
* 拿到QQ平台颁发的 client_id,secret等必要参数,表明客户端是谁
* @param clients
* @throws Exception
*/
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
super.configure(clients);
// 从内存中加载客户端详情
clients.inMemory()// 客户端信息存储在什么地方,可以在内存中,可以在数据库里
.withClient("client_lagou") // 添加一个client配置,指定其client_id
.secret("abcxyz") // 指定客户端的密码/安全码
.resourceIds("autodeliver") // 指定客户端所能访问资源id清单,此处的资源id需要在具体的资源服务器上也配置一样
// 认证类型/令牌颁发模式,可以配置多个在这里,但是不一定都用,具体使用哪种方式颁发token,需要客户端调用的时候传递参数指定
.authorizedGrantTypes("password","refresh_token")
// 客户端的权限范围,此处配置为all全部即可
.scopes("all");
}
/**
* ==> 配置 token令牌 管理功能
* (token此时就是一个字符串,当下的token需要在服务器端存储,那么存储在哪里呢?都是在这里配置)
* @param endpoints
* @throws Exception
*/
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
super.configure(endpoints);
endpoints
.tokenStore(tokenStore()) // 指定token的存储方法
.tokenServices(authorizationServerTokenServices()) // token服务的一个描述,可以认为是token生成细节的描述,比如有效时间多少等
.authenticationManager(authenticationManager) // 指定认证管理器,随后注入一个到当前类使用即可
.allowedTokenEndpointRequestMethods(HttpMethod.GET,HttpMethod.POST);
}
/**
* 该⽅法用于创建tokenStore对象(令牌存储对象)
* @return
*/
public TokenStore tokenStore(){
return new InMemoryTokenStore();
}
/**
* 该方法用户获取一个token服务对象(该对象描述了token有效期等信息)
* @return
*/
public AuthorizationServerTokenServices authorizationServerTokenServices() {
// 使用默认实现
DefaultTokenServices defaultTokenServices = new DefaultTokenServices();
defaultTokenServices.setSupportRefreshToken(true); // 是否开启令牌刷新
defaultTokenServices.setTokenStore(tokenStore());
// 设置令牌有效时间(一般设置为2个小时)
defaultTokenServices.setAccessTokenValiditySeconds(7200); // access_token 就是我们请求资源需要携带的令牌
// 设置刷新令牌的有效时间
defaultTokenServices.setRefreshTokenValiditySeconds(259200); // 3天
return defaultTokenServices;
}
}
- 认证服务器安全配置类
@Configuration
public class SecurityConfigurer extends WebSecurityConfigurerAdapter {
@Autowired
private PasswordEncoder passwordEncoder;
/**
* 注册一个认证管理器对象到容器
* @return
* @throws Exception
*/
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
/**
* 密码编码对象(密码不进行加密处理)
* @return
*/
@Bean
public PasswordEncoder passwordEncoder() {
return NoOpPasswordEncoder.getInstance();
}
/**
* 处理用户名和密码验证事宜
* 1)客户端传递username和password参数到认证服务器
* 2)一般来说,username和password会存储在数据库中的用户表中
* 3)根据用户表中数据,验证当前传递过来的用户信息的合法性
*/
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
// 在这个方法中就可以去关联数据库了,当前我们先把用户信息配置在内存中
// 实例化一个用户对象(相当于数据表中的一条用户记录)
UserDetails user = new User("admin","123456", new ArrayList<>());
auth.inMemoryAuthentication()
.withUser(user).passwordEncoder(passwordEncoder);
}
}
- 测试
访问 http://localhost:9999/oauth/token 效果:
路径分解:
- endpoint: /oauth/token
- 获取token用参数:
- client_id:客户端id
- client_secret:客户单密码
- grant_type:指定使用哪种颁发类型
- username:用户名
- password:密码
校验token: http://localhost:9999/oauth/check_token?token=a9979518-838c-49ff-b14a-ebdb7fde7d08
- 将之前的 任意微服务 改造成 资源微服务
引入aouth2依赖
<!--导入spring cloud oauth2依赖-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-oauth2</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.security.oauth.boot</groupId>
<artifactId>spring-security-oauth2-autoconfigure</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.security.oauth.boot</groupId>
<artifactId>spring-security-oauth2-autoconfigure</artifactId>
<version>2.1.11.RELEASE</version>
</dependency>
<!--引入security对oauth2的支持-->
<dependency>
<groupId>org.springframework.security.oauth</groupId>
<artifactId>spring-security-oauth2</artifactId>
<version>2.3.4.RELEASE</version>
</dependency>
增加一个 资源服务配置类
@Configuration
@EnableResourceServer // 开启资源服务器功能
@EnableWebSecurity // 开启web访问安全
public class ResourceServerConfigurer extends ResourceServerConfigurerAdapter {
/**
* 用于定义资源服务器向远程认证服务器发起请求,进行token校验等事宜
* @param resources
* @throws Exception
*/
@Override
public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
// 设置资源服务id
resources.resourceId("autodeliver");
// 定义token服务对象
RemoteTokenServices remoteTokenServices = new RemoteTokenServices();
// 校验端点/接口设置
remoteTokenServices.setCheckTokenEndpointUrl("http://localhost:9999/oauth/token");
// 携带客户端id、客户端安全码
remoteTokenServices.setClientSecret("abcxyz");
remoteTokenServices.setClientId("client_lagou");
// 确认添加资源服务
resources.tokenServices(remoteTokenServices);
}
/**
* 区分对待 需要认证 & 不需要认证 的API接口
* @param http
* @throws Exception
*/
@Override
public void configure(HttpSecurity http) throws Exception {
http // 设置session的创建策略
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)
.and()
.authorizeRequests()
.antMatchers("/autodeliver/**").authenticated() // "/autodeliver"前缀认证
.antMatchers("/demo/**").authenticated() // "/demo"前缀认证
.anyRequest().permitAll(); // 其他请求不认证
}
}
「JWT改造统⼀认证授权中心的令牌存储机制」
为了分摊认证服务器的压力,于是有人提出:
令牌可以是一个 (包含了用户信息、过期信息的) 非简单字符串,每个微服务根据这些信息直接在自己 本机上进行认证。
JWT定义
JSON Web Token —— 一个开放的行业标准(RFC 7519) ,定义了一种 简洁的、自包含的协议格式,用于 在通信双方传递json对象,传递的信息经由数字签名被验证和信任;
JWT可以使用 HMAC算法 或者使用 RSA的公钥/私钥 来签名,防止被篡改.
JWT令牌结构
三个部分,用 . 号分隔 ,如:xxxxxxx.yyyyyy.zzzzzzz
-
Header 包含令牌的类型、使用的哈希算法
如{"arg":"HS256", "typ":"JWT"}{ "arg": "HS256", "typ": "JWT" }上面的内容使用Base64编码后,得到一个字符串就是JWT令牌的第一部分
-
Payload 负载信息,存放JWT提供的现成字段:iss(签发者)/exp(过期时间戳)/sub(面向的用户)等
这部分不建议存放敏感信息,因为可以被解码还原;
如{ "sub": "1234567890", "name": "Archie Wan", "iat": 1516239022 }同样使用Base64编码后,得到JWT令牌的第二部分
-
Signature 签名,用于防止JWT内容被篡改
这个部分使用Base64url将前两部分进行编码,然后通过 . 连接成字符串,最后由 Header 中声明的 签名算法生成
认证服务器JWT令牌改造
- 令牌存储的改造
/**
* 该⽅法用于创建tokenStore对象(令牌存储对象)
* @return
*/
public TokenStore tokenStore(){
// return new InMemoryTokenStore();
// ↓ 改造为jwt令牌
return new JwtTokenStore(jwtAccessTokenConverter());
}
private String sign_key = "lagou123"; // jwt签名密钥
/**
* 返回jwt令牌转换器(帮助我们生成jwt令牌的)
* 在这里,我们可以把签名密钥传递进去给转换器对象
* @return
*/
public JwtAccessTokenConverter jwtAccessTokenConverter() {
JwtAccessTokenConverter jwtAccessTokenConverter = new JwtAccessTokenConverter();
jwtAccessTokenConverter.setSigningKey(sign_key); // 签名密钥
jwtAccessTokenConverter.setVerifier(new MacSigner(sign_key)); // 验证时使用的密钥,和签名密钥保持一致(MacSigner用于对称加密)
return jwtAccessTokenConverter;
}
- token中添加令牌服务
defaultTokenServices.setTokenEnhancer(jwtAccessTokenConverter());
/**
* 该方法用户获取一个token服务对象(该对象描述了token有效期等信息)
* @return
*/
public AuthorizationServerTokenServices authorizationServerTokenServices() {
// 使用默认实现
DefaultTokenServices defaultTokenServices = new DefaultTokenServices();
defaultTokenServices.setSupportRefreshToken(true); // 是否开启令牌刷新
defaultTokenServices.setTokenStore(tokenStore());
// ↓ 添加令牌服务(增强)
defaultTokenServices.setTokenEnhancer(jwtAccessTokenConverter());
// 设置令牌有效时间(一般设置为2个小时)
defaultTokenServices.setAccessTokenValiditySeconds(7200); // access_token 就是我们请求资源需要携带的令牌
// 设置刷新令牌的有效时间
defaultTokenServices.setRefreshTokenValiditySeconds(259200); // 3天
return defaultTokenServices;
}
- 查看token
我们去 JWT官网 看看 编码/解码的情况
- 改造资源服务器的请求代码
- 测试对资源服务器的访问
http://localhost:XXXX/autodeliver/checkState/1545346?access_token=xxxx.yyyyy.zzzz
「从数据库加载Oauth2客户端信息」
我们使用 JdbcClientDetailsService 来操作数据库管理token,但是从这个类的源码可知,所有语句都是对 oauth_client_details 这张表的操作,所以表明不是由着我们随意的。
建表语句:
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
-- ----------------------------
-- Table structure for oauth_client_details
-- ----------------------------
DROP TABLE IF EXISTS `oauth_client_details`;
CREATE TABLE `oauth_client_details` (
`client_id` varchar(48) NOT NULL,
`resource_ids` varchar(256) DEFAULT NULL,
`client_secret` varchar(256) DEFAULT NULL,
`scope` varchar(256) DEFAULT NULL,
`authorized_grant_types` varchar(256) DEFAULT NULL,
`web_server_redirect_uri` varchar(256) DEFAULT NULL,
`authorities` varchar(256) DEFAULT NULL,
`access_token_validity` int(11) DEFAULT NULL,
`refresh_token_validity` int(11) DEFAULT NULL,
`additional_information` varchar(4096) DEFAULT NULL,
`autoapprove` varchar(256) DEFAULT NULL,
PRIMARY KEY (`client_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
-- ----------------------------
-- Records of oauth_client_details
-- ----------------------------
BEGIN;
INSERT INTO `oauth_client_details` VALUES ('client_lagou123',
'autodeliver,resume', 'abcxyz', 'all', 'password,refresh_token',
NULL, NULL, 7200, 259200, NULL, NULL);
COMMIT;
SET FOREIGN_KEY_CHECKS = 1;
客户端详情配置改造:
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
super.configure(clients);
/*// 1. 从内存中加载客户端详情
clients.inMemory()// 客户端信息存储在什么地方,可以在内存中,可以在数据库里
.withClient("client_lagou") // 添加一个client配置,指定其client_id
.secret("abcxyz")
// 指定客户端的密码/安全码
.resourceIds("autodeliver") // 指定客户端所能访问资源id清单,此处的资源id需要在具体的资源服务器上也配置一样
// 认证类型/令牌颁发模式,可以配置多个在这里,但是不一定都用,具体使用哪种方式颁发token,需要客户端调用的时候传递参数指定
.authorizedGrantTypes("password","refresh_token")
// 客户端的权限范围,此处配置为all全部即可
.scopes("all");*/
// 2. 从数据库中加载客户端详情
clients.withClientDetails(createJdbcClientDetailsService());
}
@Autowired
private DataSource dataSource;
@Bean
public JdbcClientDetailsService createJdbcClientDetailsService() {
return new JdbcClientDetailsService(dataSource);
}
「从数据库验证用户合法性」
- 创建一个 users 表(表明不唯一)
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
-- ----------------------------
-- Table structure for users
-- ----------------------------
DROP TABLE IF EXISTS `users`;
CREATE TABLE `users` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`username` char(10) DEFAULT NULL,
`password` char(100) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8;
-- ----------------------------
-- Records of users
-- ----------------------------
BEGIN;
INSERT INTO `users` VALUES (4, 'lagou-user', 'iuxyzds');
COMMIT;
SET FOREIGN_KEY_CHECKS = 1;
- 创建 POJO + DAO
@Entity
@Table(name = "users")
public class Users {
@Id
private Long id;
private String username;
private String password;
// set+get 略
}
public interface UsersRepository extends JpaRepository<Users, Long> {
Users findByUsername(String username);
}
使用jpa的话 启动类记得加上
@EntityScan("XXX.XXX.XXX")注解
- 建立一个实现
org.springframework.security.core.userdetails.UserDetailsService的自定义实现类
@Service
public class JdbcUserDetailsService implements UserDetailsService {
@Autowired
private UsersRepository usersRepository;
/**
* 根据用户名加载出用户的相关信息,封装成 UserDetails类型的对象
* 密码字段,框架会自动匹配
* @param username 用户名
* @return
* @throws UsernameNotFoundException
*/
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
Users users = usersRepository.findByUsername(username);
return new User(users.getUsername(), users.getPassword(),new ArrayList<>());
}
}
- SecurityConfigurer 中使用上述 Service 进行改造
@Autowired
private JdbcUserDetailsService jdbcUserDetailsService;
/**
* 处理用户名和密码验证事宜
* 1)客户端传递username和password参数到认证服务器
* 2)一般来说,username和password会存储在数据库中的用户表中
* 3)根据用户表中数据,验证当前传递过来的用户信息的合法性
*/
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
// 在这个方法中就可以去关联数据库了,当前我们先把用户信息配置在内存中
// 实例化一个用户对象(相当于数据表中的一条用户记录)
/*UserDetails user = new User("admin","123456", new ArrayList<>());
auth.inMemoryAuthentication()
.withUser(user).passwordEncoder(passwordEncoder);*/
// ↓ 改造为从数据库匹配用户信息
auth.userDetailsService(jdbcUserDetailsService).passwordEncoder(passwordEncoder);
}