mirror of
https://github.com/zhwei820/learn.lianglianglee.com.git
synced 2025-09-17 16:56:40 +08:00
1349 lines
30 KiB
HTML
1349 lines
30 KiB
HTML
<!DOCTYPE html>
|
||
|
||
<!-- saved from url=(0046)https://kaiiiz.github.io/hexo-theme-book-demo/ -->
|
||
|
||
<html xmlns="http://www.w3.org/1999/xhtml">
|
||
|
||
<head>
|
||
|
||
<head>
|
||
|
||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
|
||
|
||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1.0, user-scalable=no">
|
||
|
||
<link rel="icon" href="/static/favicon.png">
|
||
|
||
<title>16 案例实战:基于 Spring Security 和 Spring Cloud 构建微服务安全架构.md.html</title>
|
||
|
||
<!-- Spectre.css framework -->
|
||
|
||
<link rel="stylesheet" href="/static/index.css">
|
||
|
||
<!-- theme css & js -->
|
||
|
||
<meta name="generator" content="Hexo 4.2.0">
|
||
|
||
</head>
|
||
|
||
|
||
|
||
<body>
|
||
|
||
|
||
|
||
<div class="book-container">
|
||
|
||
<div class="book-sidebar">
|
||
|
||
<div class="book-brand">
|
||
|
||
<a href="/">
|
||
|
||
<img src="/static/favicon.png">
|
||
|
||
<span>技术文章摘抄</span>
|
||
|
||
</a>
|
||
|
||
</div>
|
||
|
||
<div class="book-menu uncollapsible">
|
||
|
||
<ul class="uncollapsible">
|
||
|
||
<li><a href="/" class="current-tab">首页</a></li>
|
||
|
||
</ul>
|
||
|
||
|
||
|
||
<ul class="uncollapsible">
|
||
|
||
<li><a href="../">上一级</a></li>
|
||
|
||
</ul>
|
||
|
||
|
||
|
||
<ul class="uncollapsible">
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/Spring Security 详解与实操/00 开篇词 Spring Security,为你的应用安全与职业之路保驾护航.md.html">00 开篇词 Spring Security,为你的应用安全与职业之路保驾护航.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/Spring Security 详解与实操/01 顶级框架:Spring Security 是一款什么样的安全性框架?.md.html">01 顶级框架:Spring Security 是一款什么样的安全性框架?.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/Spring Security 详解与实操/02 用户认证:如何使用 Spring Security 构建用户认证体系?.md.html">02 用户认证:如何使用 Spring Security 构建用户认证体系?.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/Spring Security 详解与实操/03 认证体系:如何深入理解 Spring Security 用户认证机制?.md.html">03 认证体系:如何深入理解 Spring Security 用户认证机制?.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/Spring Security 详解与实操/04 密码安全:Spring Security 中包含哪些加解密技术?.md.html">04 密码安全:Spring Security 中包含哪些加解密技术?.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/Spring Security 详解与实操/05 访问授权:如何对请求的安全访问过程进行有效配置?.md.html">05 访问授权:如何对请求的安全访问过程进行有效配置?.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/Spring Security 详解与实操/06 权限管理:如何剖析 Spring Security 的授权原理?.md.html">06 权限管理:如何剖析 Spring Security 的授权原理?.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/Spring Security 详解与实操/07 案例实战:使用 Spring Security 基础功能保护 Web 应用.md.html">07 案例实战:使用 Spring Security 基础功能保护 Web 应用.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/Spring Security 详解与实操/08 管道过滤:如何基于 Spring Security 过滤器扩展安全性?.md.html">08 管道过滤:如何基于 Spring Security 过滤器扩展安全性?.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/Spring Security 详解与实操/09 攻击应对:如何实现 CSRF 保护和跨域 CORS?.md.html">09 攻击应对:如何实现 CSRF 保护和跨域 CORS?.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/Spring Security 详解与实操/10 全局方法:如何确保方法级别的安全访问?.md.html">10 全局方法:如何确保方法级别的安全访问?.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/Spring Security 详解与实操/11 案例实战:使用 Spring Security 高级主题保护 Web 应用.md.html">11 案例实战:使用 Spring Security 高级主题保护 Web 应用.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/Spring Security 详解与实操/12 开放协议:OAuth2 协议解决的是什么问题?.md.html">12 开放协议:OAuth2 协议解决的是什么问题?.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/Spring Security 详解与实操/13 授权体系:如何构建 OAuth2 授权服务器?.md.html">13 授权体系:如何构建 OAuth2 授权服务器?.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/Spring Security 详解与实操/14 资源保护:如何基于 OAuth2 协议配置授权过程?.md.html">14 资源保护:如何基于 OAuth2 协议配置授权过程?.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/Spring Security 详解与实操/15 令牌扩展:如何使用 JWT 实现定制化 Token?.md.html">15 令牌扩展:如何使用 JWT 实现定制化 Token?.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
<a class="current-tab" href="/专栏/Spring Security 详解与实操/16 案例实战:基于 Spring Security 和 Spring Cloud 构建微服务安全架构.md.html">16 案例实战:基于 Spring Security 和 Spring Cloud 构建微服务安全架构.md.html</a>
|
||
|
||
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/Spring Security 详解与实操/17 案例实战:基于 Spring Security 和 OAuth2 实现单点登录.md.html">17 案例实战:基于 Spring Security 和 OAuth2 实现单点登录.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/Spring Security 详解与实操/18 技术趋势:如何为 Spring Security 添加响应式编程特性?.md.html">18 技术趋势:如何为 Spring Security 添加响应式编程特性?.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/Spring Security 详解与实操/19 测试驱动:如何基于 Spring Security 测试系统安全性?.md.html">19 测试驱动:如何基于 Spring Security 测试系统安全性?.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/Spring Security 详解与实操/20 结束语 以终为始,Spring Security 的学习总结.md.html">20 结束语 以终为始,Spring Security 的学习总结.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
</ul>
|
||
|
||
|
||
|
||
</div>
|
||
|
||
</div>
|
||
|
||
|
||
|
||
<div class="sidebar-toggle" onclick="sidebar_toggle()" onmouseover="add_inner()" onmouseleave="remove_inner()">
|
||
|
||
<div class="sidebar-toggle-inner"></div>
|
||
|
||
</div>
|
||
|
||
|
||
|
||
<script>
|
||
|
||
function add_inner() {
|
||
|
||
let inner = document.querySelector('.sidebar-toggle-inner')
|
||
|
||
inner.classList.add('show')
|
||
|
||
}
|
||
|
||
|
||
|
||
function remove_inner() {
|
||
|
||
let inner = document.querySelector('.sidebar-toggle-inner')
|
||
|
||
inner.classList.remove('show')
|
||
|
||
}
|
||
|
||
|
||
|
||
function sidebar_toggle() {
|
||
|
||
let sidebar_toggle = document.querySelector('.sidebar-toggle')
|
||
|
||
let sidebar = document.querySelector('.book-sidebar')
|
||
|
||
let content = document.querySelector('.off-canvas-content')
|
||
|
||
if (sidebar_toggle.classList.contains('extend')) { // show
|
||
|
||
sidebar_toggle.classList.remove('extend')
|
||
|
||
sidebar.classList.remove('hide')
|
||
|
||
content.classList.remove('extend')
|
||
|
||
} else { // hide
|
||
|
||
sidebar_toggle.classList.add('extend')
|
||
|
||
sidebar.classList.add('hide')
|
||
|
||
content.classList.add('extend')
|
||
|
||
}
|
||
|
||
}
|
||
|
||
|
||
|
||
|
||
|
||
function open_sidebar() {
|
||
|
||
let sidebar = document.querySelector('.book-sidebar')
|
||
|
||
let overlay = document.querySelector('.off-canvas-overlay')
|
||
|
||
sidebar.classList.add('show')
|
||
|
||
overlay.classList.add('show')
|
||
|
||
}
|
||
|
||
function hide_canvas() {
|
||
|
||
let sidebar = document.querySelector('.book-sidebar')
|
||
|
||
let overlay = document.querySelector('.off-canvas-overlay')
|
||
|
||
sidebar.classList.remove('show')
|
||
|
||
overlay.classList.remove('show')
|
||
|
||
}
|
||
|
||
|
||
|
||
</script>
|
||
|
||
|
||
|
||
<div class="off-canvas-content">
|
||
|
||
<div class="columns">
|
||
|
||
<div class="column col-12 col-lg-12">
|
||
|
||
<div class="book-navbar">
|
||
|
||
<!-- For Responsive Layout -->
|
||
|
||
<header class="navbar">
|
||
|
||
<section class="navbar-section">
|
||
|
||
<a onclick="open_sidebar()">
|
||
|
||
<i class="icon icon-menu"></i>
|
||
|
||
</a>
|
||
|
||
</section>
|
||
|
||
</header>
|
||
|
||
</div>
|
||
|
||
<div class="book-content" style="max-width: 960px; margin: 0 auto;
|
||
|
||
overflow-x: auto;
|
||
|
||
overflow-y: hidden;">
|
||
|
||
<div class="book-post">
|
||
|
||
<p id="tip" align="center"></p>
|
||
|
||
<div><h1>16 案例实战:基于 Spring Security 和 Spring Cloud 构建微服务安全架构</h1>
|
||
|
||
<p>通过前面课程的学习,我们已经知道 Spring Security 可以集成 OAuth2 协议并实现分布式环境下的访问授权。同时,Spring Security 也可以和 Spring Cloud 框架无缝集成,并完成对各个微服务的权限控制。</p>
|
||
|
||
<p>今天我们将设计一个案例系统,从零构建一个完整的微服务系统,除了演示微服务系统构建过程,还将重点展示 OAuth2 协议以及 JWT 在其中所起到的作用。</p>
|
||
|
||
<h3>案例驱动:SpringAppointment</h3>
|
||
|
||
<p>在本课程中,我们通过构建一个相对精简的完整系统,来展示微服务架构相关的设计理念以及各项技术组件,这个案例系统称为 SpringAppointment。</p>
|
||
|
||
<p>SpringAppointment 包含的业务场景比较简单,可以用来模拟就医过程中的预约处理流程。一般而言,预约流程势必会涉及三个独立的微服务,即就诊卡(Card)服务、预约(Appointment)服务,以及医生(Doctor)服务。</p>
|
||
|
||
<p>我们把以上三个服务统称为<strong>业务服务</strong>。纵观整个 SpringAppointment 系统,除了这三个业务微服务之外,还有一批非业务性的基础设施类服务,具体包括:注册中心服务(Eureka)、配置中心服务(Spring Cloud Config),以及 API 网关服务(Zuul)。关于 Spring Cloud 中基础设施类服务的构建过程不是本专栏的重点,你可以参考拉勾上[《Spring Cloud 原理与实战》]专栏做详细了解。</p>
|
||
|
||
<p>虽然案例中的各个服务在物理上都是独立的,但就整个系统而言,需要各服务相互协作构成一个完整的微服务系统。也就是说,服务运行时存在一定的依赖性。我们结合系统架构对 SpringAppointment 的运行方式进行梳理,梳理的基本方法就是按照服务列表构建独立服务,并基于注册中心来管理它们之间的依赖关系,如下图所示:</p>
|
||
|
||
<p><img src="assets/Cgp9HWDuW_OAckmvAAIEbG-aKzA773.png" alt="图片1.png" /></p>
|
||
|
||
<p>基于注册中心的服务运行时依赖关系图</p>
|
||
|
||
<h3>构建 OAuth2 授权服务</h3>
|
||
|
||
<p>在上图中,我们注意到还存在着案例系统中的最后一个基础设施类微服务,即 OAuth2 授权服务,在这里充当着授权中心的作用。关于 OAuth2 授权服务的具体构建步骤已经在[《授权体系:如何在微服务架构中集成OAuth2协议?》]做了详细介绍,这里我们直接创建 WebSecurityConfigurerAdapter 的子类 WebSecurityConfigurer</p>
|
||
|
||
<p>以及 AuthorizationServerConfigurerAdapter 的子类 JWTOAuth2Config,实现代码如下所示:</p>
|
||
|
||
<pre><code>@Configuration
|
||
|
||
|
||
|
||
public class WebSecurityConfigurer extends WebSecurityConfigurerAdapter {
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
@Override
|
||
|
||
|
||
|
||
@Bean
|
||
|
||
|
||
|
||
public AuthenticationManager authenticationManagerBean() throws Exception {
|
||
|
||
|
||
|
||
return super.authenticationManagerBean();
|
||
|
||
|
||
|
||
}
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
@Override
|
||
|
||
|
||
|
||
@Bean
|
||
|
||
|
||
|
||
public UserDetailsService userDetailsServiceBean() throws Exception {
|
||
|
||
|
||
|
||
return super.userDetailsServiceBean();
|
||
|
||
|
||
|
||
}
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
@Override
|
||
|
||
|
||
|
||
protected void configure(AuthenticationManagerBuilder builder) throws Exception {
|
||
|
||
|
||
|
||
builder.inMemoryAuthentication().withUser("user").password("{noop}password1").roles("USER").and()
|
||
|
||
|
||
|
||
.withUser("admin").password("{noop}password2").roles("USER", "ADMIN");
|
||
|
||
|
||
|
||
}
|
||
|
||
|
||
|
||
}
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
@Configuration
|
||
|
||
|
||
|
||
public class JWTOAuth2Config extends AuthorizationServerConfigurerAdapter {
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
@Autowired
|
||
|
||
|
||
|
||
private AuthenticationManager authenticationManager;
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
@Autowired
|
||
|
||
|
||
|
||
private UserDetailsService userDetailsService;
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
@Override
|
||
|
||
|
||
|
||
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
|
||
|
||
|
||
|
||
endpoints.authenticationManager(authenticationManager).userDetailsService(userDetailsService);
|
||
|
||
|
||
|
||
}
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
@Override
|
||
|
||
|
||
|
||
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
clients.inMemory().withClient("appointment_client").secret("{noop}appointment_secret")
|
||
|
||
|
||
|
||
.authorizedGrantTypes("refresh_token", "password", "client_credentials")
|
||
|
||
|
||
|
||
.scopes("webclient", "mobileclient");
|
||
|
||
|
||
|
||
}
|
||
|
||
|
||
|
||
}
|
||
|
||
</code></pre>
|
||
|
||
<h3>初始化业务服务</h3>
|
||
|
||
<p>在 SpringAppointment 案例系统中,我们需要构建三个业务微服务,即 card-service、appointment-service 和 doctor-service,它们都是独立的 Spring Boot 应用程序。在构建业务服务时,我们首先需要完成它们与基础设施类服务集成。因为 API 网关起到的是服务路由作用,所以对于各个业务服务而言是透明的,而其他的注册中心、配置中心和授权中心都需要每个业务服务完成与它们之间的集成。</p>
|
||
|
||
<h4>集成注册中心</h4>
|
||
|
||
<p>对于注册中心 Eureka 而言,card-service、appointment-service 和 doctor-service 都是它的客户端,所以需要 spring-cloud-starter-netflix-eureka-client 的依赖,如下所示。</p>
|
||
|
||
<pre><code><dependency>
|
||
|
||
|
||
|
||
<groupId>org.springframework.cloud</groupId>
|
||
|
||
|
||
|
||
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
|
||
|
||
|
||
|
||
</dependency>
|
||
|
||
</code></pre>
|
||
|
||
<p>然后,我们以 appointment-service 为例,来看它的 Bootstrap 类,如下所示:</p>
|
||
|
||
<pre><code>@SpringBootApplication
|
||
|
||
|
||
|
||
@EnableEurekaClient
|
||
|
||
|
||
|
||
public class AppointmentApplication {
|
||
|
||
|
||
|
||
public static void main(String[] args) {
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
SpringApplication.run(AppointmentApplication.class, args);
|
||
|
||
|
||
|
||
}
|
||
|
||
|
||
|
||
}
|
||
|
||
</code></pre>
|
||
|
||
<p>这里引入了一个新的注解 @EnableEurekaClient,该注解用于表明当前服务就是一个 Eureka 客户端,这样该服务就可以自动注册到 Eureka 服务器。当然,我们也可以直接使用统一的 @SpringCloudApplication 注解来实现 @SpringBootApplication 和 @EnableEurekaClient这两个注解整合在一起的效果。</p>
|
||
|
||
<p>接下来就是最重要的配置工作,appointment-service 中的配置内容如下所示:</p>
|
||
|
||
<pre><code>spring:
|
||
|
||
|
||
|
||
application:
|
||
|
||
|
||
|
||
name: appointmentservice
|
||
|
||
|
||
|
||
server:
|
||
|
||
|
||
|
||
port: 8081
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
eureka:
|
||
|
||
|
||
|
||
client:
|
||
|
||
|
||
|
||
registerWithEureka: true
|
||
|
||
|
||
|
||
fetchRegistry: true
|
||
|
||
|
||
|
||
serviceUrl:
|
||
|
||
|
||
|
||
defaultZone: http://localhost:8761/eureka/
|
||
|
||
</code></pre>
|
||
|
||
<p>显然,这里包含两段配置内容。其中,第一段配置指定了服务的名称和运行时端口。在上面的示例中 appointment-service 的名称通过“spring.application.name=appointmentservice”进行指定,也就是说 appointment-service 在注册中心中的名称为 appointmentservice。在后续的示例中,我们会使用这一名称获取 appointment-service 在 Eureka 中的各种注册信息。</p>
|
||
|
||
<h4>集成配置中心</h4>
|
||
|
||
<p>要想获取配置服务器中的配置信息,我们首先需要初始化客户端,也就是在将各个业务微服务与 Spring Cloud Config 服务器端进行集成。初始化客户端的第一步是引入 Spring Cloud Config 的客户端组件 spring-cloud-config-client,如下所示。</p>
|
||
|
||
<pre><code><dependency>
|
||
|
||
|
||
|
||
<groupId>org.springframework.cloud</groupId>
|
||
|
||
|
||
|
||
<artifactId>spring-cloud-config-client</artifactId>
|
||
|
||
|
||
|
||
</dependency>
|
||
|
||
</code></pre>
|
||
|
||
<p>然后我们需要在配置文件 application.yml 中配置服务器的访问地址,如下所示:</p>
|
||
|
||
<pre><code>spring:
|
||
|
||
|
||
|
||
cloud:
|
||
|
||
|
||
|
||
config:
|
||
|
||
|
||
|
||
enabled: true
|
||
|
||
|
||
|
||
uri: http://localhost:8888
|
||
|
||
</code></pre>
|
||
|
||
<p>以上配置信息中,我们指定了配置服务器所在的地址,也就是上面的 uri:<a href="http://localhost:8888/">http://localhost:8888</a>。</p>
|
||
|
||
<p>一旦我们引入了 Spring Cloud Config 的客户端组件,相当于在各个微服务中自动集成了访问配置服务器中 HTTP 端点的功能。也就是说,访问配置服务器的过程对于各个微服务而言是透明的,即微服务不需要考虑如何从远程服务器获取配置信息,而只需要考虑如何在 Spring Boot 应用程序中使用这些配置信息。而对于常见的关系型数据访问配置而言,Spring 已经帮助我们内置了整合过程,我们要做的就是引入相关的依赖组件而已。</p>
|
||
|
||
<p>我们以 appointment-service 为例来演示数据库访问功能,案例中使用的是 JPA 和 MySQL,因此需要在服务中引入相关的依赖,如下所示:</p>
|
||
|
||
<pre><code><dependency>
|
||
|
||
|
||
|
||
<groupId>org.springframework.boot</groupId>
|
||
|
||
|
||
|
||
<artifactId>spring-boot-starter-data-jpa</artifactId>
|
||
|
||
|
||
|
||
</dependency>
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
<dependency>
|
||
|
||
|
||
|
||
<groupId>mysql</groupId>
|
||
|
||
|
||
|
||
<artifactId>mysql-connector-java</artifactId>
|
||
|
||
|
||
|
||
</dependency>
|
||
|
||
</code></pre>
|
||
|
||
<p>现在,我们就可以使用 JPA 提供的数据访问功能来访问 MySQL 数据库了。</p>
|
||
|
||
<h4>集成授权中心</h4>
|
||
|
||
<p>在业务服务中集成授权中心的实现方法,我们已经在[《14.资源保护:如何使用OAuth2协议实现对微服务访问进行授权?》]中做了详细介绍,这里做一些简单的回顾。首先,我们需要在 Spring Boot 的启动类上添加 @EnableResourceServer 注解:</p>
|
||
|
||
<pre><code>@SpringCloudApplication
|
||
|
||
|
||
|
||
@EnableResourceServer
|
||
|
||
|
||
|
||
public class AppointmentApplication {
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
public static void main(String[] args) {
|
||
|
||
|
||
|
||
SpringApplication.run(AppointmentApplication.class, args);
|
||
|
||
|
||
|
||
}
|
||
|
||
|
||
|
||
}
|
||
|
||
</code></pre>
|
||
|
||
<p>然后,我们需要在配置文件中指定授权中心服务的地址:</p>
|
||
|
||
<pre><code>security:
|
||
|
||
|
||
|
||
oauth2:
|
||
|
||
|
||
|
||
resource:
|
||
|
||
|
||
|
||
userInfoUri: http://localhost:8080/userinfo
|
||
|
||
</code></pre>
|
||
|
||
<p>最后,要做的就是在每个业务服务中嵌入访问授权控制。我们可以使用用户层级的权限访问控制、用户+角色层级的权限访问控制,以及用户+角色+操作层级的权限访问控制这三种策略中的任意一种来实现这一目标。</p>
|
||
|
||
<h3>集成和扩展 JWT</h3>
|
||
|
||
<p>让我们再次回到 SpringAppointment 案例系统,以用户下单这一业务场景为例,就涉及 appointment-service 同时调用 doctor-service 和 card-service,这三个服务之间的交互方式如下图所示:
|
||
|
||
<img src="assets/CioPOWDuXAyAPXM5AAGY7XqKIyA677.png" alt="图片2.png" /></p>
|
||
|
||
<p>SpringAppointment 案例系统中三个业务微服务的交互方式图</p>
|
||
|
||
<p>通过这个交互图,实际上我们已经可以梳理出这一场景下的代码结构了,如下所示:</p>
|
||
|
||
<pre><code>public Appointment generateAppointment(String doctorName, String cardCode) {
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
Appointment appointment = new Appointment();
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
//获取远程 Card 信息
|
||
|
||
|
||
|
||
CardMapper card = getCard(cardCode);
|
||
|
||
|
||
|
||
…
|
||
|
||
|
||
|
||
//获取远程 Doctor 信息
|
||
|
||
|
||
|
||
DoctorMapper doctor = getDoctor(doctorName);
|
||
|
||
|
||
|
||
…
|
||
|
||
|
||
|
||
appointmentRepository.save(appointment);
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
return appointment;
|
||
|
||
|
||
|
||
}
|
||
|
||
</code></pre>
|
||
|
||
<p>其中 appointment-service 从 card-service 获取 Card 对象,以及从 doctor-service 中获取 Doctor 对象,这两个步骤都会涉及远程 Web 服务的访问。因此,我们首先需要分别在 card-service 和 doctor-service 服务中创建对应的 HTTP 端点。这一过程不是课程的重点,如果你感兴趣,可以参考案例源码自己进行学习:<a href="https://github.com/lagouEdAnna/SpringSecurity-jianxiang/tree/main/SpringAppointment">https://github.com/lagouEdAnna/SpringSecurity-jianxiang/tree/main/SpringAppointment</a>。</p>
|
||
|
||
<h4>集成 JWT</h4>
|
||
|
||
<p>在《15 | 令牌扩展:如何使用JWT实现定制化 Token?》中,我们引入了 JWT 并完成了与 OAuth2 协议的集成,从而实现了定制化的 Token。JWT 同样也需要在整个服务调用链路中进行传递。而持有 JWT 的客户端访问 appointment-service 提供的 HTTP 端点进行下单操作,该服务会验证所传入 JWT 的有效性。然后,appointment-service 会再通过网关访问 card-service 和 doctor-service,同样这两个服务也会分别对所传入的 JWT 进行验证,并返回相应的结果。</p>
|
||
|
||
<p>现在,让我们在 appointment-service 中构建一个 CardRestTemplateClient 类,会发现它使用了在《15 | 令牌扩展:如何使用 JWT 实现定制化 Token?》中所创建的 RestTemplate 对象来发起远程调用,代码如下所示:</p>
|
||
|
||
<pre><code>@Service
|
||
|
||
|
||
|
||
public class CardRestTemplateClient {
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
@Autowired
|
||
|
||
|
||
|
||
RestTemplate restTemplate;
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
public CardMapper getCardByCardCode(String cardCode) {
|
||
|
||
|
||
|
||
ResponseEntity<CardMapper> result =
|
||
|
||
|
||
|
||
restTemplate.exchange("http://cardservice/cards/{cardCode}", HttpMethod.GET, null,
|
||
|
||
|
||
|
||
CardMapper.class, cardCode);
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
return result.getBody();
|
||
|
||
|
||
|
||
}
|
||
|
||
|
||
|
||
}
|
||
|
||
</code></pre>
|
||
|
||
<p>我们知道在这个 RestTemplate 中,基于 AuthorizationHeaderInterceptor 对请求进行了拦截,从而完成了 JWT 在各个服务中的正确传播。</p>
|
||
|
||
<p>最后,我们通过 Postman 来验证以上流程的正确性。通过访问 Zuul 中配置的 appointment-service 端点,并传入角色为“ADMIN”的用户对应的 Token 信息,可以看到订单记录已经被成功创建。你可以尝试通过生成不同的 Token 来执行这一流程,并验证授权效果。</p>
|
||
|
||
<h4>扩展 JWT</h4>
|
||
|
||
<p>在案例的最后,我们来讨论一下如何扩展 JWT。JWT 具有良好的可扩展性,开发人员可以根据需要在 JWT Token 中添加自己想要添加的各种附加信息。</p>
|
||
|
||
<p>针对 JWT 的扩展性场景,Spring Security 专门提供了一个 TokenEnhancer 接口来对 Token 进行增强(Enhance),该接口定义如下:</p>
|
||
|
||
<pre><code>public interface TokenEnhancer {
|
||
|
||
|
||
|
||
OAuth2AccessToken enhance(OAuth2AccessToken accessToken, OAuth2Authentication authentication);
|
||
|
||
|
||
|
||
}
|
||
|
||
</code></pre>
|
||
|
||
<p>可以看到这里处理的是一个 OAuth2AccessToken 接口,而该接口有一个默认的实现类 DefaultOAuth2AccessToken。我们可以通过该实现类的 setAdditionalInformation 方法,以键值对的方式将附加信息添加到 OAuth2AccessToken 中,示例代码如下所示:</p>
|
||
|
||
<pre><code>public class JWTTokenEnhancer implements TokenEnhancer {
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
@Override
|
||
|
||
|
||
|
||
public OAuth2AccessToken enhance(OAuth2AccessToken accessToken, OAuth2Authentication authentication) {
|
||
|
||
|
||
|
||
Map<String, Object> systemInfo = new HashMap<>();
|
||
|
||
|
||
|
||
systemInfo.put("system", "Appointment System");
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
((DefaultOAuth2AccessToken) accessToken).setAdditionalInformation(systemInfo);
|
||
|
||
|
||
|
||
return accessToken;
|
||
|
||
|
||
|
||
}
|
||
|
||
|
||
|
||
}
|
||
|
||
</code></pre>
|
||
|
||
<p>这里我们以硬编码的方式添加了一个“system”属性,你也可以根据需要进行相应的调整。</p>
|
||
|
||
<p>要想使得上述 JWTTokenEnhancer 类能够生效,我们需要对 JWTOAuth2Config 类中的 configure 方法进行重新配置,并将 JWTTokenEnhancer 嵌入到 TokenEnhancerChain 中,如下所示:</p>
|
||
|
||
<pre><code>@Override
|
||
|
||
|
||
|
||
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
|
||
|
||
|
||
|
||
TokenEnhancerChain tokenEnhancerChain = new TokenEnhancerChain();
|
||
|
||
|
||
|
||
tokenEnhancerChain.setTokenEnhancers(Arrays.asList(jwtTokenEnhancer, jwtAccessTokenConverter));
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
endpoints.tokenStore(tokenStore)
|
||
|
||
|
||
|
||
.accessTokenConverter(jwtAccessTokenConverter)
|
||
|
||
|
||
|
||
.tokenEnhancer(tokenEnhancerChain)
|
||
|
||
|
||
|
||
.authenticationManager(authenticationManager)
|
||
|
||
|
||
|
||
.userDetailsService(userDetailsService);
|
||
|
||
|
||
|
||
}
|
||
|
||
</code></pre>
|
||
|
||
<p>请注意,我们在这里通过创建一个 TokenEnhancer 的列表将包括 JWTTokenEnhancer 在内的多个 TokenEnhancer 嵌入到 TokenEnhancerChain 中。</p>
|
||
|
||
<p>现在,我们已经扩展了 JWT Token。那么,如何从这个 JWT Token 中获取所扩展的属性呢?方法也比较简单和固定,如下所示:</p>
|
||
|
||
<pre><code>//获取 JWTToken
|
||
|
||
|
||
|
||
RequestContext ctx = RequestContext.getCurrentContext();
|
||
|
||
|
||
|
||
String authorizationHeader = ctx.getRequest().getHeader(AUTHORIZATION_HEADER);
|
||
|
||
|
||
|
||
String jwtToken = authorizationHeader.replace("Bearer ","");
|
||
|
||
|
||
|
||
//解析 JWTToken
|
||
|
||
|
||
|
||
String[] split_string = jwtToken.split("\\.");
|
||
|
||
|
||
|
||
String base64EncodedBody = split_string[1];
|
||
|
||
|
||
|
||
Base64 base64Url = new Base64(true);
|
||
|
||
|
||
|
||
String body = new String(base64Url.decode(base64EncodedBody));
|
||
|
||
|
||
|
||
JSONObject jsonObj = new JSONObject(body);
|
||
|
||
|
||
|
||
//获取定制化属性值
|
||
|
||
|
||
|
||
String systemName = jsonObj.getString("system");
|
||
|
||
</code></pre>
|
||
|
||
<p>我们可以把这段代码嵌入到需要使用到自定义“system”属性的任何场景中。</p>
|
||
|
||
<h3>小结与预告</h3>
|
||
|
||
<p>案例分析是掌握一个框架应用方式的最好方法,对于 OAuth2 协议也是一样。本讲中,我们将 Spring Security 结合 Spring Cloud 构建了一个微服务案例系统 SpringAppointment。然后根据 SpringAppointment 案例中的业务场景划分了各个微服务,并重点介绍了各个业务服务的构建过程。我们一方面展示了业务服务与基础设施服务的集成过程,另一方面也演示了如何集成和扩展 JWT 的实现过程。</p>
|
||
|
||
<p>最后再给你留一道思考题:在业务系统中如何实现对 JWT 进行定制化的扩展呢?欢迎在留言区和我分享你的收获。</p>
|
||
|
||
</div>
|
||
|
||
</div>
|
||
|
||
<div>
|
||
|
||
<div style="float: left">
|
||
|
||
<a href="/专栏/Spring Security 详解与实操/15 令牌扩展:如何使用 JWT 实现定制化 Token?.md.html">上一页</a>
|
||
|
||
</div>
|
||
|
||
<div style="float: right">
|
||
|
||
<a href="/专栏/Spring Security 详解与实操/17 案例实战:基于 Spring Security 和 OAuth2 实现单点登录.md.html">下一页</a>
|
||
|
||
</div>
|
||
|
||
</div>
|
||
|
||
|
||
|
||
</div>
|
||
|
||
</div>
|
||
|
||
</div>
|
||
|
||
</div>
|
||
|
||
|
||
|
||
<a class="off-canvas-overlay" onclick="hide_canvas()"></a>
|
||
|
||
</div>
|
||
|
||
<script defer src="https://static.cloudflareinsights.com/beacon.min.js/v652eace1692a40cfa3763df669d7439c1639079717194" integrity="sha512-Gi7xpJR8tSkrpF7aordPZQlW2DLtzUlZcumS8dMQjwDHEnw9I7ZLyiOj/6tZStRBGtGgN6ceN6cMH8z7etPGlw==" data-cf-beacon='{"rayId":"709975783e343d60","version":"2021.12.0","r":1,"token":"1f5d475227ce4f0089a7cff1ab17c0f5","si":100}' crossorigin="anonymous"></script>
|
||
|
||
</body>
|
||
|
||
<!-- Global site tag (gtag.js) - Google Analytics -->
|
||
|
||
<script async src="https://www.googletagmanager.com/gtag/js?id=G-NPSEEVD756"></script>
|
||
|
||
<script>
|
||
|
||
window.dataLayer = window.dataLayer || [];
|
||
|
||
|
||
|
||
function gtag() {
|
||
|
||
dataLayer.push(arguments);
|
||
|
||
}
|
||
|
||
|
||
|
||
gtag('js', new Date());
|
||
|
||
gtag('config', 'G-NPSEEVD756');
|
||
|
||
var path = window.location.pathname
|
||
|
||
var cookie = getCookie("lastPath");
|
||
|
||
console.log(path)
|
||
|
||
if (path.replace("/", "") === "") {
|
||
|
||
if (cookie.replace("/", "") !== "") {
|
||
|
||
console.log(cookie)
|
||
|
||
document.getElementById("tip").innerHTML = "<a href='" + cookie + "'>跳转到上次进度</a>"
|
||
|
||
}
|
||
|
||
} else {
|
||
|
||
setCookie("lastPath", path)
|
||
|
||
}
|
||
|
||
|
||
|
||
function setCookie(cname, cvalue) {
|
||
|
||
var d = new Date();
|
||
|
||
d.setTime(d.getTime() + (180 * 24 * 60 * 60 * 1000));
|
||
|
||
var expires = "expires=" + d.toGMTString();
|
||
|
||
document.cookie = cname + "=" + cvalue + "; " + expires + ";path = /";
|
||
|
||
}
|
||
|
||
|
||
|
||
function getCookie(cname) {
|
||
|
||
var name = cname + "=";
|
||
|
||
var ca = document.cookie.split(';');
|
||
|
||
for (var i = 0; i < ca.length; i++) {
|
||
|
||
var c = ca[i].trim();
|
||
|
||
if (c.indexOf(name) === 0) return c.substring(name.length, c.length);
|
||
|
||
}
|
||
|
||
return "";
|
||
|
||
}
|
||
|
||
|
||
|
||
</script>
|
||
|
||
|
||
|
||
</html>
|
||
|