文章目录
第六部分 微服务监控 Sleuth + Zipkin
第 1 节 微服务监控之 Turbine 聚合监控
参考上文 Spring Cloud 笔记中 hystrix 和 Springcloud dashboard 和 turbine
第 2 节 微服务监控之分布式链路追踪技术 Sleuth + Zipkin
2.1 分布式链路追踪技术适⽤场景
场景描述
为了⽀撑⽇益增⻓的庞⼤业务量,我们会使⽤微服务架构设计我们的系统,使得 我们的系统不仅能够通过集群部署抵挡流量的冲击,⼜能根据业务进⾏灵活的扩 展。
那么,在微服务架构下,⼀次请求少则经过三四次服务调⽤完成,多则跨越⼏⼗ 个甚⾄是上百个服务节点。那么问题接踵⽽来:
1)如何动态展示服务的调⽤链路?(⽐如 A 服务调⽤了哪些其他的服务—依赖关系)
2)如何分析服务调⽤链路中的瓶颈节点并对其进⾏调优?(⽐如 A—>B—>C,C 服务处理时间特别⻓)
3)如何快速进⾏服务链路的故障发现?
这就是分布式链路追踪技术存在的⽬的和意义
分布式链路追踪技术
如果我们在⼀个请求的调⽤处理过程中,在各个链路节点都能够记录下⽇志,并 最终将⽇志进⾏集中可视化展示,那么我们想监控调⽤链路中的⼀些指标就有希 望了~~~⽐如,请求到达哪个服务实例?请求被处理的状态怎样?处理耗时怎 样?这些都能够分析出来了…
分布式环境下基于这种想法实现的监控技术就是就是分布式链路追踪(全链路追 踪)。
市场上的分布式链路追踪⽅案
分布式链路追踪技术已然成熟,产品也不少,国内外都有,⽐如
- Spring Cloud Sleuth + Twitter Zipkin
- 阿⾥巴巴的“鹰眼”
- ⼤众点评的“CAT”
- 美团的“Mtrace”
- 京东的“Hydra”
- 新浪的“Watchman”
- 最近的 Apache Skywalking。
2.2 分布式链路追踪技术核⼼思想
本质:记录⽇志,作为⼀个完整的技术,分布式链路追踪也有⾃⼰的理论和概念
微服务架构中,针对请求处理的调⽤链可以展现为⼀棵树,示意如下
上图描述了⼀个常⻅的调⽤场景,⼀个请求通过⽹关服务路由到下游的微服务-1, 然后微服务-1 调⽤微服务-2,拿到结果后再调⽤微服务-3,最后组合微服务-2 和微服 务-3 的结果,通过⽹关返回给⽤户
为了追踪整个调⽤链路,肯定需要记录⽇志,⽇志记录是基础,在此之上肯定有⼀ 些理论概念,当下主流的的分布式链路追踪技术/系统所基于的理念都来⾃于 Google 的⼀篇论⽂《Dapper, a Large-Scale Distributed Systems Tracing Infrastructure》,这⾥⾯涉及到的核⼼理念是什么,我们来看下,还以前⾯的服务 调⽤来说
上图标识⼀个请求链路,⼀条链路通过 TraceId 唯⼀标识,span 标识发起的请求信 息,各 span 通过 parentId 关联起来
Trace:
服务追踪的追踪单元是从客户发起请求(request)抵达被追踪系统的边界开始,到被追踪系统向客户返回响应(response)为⽌的过程
Trace ID:
为了实现请求跟踪,当请求发送到分布式系统的⼊⼝端点时,只需要服 务跟踪框架为该请求创建⼀个唯⼀的跟踪标识 Trace ID,同时在分布式系统内部流转 的时候,框架失踪保持该唯⼀标识,直到返回给请求⽅
⼀个 Trace 由⼀个或者多个 Span 组成,每⼀个 Span 都有⼀个 SpanId,Span 中会记录 TraceId,同时还有⼀个叫做 ParentId,指向了另外⼀个 Span 的 SpanId,表明⽗⼦ 关系,其实本质表达了依赖关系
Span ID:
为了统计各处理单元的时间延迟,当请求到达各个服务组件时,也是通过 ⼀个唯⼀标识 Span ID 来标记它的开始,具体过程以及结束。对每⼀个 Span 来说, 它必须有开始和结束两个节点,通过记录开始 Span 和结束 Span 的时间戳,就能统计 出该 Span 的时间延迟,除了时间戳记录之外,它还可以包含⼀些其他元数据,⽐如 时间名称、请求信息等。
每⼀个 Span 都会有⼀个唯⼀跟踪标识 Span ID,若⼲个有序的 span 就组成了⼀个 trace。
Span 可以认为是⼀个⽇志数据结构,在⼀些特殊的时机点会记录了⼀些⽇志信息, ⽐如有时间戳、spanId、TraceId,parentIde 等,Span 中也抽象出了另外⼀个概 念,叫做事件,核⼼事件如下
CS : client send/start 客户端/消费者发出⼀个请求,描述的是⼀个 span 开始
SR: server received/start 服务端/⽣产者接收请求
SR-CS 属于请求发送的⽹络延 迟
SS: server send/finish 服务端/⽣产者发送应答
SS-SR 属于服务端消耗时间
CR:client received/finished 客户端/消费者接收应答
CR-SS 表示回复需要的时 间(响应的⽹络延迟)
Spring Cloud Sleuth (追踪服务框架)可以追踪服务之间的调⽤,Sleuth 可以记录 ⼀个服务请求经过哪些服务、服务处理时⻓等,根据这些,我们能够理清各微服务 间的调⽤关系及进⾏问题追踪分析。
耗时分析:通过 Sleuth 了解采样请求的耗时,分析服务性能问题(哪些服务调 ⽤⽐较耗时)
链路优化:发现频繁调⽤的服务,针对性优化等
Sleuth 就是通过记录⽇志的⽅式来记录踪迹数据的
注意:我们往往把 Spring Cloud Sleuth 和 Zipkin ⼀起使⽤,把 Sleuth 的数据信 息发送给 Zipkin 进⾏聚合,利⽤ Zipkin 存储并展示数据。
2.3 Sleuth + Zipkin
1)每⼀个需要被追踪踪迹的微服务⼯程都引⼊依赖坐标
<!--链路追踪-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-sleuth</artifactId>
</dependency>
2)每⼀个微服务都修改 application.yml 配置⽂件,添加⽇志级别
#分布式链路追踪
logging:
level:
org.springframework.web.servlet.DispatcherServlet: debug
org.springframework.cloud.sleuth: debug
请求到来时,我们在控制台可以观察到 Sleuth 输出的⽇志(全局 TraceId、SpanId 等)。
这样的⽇志⾸先不容易阅读观察,另外⽇志分散在各个微服务服务器上,接下来我 们使⽤ zipkin 统⼀聚合轨迹⽇志并进⾏存储展示。
3)结合 Zipkin 展示追踪数据
Zipkin 包括 Zipkin Server 和 Zipkin Client 两部分,Zipkin Server 是⼀个单独的服 务,Zipkin Client 就是具体的微服务
Zipkin Server 构建
pom.xml
<!--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>
⼊⼝启动类
package com.galaxy;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import zipkin2.server.internal.EnableZipkinServer;
/**
* @author lane
* @date 2021年06月29日 下午3:13
*/
@SpringBootApplication
@EnableZipkinServer
public class ZipkinServerApplication9411 {
public static void main(String[] args) {
SpringApplication.run(ZipkinServerApplication9411.class,args);
}
}
配置文件
server:
port: 9411
management:
metrics:
web:
server:
#关闭自动检测,不然会报错
auto-time-requests: false
Zipkin Client 构建(在具体微服务中修改)
在 resume8081 和 autodeliver8096 及 9002 的 gateway 分别添加如下
pom 中添加 zipkin 依赖
<!--zipkin客户端-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-zipkin</artifactId>
</dependency>
配置文件
spring:
#zipkin配置
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
另外,对于 log ⽇志,依然保持开启 debug 状态
Zipkin server ⻚⾯⽅便我们查看服务调⽤依赖关系及⼀些性能指标和异常信息 追踪数据
Zipkin 持久化到 mysql
因为数据存在内存当中,一旦重启数据会丢失掉,这就需要持久化到本地 mysql 或者 els 中
mysql 中创建名称为 zipkin 的数据库,并执⾏如下 sql 语句(官⽅提供)
sql 地址 zipkin-storage/mysql-v1/src/main/resources/mysql.sql
CREATE TABLE IF NOT EXISTS zipkin_spans (
`trace_id_high` BIGINT NOT NULL DEFAULT 0 COMMENT 'If non zero, this means the trace uses 128 bit traceIds instead of 64 bit',
`trace_id` BIGINT NOT NULL,
`id` BIGINT NOT NULL,
`name` VARCHAR(255) NOT NULL,
`remote_service_name` VARCHAR(255),
`parent_id` BIGINT,
`debug` BIT(1),
`start_ts` BIGINT COMMENT 'Span.timestamp(): epoch micros used for endTs query and to implement TTL',
`duration` BIGINT COMMENT 'Span.duration(): micros used for minDuration and maxDuration query',
PRIMARY KEY (`trace_id_high`, `trace_id`, `id`)
) ENGINE=InnoDB ROW_FORMAT=COMPRESSED CHARACTER SET=utf8 COLLATE utf8_general_ci;
ALTER TABLE zipkin_spans ADD INDEX(`trace_id_high`, `trace_id`) COMMENT 'for getTracesByIds';
ALTER TABLE zipkin_spans ADD INDEX(`name`) COMMENT 'for getTraces and getSpanNames';
ALTER TABLE zipkin_spans ADD INDEX(`remote_service_name`) COMMENT 'for getTraces and getRemoteServiceNames';
ALTER TABLE zipkin_spans ADD INDEX(`start_ts`) COMMENT 'for getTraces ordering and range';
CREATE TABLE IF NOT EXISTS zipkin_annotations (
`trace_id_high` BIGINT NOT NULL DEFAULT 0 COMMENT 'If non zero, this means the trace uses 128 bit traceIds instead of 64 bit',
`trace_id` BIGINT NOT NULL COMMENT 'coincides with zipkin_spans.trace_id',
`span_id` BIGINT NOT NULL COMMENT 'coincides with zipkin_spans.id',
`a_key` VARCHAR(255) NOT NULL COMMENT 'BinaryAnnotation.key or Annotation.value if type == -1',
`a_value` BLOB COMMENT 'BinaryAnnotation.value(), which must be smaller than 64KB',
`a_type` INT NOT NULL COMMENT 'BinaryAnnotation.type() or -1 if Annotation',
`a_timestamp` BIGINT COMMENT 'Used to implement TTL; Annotation.timestamp or zipkin_spans.timestamp',
`endpoint_ipv4` INT COMMENT 'Null when Binary/Annotation.endpoint is null',
`endpoint_ipv6` BINARY(16) COMMENT 'Null when Binary/Annotation.endpoint is null, or no IPv6 address',
`endpoint_port` SMALLINT COMMENT 'Null when Binary/Annotation.endpoint is null',
`endpoint_service_name` VARCHAR(255) COMMENT 'Null when Binary/Annotation.endpoint is null'
) ENGINE=InnoDB ROW_FORMAT=COMPRESSED CHARACTER SET=utf8 COLLATE utf8_general_ci;
ALTER TABLE zipkin_annotations ADD UNIQUE KEY(`trace_id_high`, `trace_id`, `span_id`, `a_key`, `a_timestamp`) COMMENT 'Ignore insert on duplicate';
ALTER TABLE zipkin_annotations ADD INDEX(`trace_id_high`, `trace_id`, `span_id`) COMMENT 'for joining with zipkin_spans';
ALTER TABLE zipkin_annotations ADD INDEX(`trace_id_high`, `trace_id`) COMMENT 'for getTraces/ByIds';
ALTER TABLE zipkin_annotations ADD INDEX(`endpoint_service_name`) COMMENT 'for getTraces and getServiceNames';
ALTER TABLE zipkin_annotations ADD INDEX(`a_type`) COMMENT 'for getTraces and autocomplete values';
ALTER TABLE zipkin_annotations ADD INDEX(`a_key`) COMMENT 'for getTraces and autocomplete values';
ALTER TABLE zipkin_annotations ADD INDEX(`trace_id`, `span_id`, `a_key`) COMMENT 'for dependencies job';
CREATE TABLE IF NOT EXISTS zipkin_dependencies (
`day` DATE NOT NULL,
`parent` VARCHAR(255) NOT NULL,
`child` VARCHAR(255) NOT NULL,
`call_count` BIGINT,
`error_count` BIGINT,
PRIMARY KEY (`day`, `parent`, `child`)
) ENGINE=InnoDB ROW_FORMAT=COMPRESSED CHARACTER SET=utf8 COLLATE utf8_general_ci;
添加 mysql 相关的依赖
<!--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>
修改配置⽂件,添加如下内容
server:
port: 9411
management:
metrics:
web:
server:
#关闭自动检测,不然会报错
auto-time-requests: false
spring:
datasource:
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://localhost:3306/zipkin?useUnicode=true&characterEncoding=utf-8&useSSL=false&allowMultiQueries=true
username: root
password: root
druid:
initialSize: 10
minIdle: 10
maxActive: 30
maxWait: 50000
# 指定zipkin持久化介质为mysql
zipkin:
storage:
type: mysql
启动类中注⼊事务管理器
package com.galaxy;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.PlatformTransactionManager;
import zipkin2.server.internal.EnableZipkinServer;
import javax.sql.DataSource;
/**
* @author lane
* @date 2021年06月29日 下午3:13
*/
@SpringBootApplication
@EnableZipkinServer
public class ZipkinServerApplication9411 {
public static void main(String[] args) {
SpringApplication.run(ZipkinServerApplication9411.class, args);
}
@Bean
public PlatformTransactionManager transactionManager(DataSource dataSource){
return new DataSourceTransactionManager(dataSource);
}
}
数据存储到了数据库当中了,再次重启的话数据不会丢失了
第七部分 微服务统⼀认证⽅案 Spring Cloud OAuth2 + JWT
认证:验证⽤户的合法身份,⽐如输⼊⽤户名和密码,系统会在后台验证⽤户名 和密码是否合法,合法的前提下,才能够进⾏后续的操作,访问受保护的资源
第一节 微服务架构下统⼀认证场景
分布式系统的每个服务都会有认证需求,如果每个服务都实现⼀套认证逻辑会⾮ 常冗余,考虑分布式系统共享性的特点,需要由独⽴的认证服务处理系统认证的 请求。
1.1 微服务架构下统⼀认证思路
基于 Session 的认证⽅式
在分布式的环境下,基于 session 的认证会出现⼀个问题,每个应⽤服务都需要 在 session 中存储⽤户身份信息,通过负载均衡将本地的请求分配到另⼀个应⽤ 服务需要将 session 信息带过去,否则会重新认证。我们可以使⽤ Session 共享、 Session 黏贴等⽅案。
Session ⽅案也有缺点,⽐如基于 cookie,移动端不能有效使⽤等
基于 token 的认证⽅式
基于 token 的认证⽅式,服务端不⽤存储认证数据,易维护扩展性强, 客户端可 以把 token 存在任意地⽅,并且可以实现 web 和 app 统⼀认证机制。其缺点也很 明显,token 由于⾃包含信息,因此⼀般数据量较⼤,⽽且每次请求 都需要传 递,因此⽐较占带宽。另外,token 的签名验签操作也会给 cpu 带来额外的处理 负担。
1.2 OAuth2 开放授权协议/标准
1.2.1 OAuth2 介绍
OAuth(开放授权)是⼀个开放协议/标准,允许⽤户授权第三⽅应⽤访问他们存储 在另外的服务提供者上的信息,⽽不需要将⽤户名和密码提供给第三⽅应⽤或分享 他们数据的所有内容。
允许⽤户授权第三⽅应⽤访问他们存储在另外的服务提供者上的信息,⽽不需要将 ⽤户名和密码提供给第三⽅应⽤或分享他们数据的所有内容
结合“使⽤ QQ 登录拉勾”这个场景拆分理解上述那句话
⽤户:我们⾃⼰
第三⽅应⽤:拉勾⽹
另外的服务提供者:QQ
OAuth2 是 OAuth 协议的延续版本,但不向后兼容 OAuth1 即完全废⽌了 OAuth1。
1.2.2 OAuth2 协议⻆⾊和流程
拉勾⽹要开发使⽤ QQ 登录这个功能的话,那么拉勾⽹是需要提前到 QQ 平台进⾏登 记的(否则 QQ 凭什么陪着拉勾⽹玩授权登录这件事)
1)拉勾⽹——登记——>QQ 平台
2)QQ 平台会颁发⼀些参数给拉勾⽹,后续上线进⾏授权登录的时候(刚才打开授 权⻚⾯)需要携带这些参数
client_id :客户端 id(QQ 最终相当于⼀个认证授权服务器,拉勾⽹就相当于⼀个客 户端了,所以会给⼀个客户端 id),相当于账号
secret:相当于密码
资源所有者(Resource Owner):可以理解为⽤户⾃⼰
客户端(Client):我们想登陆的⽹站或应⽤,⽐如拉勾⽹
认证服务器(Authorization Server):可以理解为微信或者 QQ
资源服务器(Resource Server):可以理解为微信或者 QQ
1.2.3 什么情况下需要使⽤ OAuth2?
第三⽅授权登录的场景:
⽐如,我们经常登录⼀些⽹站或者应⽤的时候,可以选择 使⽤第三⽅授权登录的⽅式,⽐如:微信授权登录、QQ 授权登录、微博授权登录 等,这是典型的 OAuth2 使⽤场景。
单点登录的场景:
如果项⽬中有很多微服务或者公司内部有很多服务,可以专⻔做 ⼀个认证中⼼(充当认证平台⻆⾊),所有的服务都要到这个认证中⼼做认证,只 做⼀次登录,就可以在多个授权范围内的服务中⾃由串⾏。
1.2.4 OAuth2 的颁发 Token 授权⽅式
1)授权码(authorization-code)
2)密码式(password)提供⽤户名 + 密码换取 token 令牌
3)隐藏式(implicit)
4)客户端凭证(client credentials)
授权码模式使⽤到了回调地址,是最复杂的授权⽅式,微博、微信、QQ 等第三⽅登 录就是这种模式。我们重点讲解接⼝对接中常使⽤的 password 密码模式(提供⽤户 名 + 密码换取 token)。
第二节 Spring Cloud OAuth2 实现
2.1 Spring Cloud OAuth2 介绍
Spring Cloud OAuth2 是 Spring Cloud 体系对 OAuth2 协议的实现,可以⽤来做多个微服务的统⼀认证(验证身份合法性)授权(验证权限)。通过向 OAuth2 服务 (统⼀认证授权服务)发送某个类型的 grant_type 进⾏集中认证和授权,从⽽获得 access_token(访问令牌),⽽这个 token 是受其他微服务信任的。
注意:使⽤ OAuth2 解决问题的本质是,引⼊了⼀个认证授权层,认证授权层连接了 资源的拥有者,在授权层⾥⾯,资源的拥有者可以给第三⽅应⽤授权去访问我们的 某些受保护资源。
2.2 Spring Cloud OAuth2 构建微服务统⼀认证服务思路
注意:在我们统⼀认证的场景中,Resource Server 其实就是我们的各种受保护的 微服务,微服务中的各种 API 访问接⼝就是资源,发起 http 请求的浏览器就是 Client 客户端(对应为第三⽅应⽤)
2.3. 搭建认证服务器(Authorization Server)
认证服务器(Authorization Server),负责颁发 token
新建项⽬ lane-cloud-oauth-server-9999
pom.xml
<?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">
<parent>
<artifactId>lane-parent-project</artifactId>
<groupId>com.galaxy</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>lane-cloud-oauth2-server-9999</artifactId>
<dependencies>
<!--导入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>
</dependencies>
</project>
启动类
package com.galaxy;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
/**
* @author lane
* @date 2021年06月29日 下午7:18
*/
@SpringBootApplication
@EnableDiscoveryClient
public class OauthServerApplication9999 {
public static void main(String[] args) {
SpringApplication.run(OauthServerApplication9999.class,args);
}
}
oauth2 认证服务器配置类
package com.galaxy.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerSecurityConfigurer;
import org.springframework.security.oauth2.provider.token.AuthorizationServerTokenServices;
import org.springframework.security.oauth2.provider.token.DefaultTokenServices;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.oauth2.provider.token.store.InMemoryTokenStore;
/**
* 当前类为Oauth2 server的配置类(需要继承特定的父类 AuthorizationServerConfigurerAdapter)
* @author lane
* @date 2021年06月29日 下午7:33
*/
@Configuration
@EnableAuthorizationServer //开启认证服务器功能
public class OauthServerConfig extends AuthorizationServerConfigurerAdapter {
@Autowired
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()");
}
/**
* 客户端详情配置,
* 比如client_id,secret
* 当前这个服务就如同QQ平台,拉勾网作为客户端需要qq平台进行登录授权认证等,提前需要到QQ平台注册,QQ平台会给拉勾网
* 颁发client_id等必要参数,表明客户端是谁
* @param clients
* @throws Exception
*/
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
super.configure(clients);
// 从内存中加载客户端详情
clients.inMemory()// 客户端信息存储在什么地方,可以在内存中,可以在数据库里
.withClient("client_lane") // 添加一个client配置,指定其client_id
.secret("abcxyz") // 指定客户端的密码/安全码
.resourceIds("autodeliver") // 指定客户端所能访问资源id清单,此处的资源id是需要在具体的资源服务器上也配置一样
// 认证类型/令牌颁发模式,可以配置多个在这里,但是不一定都用,具体使用哪种方式颁发token,需要客户端调用的时候传递参数指定
.authorizedGrantTypes("password","refresh_token")
// 客户端的权限范围,此处配置为all全部即可
.scopes("all");
}
/**
* 认证服务器是玩转token的,那么这里配置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对象(令牌存储对象)
token以什么形式存储
*/
public TokenStore tokenStore(){
//存储内存当中
return new InMemoryTokenStore();
// 使用jwt令牌
}
/**
* 该方法用户获取一个token服务对象(该对象描述了token有效期等信息)
*/
public AuthorizationServerTokenServices authorizationServerTokenServices() {
// 使用默认实现
DefaultTokenServices defaultTokenServices = new DefaultTokenServices();
defaultTokenServices.setSupportRefreshToken(true); // 是否开启令牌刷新
defaultTokenServices.setTokenStore(tokenStore());
// 设置令牌有效时间(一般设置为2个小时)
defaultTokenServices.setAccessTokenValiditySeconds(20); // access_token就是我们请求资源需要携带的令牌
// 设置刷新令牌的有效时间
defaultTokenServices.setRefreshTokenValiditySeconds(259200); // 3天
return defaultTokenServices;
}
}
关于三个 configure ⽅法
configure(ClientDetailsServiceConfigurer clients)
⽤来配置客户端详情服务(ClientDetailsService),客户端详情信息在 这 ⾥进⾏初始化,你能够把客户端详情信息写死在这⾥或者是通过数据库来存 储调取详情信息
configure(AuthorizationServerEndpointsConfigurer endpoints)
⽤来配置令牌(token)的访问端点和令牌服务(token services)
configure(AuthorizationServerSecurityConfigurer oauthServer)
⽤来配置令牌端点的安全约束.
TokenStore 种类
InMemoryTokenStore
默认采⽤,它可以完美的⼯作在单服务器上(即访问并发量 压⼒不⼤ 的情况下,并且它在失败的时候不会进⾏备份),⼤多数的项⽬都可以 使⽤这个版本的实现来进⾏ 尝试,你可以在开发的时候使⽤它来进⾏ 管理,因为不会被保存到磁盘中,所以更易于调试。
JdbcTokenStore
这是⼀个基于 JDBC 的实现版本,令牌会被保存进关系型数据库。使⽤ 这个版本的实现时, 你可以在不同的服务器之间共享令牌信息,使⽤ 这个版本的时候请注意把"spring-jdbc"这个依赖加⼊到你的 classpath 当中。
JwtTokenStore 这个版本的全称是 JSON Web Token(JWT),它可以 把令牌相关的数据进⾏编码(因此对于后端服务来说,它不需要进⾏存 储,这将是⼀个重⼤优势),缺点就是这个令牌占⽤的空间会⽐较⼤, 如果你加⼊了⽐较多⽤户凭证信息,JwtTokenStore 不会保存任何数据。
认证服务器安全配置类
package com.galaxy.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.crypto.password.NoOpPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import java.util.ArrayList;
/**
* 该配置类,主要处理⽤户名和密码的校验等事宜
* @author lane
* @date 2021年06月30日 上午11:42
*/
@Configuration
public class SecurityConfigure extends WebSecurityConfigurerAdapter {
@Autowired
private PasswordEncoder passwordEncoder;
/**
* 注册一个认证管理器对象到容器
*/
@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);
}
}
测试认证服务器
获取 token:
http://localhost:9999/oauth/token?client_secret=abcxyz&grant_type=password&username=admin&password=123456&client_id=client_lane
endpoint:/oauth/token
获取 token 携带的参数
client_id:客户端 id
client_secret:客户端密码
grant_type:指定使⽤哪种颁发类型,password
username:⽤户名
password:密码
获取 token 信息如下
{
"access_token": "ea249651-d6c9-46bd-a150-dd0a9096b9bf",
"token_type": "bearer",
"refresh_token": "43c56393-d845-4f78-8faa-7bb7b3043f25",
"expires_in": 19,
"scope": "all"
}
其中 access_token 是获取到的 token,一般设置过期时间较短
refresh_token 是获取刷新 token,作用是当 token 过期之后可以携带 refresh_token 去获取新的 access_token,而不需要用户名和密码再次去获取新的 access_token,一般设置时间较长
expires_in 是 access_token 的过期时间
校验 token:
http://localhost:9999/oauth/check_token?token=bf163553-f232-4a91-8367-fb79e87cbfaa
校验 token 的合法性,获取一些客户端信息
{
"aud": [
"autodeliver"
],
"active": true,
"exp": 1625026392,
"user_name": "admin",
"client_id": "client_lane",
"scope": [
"all"
]
}
刷新 token:
既是根据 refresh_token 获取 access_token,而不需要用户名和密码
http://localhost:9999/oauth/token?grant_type=refresh_token&client_id=client_lane&client_secret=abcxyz&refresh_token=43c56393-d845-4f78-8faa-7bb7b3043f25
{
"access_token": "66895653-f86d-45b8-8d0a-4455a098f133",
"token_type": "bearer",
"refresh_token": "43c56393-d845-4f78-8faa-7bb7b3043f25",
"expires_in": 19,
"scope": "all"
}
2.4 资源服务器(访问被认证的微服务)
对于资源服务器我们采用的是 8096 微服务
添加依赖
<!--导入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>
Resource Server 配置类
package com.galaxy.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer;
import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configurers.ResourceServerSecurityConfigurer;
import org.springframework.security.oauth2.provider.token.RemoteTokenServices;
/**
* @author lane
* @date 2021年07月01日 上午10:57
*/
@Configuration
@EnableResourceServer // 开启资源服务器功能
@EnableWebSecurity // 开启web访问安全
public class ResourceServerConfiger extends ResourceServerConfigurerAdapter {
/**
* 该方法用于定义资源服务器向远程认证服务器发起请求,进行token校验等事宜
* @param resources
* @throws Exception
*/
@Override
public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
// 设置当前资源服务的资源id
resources.resourceId("autodeliver");
// 定义token服务对象(token校验就应该靠token服务对象)
RemoteTokenServices remoteTokenServices = new RemoteTokenServices();
// 校验端点/接口设置
remoteTokenServices.setCheckTokenEndpointUrl("http://localhost:9999/oauth/check_token");
// 携带客户端id和客户端安全码
remoteTokenServices.setClientId("client_lane");
remoteTokenServices.setClientSecret("abcxyz");
// 别忘了这一步
resources.tokenServices(remoteTokenServices);
}
/**
* 场景:一个服务中可能有很多资源(API接口)
* 某一些API接口,需要先认证,才能访问
* 某一些API接口,压根就不需要认证,本来就是对外开放的接口
* 我们就需要对不同特点的接口区分对待(在当前configure方法中完成),设置是否需要经过认证
*
* @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(); // 其他请求不认证
}
}
添加测试 controller
package com.galaxy.controller;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/demo")
public class DemoController {
@GetMapping("/test")
public String findResumeOpenState() {
Object details = SecurityContextHolder.getContext().getAuthentication().getDetails();
return "demo/test!";
}
}
------------------------------------------------------------------------------------------------
package com.galaxy.controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/others")
public class OthersController {
@GetMapping("/test")
public String findResumeOpenState() {
return "others/test!";
}
}
测试效果未携带 token
携带 token
访问 http://localhost:8096/autodeliver/checkState/1545132?access_token=4529f559-e662-42ca-80cf-9d85ee0c9e97
思考:当我们第⼀次登陆之后,认证服务器颁发 token 并将其存储在认证服 务器中,后期我们访问资源服务器时会携带 token,资源服务器会请求认证 服务器验证 token 有效性,如果资源服务器有很多,那么认证服务器压⼒会 很⼤…
另外,资源服务器向认证服务器 check_token,获取的也是⽤户信息 UserInfo,能否把⽤户信息存储到令牌中,让客户端⼀直持有这个令牌,令 牌的验证也在资源服务器进⾏,这样避免和认证服务器频繁的交互…
我们可以考虑使⽤ JWT 进⾏改造,使⽤ JWT 机制之后资源服务器不需要访问 认证服务器.…
第 三 节 Spring Cloud OAuth2 + JWT 实现
3.1 JWT 改造统⼀认证授权中⼼的令牌存储机制
JWT 令牌介绍 通过上边的测试我们发现,当资源服务和授权服务不在⼀起时资源服务使⽤ RemoteTokenServices 远程请求授权 服务验证 token,如果访问量较⼤将会影响系 统的性能。
解决上边问题: 令牌采⽤ JWT 格式即可解决上边的问题,⽤户认证通过会得到⼀个 JWT 令牌,JWT 令牌中已经包括了⽤户相关的信 息,客户端只需要携带 JWT 访问资源 服务,资源服务根据事先约定的算法⾃⾏完成令牌校验,⽆需每次都请求认证 服务 完成授权
jwt 官网信息可以看到 JWT 令牌只是 xxx.yyy.zzz 的一个字符串
xxx 是携带的一个加密类型相关的 json 字符串
yyy 是携带用户基本信息
zzz 是将 base64(xxx).base64(yyy)通过 secret 字符串进行进行 SHA256 加密后的一个签名信息字符串,资源服务器可以通过密钥 secret 进行解密之后查看信息是否被篡改。
1)什么是 JWT?
JSON Web Token(JWT)是⼀个开放的⾏业标准(RFC 7519),它定义了⼀种简介 的、⾃包含的协议格式,⽤于 在通信双⽅传递 json 对象,传递的信息经过数字签名 可以被验证和信任。JWT 可以使⽤ HMAC 算法或使⽤ RSA 的公 钥/私钥对来签名,防 ⽌被篡改。
2)JWT 令牌结构
JWT 令牌由三部分组成,每部分中间使⽤点(.)分隔,⽐如:xxxxx.yyyyy.zzzzz
Header 头部
包括令牌的类型(即 JWT)及使⽤的哈希算法(如 HMAC SHA256 或 RSA),例如
{
"alg": "HS256", "typ": "JWT"
}
将上边的内容使⽤ Base64Url 编码,得到⼀个字符串就是 JWT 令牌的第⼀部分。
Payload
第⼆部分是负载,内容也是⼀个 json 对象,它是存放有效信息的地⽅,它可以存 放 jwt 提供的现成字段,⽐ 如:iss(签发者),exp(过期时间戳), sub(⾯向的 ⽤户)等,也可⾃定义字段。 此部分不建议存放敏感信息,因为此部分可以解 码还原原始内容。 最后将第⼆部分负载使⽤ Base64Url 编码,得到⼀个字符串 就是 JWT 令牌的第⼆部分。 ⼀个例⼦:
{
"sub": "1234567890", "name": "John Doe", "iat": 1516239022
}
Signature
第三部分是签名,此部分⽤于防⽌ jwt 内容被篡改。 这个部分使⽤ base64url 将 前两部分进⾏编码,编码后使⽤点(.)连接组成字符串,最后使⽤ header 中声 明 签名算法进⾏签名。
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
) secret base64 encoded
base64UrlEncode(header):jwt 令牌的第⼀部分。
base64UrlEncode(payload):jwt 令牌的第⼆部分。
secret:签名所使⽤的密钥。
认证服务器端 JWT 改造(改造主配置类)
/**
* 当前类为Oauth2 server的配置类(需要继承特定的父类 AuthorizationServerConfigurerAdapter)
* @author lane
* @date 2021年06月29日 下午7:33
*/
@Configuration
@EnableAuthorizationServer //开启认证服务器功能
public class OauthServerConfig extends AuthorizationServerConfigurerAdapter {
@Autowired
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()");
}
/**
* 客户端详情配置,
* 比如client_id,secret
* 当前这个服务就如同QQ平台,拉勾网作为客户端需要qq平台进行登录授权认证等,提前需要到QQ平台注册,QQ平台会给拉勾网
* 颁发client_id等必要参数,表明客户端是谁
* @param clients
* @throws Exception
*/
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
super.configure(clients);
// 从内存中加载客户端详情
clients.inMemory()// 客户端信息存储在什么地方,可以在内存中,可以在数据库里
.withClient("client_lane") // 添加一个client配置,指定其client_id
.secret("abcxyz") // 指定客户端的密码/安全码
.resourceIds("autodeliver") // 指定客户端所能访问资源id清单,此处的资源id是需要在具体的资源服务器上也配置一样
// 认证类型/令牌颁发模式,可以配置多个在这里,但是不一定都用,具体使用哪种方式颁发token,需要客户端调用的时候传递参数指定
.authorizedGrantTypes("password","refresh_token")
// 客户端的权限范围,此处配置为all全部即可
.scopes("all");
}
/**
* 认证服务器是玩转token的,那么这里配置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);
}
/**
* 该方法用户获取一个token服务对象(该对象描述了token有效期等信息)
*/
public AuthorizationServerTokenServices authorizationServerTokenServices() {
// 使用默认实现
DefaultTokenServices defaultTokenServices = new DefaultTokenServices();
defaultTokenServices.setSupportRefreshToken(true); // 是否开启令牌刷新
defaultTokenServices.setTokenStore(tokenStore());
// 针对jwt令牌的添加
defaultTokenServices.setTokenEnhancer(jwtAccessTokenConverter());
// 设置令牌有效时间(一般设置为2个小时)
defaultTokenServices.setAccessTokenValiditySeconds(60); // access_token就是我们请求资源需要携带的令牌
// 设置刷新令牌的有效时间
defaultTokenServices.setRefreshTokenValiditySeconds(259200); // 3天
return defaultTokenServices;
}
private final String sign_key ="lane123";
/*
该方法用于创建tokenStore对象(令牌存储对象)
token以什么形式存储
*/
public TokenStore tokenStore(){
//return new InMemoryTokenStore();
// 使用jwt令牌
return new JwtTokenStore(jwtAccessTokenConverter());
}
/**
* 返回jwt令牌转换器(帮助我们生成jwt令牌的)
* 在这里,我们可以把签名密钥传递进去给转换器对象
* @return
*/
public JwtAccessTokenConverter jwtAccessTokenConverter() {
JwtAccessTokenConverter jwtAccessTokenConverter = new JwtAccessTokenConverter();
jwtAccessTokenConverter.setSigningKey(sign_key); // 签名密钥
jwtAccessTokenConverter.setVerifier(new MacSigner(sign_key)); // 验证时使用的密钥,和签名密钥保持一致
// jwtAccessTokenConverter.setAccessTokenConverter(lagouAccessTokenConvertor);
return jwtAccessTokenConverter;
}
}
访问测试
http://localhost:9999/oauth/token?client_secret=abcxyz&grant_type=password&username=admin&password=123456&client_id=client_lane
官网输入 token 和 secret 解密获取用户信息
修改资源微服务
不需要和远程认证服务器交互,添加本地 tokenStore,自己进行验证 token 合法性
修改后如下
package com.galaxy.config;
/**
* @author lane
* @date 2021年07月01日 上午10:57
*/
@Configuration
@EnableResourceServer // 开启资源服务器功能
@EnableWebSecurity // 开启web访问安全
public class ResourceServerConfiger extends ResourceServerConfigurerAdapter {
private final String sign_key = "lane123";
/**
* 该方法用于定义资源服务器向远程认证服务器发起请求,进行token校验等事宜
* @param resources
* @throws Exception
*/
@Override
public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
// // 设置当前资源服务的资源id
// resources.resourceId("autodeliver");
// // 定义token服务对象(token校验就应该靠token服务对象)
// RemoteTokenServices remoteTokenServices = new RemoteTokenServices();
// // 校验端点/接口设置
// remoteTokenServices.setCheckTokenEndpointUrl("http://localhost:9999/oauth/check_token");
// // 携带客户端id和客户端安全码
// remoteTokenServices.setClientId("client_lane");
// remoteTokenServices.setClientSecret("abcxyz");
// 别忘了这一步
// resources.tokenServices(remoteTokenServices);
// jwt令牌改造
resources.resourceId("autodeliver").tokenStore(tokenStore()).stateless(true);// 无状态设置
}
/**
* 场景:一个服务中可能有很多资源(API接口)
* 某一些API接口,需要先认证,才能访问
* 某一些API接口,压根就不需要认证,本来就是对外开放的接口
* 我们就需要对不同特点的接口区分对待(在当前configure方法中完成),设置是否需要经过认证
*
* @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(); // 其他请求不认证
}
/*
该方法用于创建tokenStore对象(令牌存储对象)
token以什么形式存储
*/
public TokenStore tokenStore(){
//return new InMemoryTokenStore();
// 使用jwt令牌
return new JwtTokenStore(jwtAccessTokenConverter());
}
/**
* 返回jwt令牌转换器(帮助我们生成jwt令牌的)
* 在这里,我们可以把签名密钥传递进去给转换器对象
* @return
*/
public JwtAccessTokenConverter jwtAccessTokenConverter() {
JwtAccessTokenConverter jwtAccessTokenConverter = new JwtAccessTokenConverter();
jwtAccessTokenConverter.setSigningKey(sign_key); // 签名密钥
jwtAccessTokenConverter.setVerifier(new MacSigner(sign_key)); // 验证时使用的密钥,和签名密钥保持一致
// jwtAccessTokenConverter.setAccessTokenConverter(lagouAccessTokenConvertor);
return jwtAccessTokenConverter;
}
}
测试下效果
没有携带 token
携带 token 访问
3.2 从数据库加载 Oauth2 客户端信息
在数据库中新建一张表
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
SET FOREIGN_KEY_CHECKS = 1;
添加客户端信息
INSERT INTO `oauth_client_details`
VALUES
( 'client_lane123', 'autodeliver,resume', 'abcxyz', 'all', 'password,refresh_token', NULL, NULL, 7200, 259200, NULL, NULL );
COMMIT;
配置数据源依赖
<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>
配置数据源
server:
port: 9999
spring:
application:
name: lane-cloud-oauth2-server
datasource:
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://localhost:3306/oauth2?useUnicode=true&characterEncoding=utf-8&useSSL=false&allowMultiQueries=true
username: root
password: root
druid:
initialSize: 10
minIdle: 10
maxActive: 30
maxWait: 50000
eureka:
client:
serviceUrl: # eureka server的路径
defaultZone: http://www.abc.com:8761/eureka/,http://www.def.com: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@
修改配置信息
package com.galaxy.config;
/**
* 当前类为Oauth2 server的配置类(需要继承特定的父类 AuthorizationServerConfigurerAdapter)
* @author lane
* @date 2021年06月29日 下午7:33
*/
@Configuration
@EnableAuthorizationServer //开启认证服务器功能
public class OauthServerConfig extends AuthorizationServerConfigurerAdapter {
@Autowired
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()");
}
/**
* 客户端详情配置,
* 比如client_id,secret
* 当前这个服务就如同QQ平台,拉勾网作为客户端需要qq平台进行登录授权认证等,提前需要到QQ平台注册,QQ平台会给拉勾网
* 颁发client_id等必要参数,表明客户端是谁
* @param clients
* @throws Exception
*/
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
super.configure(clients);
// 从内存中加载客户端详情
// clients.inMemory()// 客户端信息存储在什么地方,可以在内存中,可以在数据库里
// .withClient("client_lane") // 添加一个client配置,指定其client_id
// .secret("abcxyz") // 指定客户端的密码/安全码
// .resourceIds("autodeliver") // 指定客户端所能访问资源id清单,此处的资源id是需要在具体的资源服务器上也配置一样
// // 认证类型/令牌颁发模式,可以配置多个在这里,但是不一定都用,具体使用哪种方式颁发token,需要客户端调用的时候传递参数指定
// .authorizedGrantTypes("password","refresh_token")
// // 客户端的权限范围,此处配置为all全部即可
// .scopes("all");
// 从数据库中加载客户端详情
clients.withClientDetails(createJdbcClientDetailsService());
}
@Autowired
private DataSource dataSource;
@Bean
public JdbcClientDetailsService createJdbcClientDetailsService() {
JdbcClientDetailsService jdbcClientDetailsService = new JdbcClientDetailsService(dataSource);
return jdbcClientDetailsService;
}
/**
* 认证服务器是玩转token的,那么这里配置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);
}
/**
* 该方法用户获取一个token服务对象(该对象描述了token有效期等信息)
*/
public AuthorizationServerTokenServices authorizationServerTokenServices() {
// 使用默认实现
DefaultTokenServices defaultTokenServices = new DefaultTokenServices();
defaultTokenServices.setSupportRefreshToken(true); // 是否开启令牌刷新
defaultTokenServices.setTokenStore(tokenStore());
// 针对jwt令牌的添加
defaultTokenServices.setTokenEnhancer(jwtAccessTokenConverter());
// 设置令牌有效时间(一般设置为2个小时)
defaultTokenServices.setAccessTokenValiditySeconds(60); // access_token就是我们请求资源需要携带的令牌
// 设置刷新令牌的有效时间
defaultTokenServices.setRefreshTokenValiditySeconds(259200); // 3天
return defaultTokenServices;
}
private final String sign_key ="lane123";
/*
该方法用于创建tokenStore对象(令牌存储对象)
token以什么形式存储
*/
public TokenStore tokenStore(){
//return new InMemoryTokenStore();
// 使用jwt令牌
return new JwtTokenStore(jwtAccessTokenConverter());
}
/**
* 返回jwt令牌转换器(帮助我们生成jwt令牌的)
* 在这里,我们可以把签名密钥传递进去给转换器对象
* @return
*/
public JwtAccessTokenConverter jwtAccessTokenConverter() {
JwtAccessTokenConverter jwtAccessTokenConverter = new JwtAccessTokenConverter();
jwtAccessTokenConverter.setSigningKey(sign_key); // 签名密钥
jwtAccessTokenConverter.setVerifier(new MacSigner(sign_key)); // 验证时使用的密钥,和签名密钥保持一致
// jwtAccessTokenConverter.setAccessTokenConverter(lagouAccessTokenConvertor);
return jwtAccessTokenConverter;
}
}
添加用户信息
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
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;
BEGIN;
INSERT INTO `users`
VALUES
( 4, 'lane-user', 'iuxyzds' );
COMMIT;
package com.galaxy.pojo;
import lombok.Data;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.Table;
/**
* @author lane
* @date 2021年07月01日 下午3:43
*/
@Data
@Entity
@Table(name="users")
public class Users {
@Id
private Long id;
private String username;
private String password;
}
用户 dao
package com.galaxy.dao;
import com.galaxy.pojo.Users;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.security.core.userdetails.UserDetails;
/**
* @author lane
* @date 2021年07月01日 下午3:48
*/
public interface UsersRepository extends JpaRepository<Users,Long> {
Users findByUsername(String username);
}
service
package com.galaxy.service;
import com.galaxy.dao.UsersRepository;
import com.galaxy.pojo.Users;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
/**
* @author lane
* @date 2021年07月01日 下午3:35
*/
@Service
public class JdbcUserDetailsService implements UserDetailsService {
@Autowired
private UsersRepository usersRepository;
/**
* 根据username查询出该用户的所有信息,封装成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<>());
};
}
配置类
package com.galaxy.config;
import java.util.ArrayList;
/**
* 该配置类,主要处理⽤户名和密码的校验等事宜
* @author lane
* @date 2021年06月30日 上午11:42
*/
@Configuration
public class SecurityConfigure extends WebSecurityConfigurerAdapter {
@Autowired
private PasswordEncoder passwordEncoder;
@Autowired
private JdbcUserDetailsService jdbcUserDetailsService;
/**
* 注册一个认证管理器对象到容器
*/
@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);
//从数据库获取用户来进行校验
auth.userDetailsService(jdbcUserDetailsService).passwordEncoder(passwordEncoder);
}
}
测试结果
改为数据库用户测试成功获取 token
添加认证中心的网关
spring:
application:
name: lane-cloud-gateway
cloud:
gateway:
routes: # 路由可以有多个
- id: service-oauth2-router # 我们自定义的路由 ID,保持唯一
# uri: http://127.0.0.1:8090 # 目标服务地址 自动投递微服务(部署多实例) 动态路由:uri配置的应该是一个服务名称,而不应该是一个具体的服务实例的地址
uri: lb://lane-cloud-oauth2-server # gateway网关从服务注册中心获取实例信息然后负载后路由
predicates: # 断言:路由条件,Predicate 接受一个输入参数,返回一个布尔值结果。该接口包含多种默 认方法来将 Predicate 组合成其他复杂的逻辑(比如:与,或,非)。
- Path=/oauth/**
通过网关访问测试
3.3 基于 Oauth2 的 JWT 令牌信息扩展
OAuth2 帮我们⽣成的 JWT 令牌载荷部分信息有限,关于⽤户信息只有⼀个 user_name,有些场景下我们希望放⼊⼀些扩展信息项,⽐如,之前我们经常向 session 中存⼊ userId,或者现在我希望在 JWT 的载荷部分存⼊当时请求令牌的客户 端 IP,客户端携带令牌访问资源服务时,可以对⽐当前请求的客户端真实 IP 和令牌中 存放的客户端 IP 是否匹配,不匹配拒绝请求,以此进⼀步提⾼安全性。那么如何在 OAuth2 环境下向 JWT 令牌中存如扩展信息?
认证服务器⽣成 JWT 令牌时存⼊扩展信息(⽐如 clientIp)
继承 DefaultAccessTokenConverter 类,重写 convertAccessToken ⽅法存⼊扩 展信息
package com.galaxy.config;
import org.springframework.security.oauth2.common.OAuth2AccessToken;
import org.springframework.security.oauth2.provider.OAuth2Authentication;
import org.springframework.security.oauth2.provider.token.DefaultAccessTokenConverter;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;
import java.util.Map;
/**
* @author lane
* @date 2021年07月01日 下午6:08
*/
@Component
public class LaneAccessTokenConverter extends DefaultAccessTokenConverter {
@Override
public Map<String, ?> convertAccessToken(OAuth2AccessToken token, OAuth2Authentication authentication) {
// 获取到request对象
HttpServletRequest request = ((ServletRequestAttributes) (RequestContextHolder.getRequestAttributes())).getRequest();
// 获取客户端ip(注意:如果是经过代理之后到达当前服务的话,那么这种方式获取的并不是真实的浏览器客户端ip)
String remoteAddr = request.getRemoteAddr();
Map<String, String> stringMap = (Map<String, String>) super.convertAccessToken(token, authentication);
stringMap.put("clientIp",remoteAddr);
return stringMap;
}
}
资源服务器取出 JWT 令牌扩展信息
资源服务器也需要⾃定义⼀个转换器类,继承 DefaultAccessTokenConverter,重 写 extractAuthentication 提取⽅法,把载荷信息设置到认证对象的 details 属性中
package com.galaxy.config;
import org.springframework.security.oauth2.provider.OAuth2Authentication;
import org.springframework.security.oauth2.provider.token.DefaultAccessTokenConverter;
import org.springframework.stereotype.Component;
import java.util.Map;
/**
* @author lane
* @date 2021年07月01日 下午6:12
*/
@Component
public class LaneAccessTokenConverter extends DefaultAccessTokenConverter {
//取出认证信息
@Override
public OAuth2Authentication extractAuthentication(Map<String, ?> map) {
OAuth2Authentication oAuth2Authentication = super.extractAuthentication(map);
oAuth2Authentication.setDetails(map); // 将map放入认证对象中,认证对象在controller中可以拿到
return oAuth2Authentication;
}
}
将⾃定义的转换器对象注⼊
业务类⽐如 Controller 类中,可以通过 SecurityContextHolder.getContext().getAuthentication()获取到认证对象,进⼀步 获取到扩展信息获取到扩展信息后,就可以做其他的处理了,⽐如根据 userId 进⼀步处理,或者根 据 clientIp 处理,或者其他都是可以的了
测试访问如下
关于 JWT 令牌我们需要注意
JWT 令牌就是⼀种可以被验证的数据组织格式,它的玩法很灵活,我们这⾥是基 于 Spring Cloud Oauth2 创建、校验 JWT 令牌
我们也可以⾃⼰写⼯具类⽣成、校验 JWT 令牌
JWT 令牌中不要存放过于敏感的信息,因为我们知道拿到令牌后,我们可以解码 看到载荷部分的信息
JWT 令牌每次请求都会携带,内容过多,会增加⽹络带宽占⽤
结语
这部分主要是在 springcloud 基础上的进阶部分知识
链路追踪 slueth 是添加日志 traceid,spanid 等信息
zipkin 是抓取日志信息进行计算后的图形化展示
oauth2 是第三方授权信息
JWT 则是 token 的一种封装方式
在这部分当中也遇到了一个问题就是,微服务之间无法调用,后来打开 eureka 的页面发现 ip 竟然不一样有的是 192.168.1.111 有的是 172.20.1.2,在 配置中的 eureka 添加 下 ip-address: 127.0.0.1,就一致了