This commit is contained in:
by931
2022-09-06 22:30:37 +08:00
parent 66970f3e38
commit 3d6528675a
796 changed files with 3382 additions and 3382 deletions

View File

@@ -161,12 +161,12 @@ function hide_canvas() {
<p>说到安全性你可能会想到加解密算法、HTTPS 协议等常见的技术体系,<strong>但系统安全是一个综合性的主题,并非简单采用一些技术体系就能构建有效的解决方案</strong></p>
<p>我们以一个分布式环境下的应用场景为例。假设你要开发一个工单系统,而生成工单所依赖的用户订单信息维护在第三方订单系统中。为了生成工单,就必须让工单系统读取订单系统中的用户订单信息。</p>
<p>那么问题来了,工单系统如何获得用户的授权呢?一般我们想到的方法是用户将自己在订单管理平台上用户名和密码告诉工单系统,然后工单系统通过用户名和密码登录到订单管理平台并读取用户的订单记录,整个过程如下图所示:</p>
<p><img src="assets/CioPOWC3WquAXuQVAACRr8Z_UXc476.png" alt="Drawing 0.png" /></p>
<p><img src="assets/CioPOWC3WquAXuQVAACRr8Z_UXc476.png" alt="png" /></p>
<p>订单系统用户认证和授权交互示意图</p>
<p>上述方案看起来没有什么问题,但如果你仔细分析一下,就会发现这个流程在安全性上存在一些漏洞。</p>
<p>比如,一旦用户修改了订单管理平台的密码,工单系统就无法正常访问了。为此,我们<strong>需要引入诸如 OAuth2 协议完成分布式环境下的认证和授权</strong></p>
<p>我们通过一张图简单对比一下 OAuth2 协议与传统实现方案:</p>
<p><img src="assets/CioPOWC3WrKADX7QAACv6cIqFgQ494.png" alt="Drawing 1.png" /></p>
<p><img src="assets/CioPOWC3WrKADX7QAACv6cIqFgQ494.png" alt="png" /></p>
<p>OAuth2 协议与传统实现方案的对比图</p>
<p>但是想要实现 OAuth2 协议并没有那么简单。OAuth2 协议涉及的技术体系非常复杂需要综合考虑用户认证、密码加解密和存储、Token 生成和校验、分布式 Session 和公私钥管理,以及完成各个客户端的权限管理。这时就<strong>需要引入专门的安全性开发框架</strong>,而<strong>Spring Security 就是这样一款开发框架</strong></p>
<p>Spring Security 专门提供了 UAAUser Account and Authentication用户账户和认证服务封装了 OAuth2 协议用于管理用户账户、OAuth2 客户端以及用于鉴权的 Token。而 UAA 也只是 Spring Security 众多核心功能中的一部分。</p>
@@ -185,7 +185,7 @@ function hide_canvas() {
<li>……</li>
</ul>
<p>同时,<strong>在普遍倡导用户隐私和数据价值的当下,掌握各种安全性相关技术的开发人员和架构师也成了稀缺人才,越发受到行业的认可和重视</strong></p>
<p><img src="assets/CioPOWC3WrqAGPu1AAClhPaa34E826.png" alt="Drawing 2.png" /></p>
<p><img src="assets/CioPOWC3WrqAGPu1AAClhPaa34E826.png" alt="png" /></p>
<p>(职位信息来源:拉勾网)</p>
<p>对于开发人员而言,如何<strong>使用各种技术体系解决安全性问题</strong>是一大困惑。经验丰富的开发人员需要熟练使用 Spring Security 框架来应对业务发展的需求。例如,全面掌握 Spring Security 框架提供的认证、授权、方法级安全访问、OAuth2、JWT 等核心功能,构建自己对系统安全性设计的知识体系和解决方案。</p>
<p>而对于架构师而言,难点在于如何<strong>基于框架提供的功能并结合具体的业务场景,对框架进行扩展和定制化开发</strong>。这就需要他们对 Spring Security 中的用户认证和访问授权等核心功能的设计原理有充分的了解,能够从源码级别剖析框架的底层实现机制,进而满足更深层次的需求。</p>

View File

@@ -173,7 +173,7 @@ public class DemoController {
}
</code></pre>
<p>现在,启动这个 Spring Boot 应用程序,然后通过浏览器访问&quot;/hello&quot;端点。你可能希望得到的是&quot;Hello World!&quot;这个输出结果,但事实上,浏览器会跳转到一个如下所示的登录界面:</p>
<p><img src="assets/Cgp9HWC5_NmAKJV2AABBm4b32Ns284.png" alt="Drawing 0.png" /></p>
<p><img src="assets/Cgp9HWC5_NmAKJV2AABBm4b32Ns284.png" alt="png" /></p>
<p>Spring Security 内置的登录界面</p>
<p>那么,为什么会弹出这个登录界面呢?原因就在于我们添加了 spring-boot-starter-security 依赖之后Spring Security 为应用程序<strong>自动嵌入了用户认证机制</strong></p>
<p>接下来,我们围绕这个登录场景,分析如何获取登录所需的用户名和密码。我们注意到在 Spring Boot 的控制台启动日志中,出现了如下所示的一行日志:</p>
@@ -188,28 +188,28 @@ public class DemoController {
<p>首先我们需要明确,所谓认证,解决的是“你是谁”这一个问题,也就是说对于每一次访问请求,系统都能判断出访问者是否具有合法的身份标识。</p>
<p>一旦明确 “<strong>你是谁</strong>”,下一步就可以判断“<strong>你能做什么</strong>”,这个步骤就是授权。通用的授权模型大多基于权限管理体系,即对资源、权限、角色和用户的一种组合处理。</p>
<p>如果我们将认证和授权结合起来,就构成了对系统中的资源进行安全性管理的最常见解决方案,即先判断资源访问者的有效身份,再来确定其是否有对这个资源进行访问的合法权限,如下图所示:</p>
<p><img src="assets/CioPOWC5_OuABmKhAABO7TPypr0449.png" alt="Drawing 2.png" /></p>
<p><img src="assets/CioPOWC5_OuABmKhAABO7TPypr0449.png" alt="png" /></p>
<p>基于认证和授权机制的资源访问安全性示意图</p>
<p>上图代表的是一种通用方案而不同的应用场景和技术体系下可以衍生出很多具体的实现策略。Web 应用系统中的认证和授权模型与上图类似,但在具体设计和实现过程中也有其特殊性。</p>
<p>针对认证,这部分的需求相对比较明确。显然我们需要构建<strong>一套完整的存储体系</strong>来保存和维护用户信息,并且确保这些用户信息在处理请求的过程中能够得到合理的利用。</p>
<p>而针对授权,情况可能会比较复杂。对于某一个特定的 Web 应用程序而言,我们面临的第一个问题是如何判断一个 HTTP 请求是否具备访问自己的权限。更进一步,就算这个请求具备访问该应用程序的权限,但并不意味着该请求能够访问应用程序所有的 HTTP 端点。某些核心功能需要具备较高的权限才能访问,而有些则不需要。这就是我们需要解决的第二个问题,如何<strong>对访问的权限进行精细化管理</strong>?如下图所示:</p>
<p><img src="assets/CioPOWC5_POAP95KAABUwwJSnkM160.png" alt="Drawing 4.png" /></p>
<p><img src="assets/CioPOWC5_POAP95KAABUwwJSnkM160.png" alt="png" /></p>
<p>Web 应用程序访问授权效果示意图</p>
<p>在上图中,我们假设该请求具备对应用程序中端点 2、3、4 的访问权限,但不具备访问端点 1 的权限。想要达到这种效果,一般的做法是<strong>引入角色体系</strong>。我们对不同的用户设置不同等级的角色,角色等级不同对应的访问权限也不同。而每一个请求都可以绑定到某一个角色,也就具备了访问权限。</p>
<p>接下来,我们把认证和授权结合起来,梳理出 Web 应用程序访问场景下的安全性实现方案,如下图所示:</p>
<p><img src="assets/CioPOWC5_QiAM0UXAAA8v_6Frio548.png" alt="Drawing 6.png" /></p>
<p><img src="assets/CioPOWC5_QiAM0UXAAA8v_6Frio548.png" alt="png" /></p>
<p>单体服务下的认证和授权整合示意图</p>
<p>结合示意图我们可以看到,通过请求传递用户凭证完成用户认证,然后根据该用户信息中具备的角色信息获取访问权限,并最终完成对 HTTP 端点的访问授权。</p>
<p>围绕认证和授权,我们还需要一系列的额外功能确保整个流程得以实现。这些功能包括用于密码保护的加解密机制、用于实现方法级的安全访问,以及支持跨域等,这些功能在我们专栏的后续内容中都会一一展开讨论。</p>
<h4>Spring Security 与微服务架构</h4>
<p>微服务架构的情况要比单体应用复杂很多,因为涉及了服务与服务之间的<strong>调用关系</strong>。我们继续沿用“资源”这个概念,对应到微服务系统中,服务提供者充当的角色就是资源的服务器,而服务消费者就是客户端。所以各个服务本身既可以是客户端,也可以作为资源服务器,或者两者兼之。</p>
<p>接下来,我们把认证和授权结合起来,梳理出微服务访问场景下的安全性实现方案,如下图所示:</p>
<p><img src="assets/CioPOWC5_RGAOITBAACOS5oiq6U626.png" alt="Drawing 8.png" /></p>
<p><img src="assets/CioPOWC5_RGAOITBAACOS5oiq6U626.png" alt="png" /></p>
<p>微服务架构下的认证和授权整合示意图</p>
<p>可以看到,与单体应用相比,在微服务架构中需要把认证和授权的过程进行集中化管理,所以在上图中出现了一个<strong>授权中心。</strong> 授权中心会获取客户端请求中所带有的身份凭证信息,然后基于凭证信息生成一个 Token这个 Token 中就包含了<strong>权限范围和有效期</strong></p>
<p>客户端获取 Token 之后就可以基于这个 Token 发起对微服务的访问。这个时候,服务的提供者需要对这个 Token 进行认证,并根据 Token 的权限范围和有效期从授权中心获取该请求能够访问的特定资源。在微服务系统中,对外的资源表现形式同样可以理解为一个个 HTTP 端点。</p>
<p>上图中关键点就在于构建用于生成和验证 Token 的授权中心,为此我们需要引入<strong>OAuth2 协议</strong>。OAuth2 协议为我们在客户端程序和资源服务器之间设置了一个<strong>授权层</strong>,并确保 Token 能够在各个微服务中进行有效传递,如下图所示:</p>
<p><img src="assets/CioPOWC5_R6AGH-TAAB7aKF2qcE086.png" alt="Drawing 10.png" /></p>
<p><img src="assets/CioPOWC5_R6AGH-TAAB7aKF2qcE086.png" alt="png" /></p>
<p>OAuth2 协议在服务访问场景中的应用</p>
<p>OAuth2 是一个相对复杂的协议综合应用摘要认证、签名认证、HTTPS 等安全性手段,需要提供 Token 生成和校验以及公私钥管理等功能,同时需要开发者入驻并进行权限粒度控制。一般我们应该避免自行实现这类复杂的协议,倾向于借助于特定工具以免重复造轮子。而 Spring Security 为我们提供了实现这一协议的完整解决方案,我们可以使用该框架完成适用于微服务系统中的认证授权机制。</p>
<h4>Spring Security 与响应式系统</h4>
@@ -218,7 +218,7 @@ public class DemoController {
<h3>小结与预告</h3>
<p>本讲是整个专栏内容的第一讲,我们通过一个简单的示例引入了 Spring Security 框架,并基于日常开发的安全需求,全面剖析了 Spring Security 框架具备的功能体系。不同的功能对应不同的应用场景,在普通的单体应用、微服务架构、响应式系统中都可以使用 Spring Security 框架提供的各种功能确保系统的安全性。</p>
<p>本讲内容总结如下:</p>
<p><img src="assets/CioPOWC5_SiAd8m0AABqq0x1Qls852.png" alt="Drawing 12.png" /></p>
<p><img src="assets/CioPOWC5_SiAd8m0AABqq0x1Qls852.png" alt="png" /></p>
<p>这里给你留一道思考题:针对单体应用和微服务架构,你能分别描述它们所需要的认证和授权机制吗?</p>
<p>接下来我们将正式进入到 Spring Security 框架各项功能的学习过程中,首先介绍的就是用户认证功能。下一讲,我们将讨论如何基于 Spring Security 对用户进行有效的认证。</p>
</div>

View File

@@ -197,7 +197,7 @@ function hide_canvas() {
</code></pre>
<p>显然,响应码 401 告诉我们没有访问该地址的权限。同时在响应中出现了一个“WWW-Authenticate”消息头其值为“Basic realm=&quot;Realm&quot;”,这里的 Realm 表示 Web 服务器中受保护资源的安全域。</p>
<p>现在,让我们来执行 HTTP 基础认证可以通过设置认证类型为“Basic Auth”并输入对应的用户名和密码来完成对 HTTP 端点的访问,设置界面如下所示:</p>
<p><img src="assets/CioPOWC5_duASv7jAACBgx8x8WU605.png" alt="Drawing 0.png" /></p>
<p><img src="assets/CioPOWC5_duASv7jAACBgx8x8WU605.png" alt="png" /></p>
<p>使用 Postman 完成 HTTP 基础认证信息的设置</p>
<p>现在查看 HTTP 请求,可以看到 Request Header 中添加了 <a href="https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Headers/Authorization?fileGuid=xxQTRXtVcqtHK6j8">Authorization</a> 标头格式为Authorization:<code> &lt;type&gt;</code> <code>&lt;credentials</code>&gt;。这里的 type 就是“Basic”而 credentials 则是这样一个字符串:</p>
<pre><code>dXNlcjo5YjE5MWMwNC1lNWMzLTQ0YzctOGE3ZS0yNWNkMjY3MmVmMzk=
@@ -218,7 +218,7 @@ function hide_canvas() {
}
</code></pre>
<p>formLogin() 方法的执行效果就是提供了一个默认的登录界面,如下所示:</p>
<p><img src="assets/CioPOWC5_fmAOso9AAAqvMlklW8869.png" alt="Drawing 1.png" /></p>
<p><img src="assets/CioPOWC5_fmAOso9AAAqvMlklW8869.png" alt="png" /></p>
<p>Spring Security 默认的登录界面</p>
<p>我们已经在上一讲中看到过这个登录界面。对于登录操作而言,这个登录界面通常都是定制化的,同时,我们也需要对登录的过程和结果进行细化控制。此时,我们就可以通过如下所示的配置内容来修改系统的默认配置:</p>
<pre><code>@Override
@@ -293,7 +293,7 @@ protected void configure(AuthenticationManagerBuilder auth) throws Exception {
<h3>小结与预告</h3>
<p>这一讲我们详细介绍了如何使用 Spring Security 构建用户认证体系的系统方法。在 Spring Security 中,认证相关的功能都是可以通过配置体系进行定制化开发和管理的。通过简单的配置方法,我们可以组合使用 HTTP 基础认证和表单登录认证,也可以分别基于内存以及基于数据库方案来存储用户信息,这些功能都是 Spring Security 内置的。</p>
<p>本讲内容总结如下:</p>
<p><img src="assets/CioPOWC5_g-AP68cAACSqF1r51g526.png" alt="Drawing 2.png" /></p>
<p><img src="assets/CioPOWC5_g-AP68cAACSqF1r51g526.png" alt="png" /></p>
<p>最后我想给你留一道思考题:你知道在 Spring Security 中有哪几种存储用户信息的实现方案吗?欢迎在留言区和我分享你的想法。</p>
</div>
</div>

View File

@@ -166,7 +166,7 @@ function hide_canvas() {
<li>UserDetailsManager扩展 UserDetailsService添加了创建用户、修改用户密码等功能。</li>
</ul>
<p>这四个对象之间的关联关系如下图所示,显然,对于由 UserDetails 对象所描述的一个用户而言,它应该具有 1 个或多个能够执行的 GrantedAuthority</p>
<p><img src="assets/CioPOWC5_ryAZ3qSAABXNOe10bU172.png" alt="Drawing 0.png" /></p>
<p><img src="assets/CioPOWC5_ryAZ3qSAABXNOe10bU172.png" alt="png" /></p>
<p>Spring Security 中的四大核心用户对象</p>
<p>结合上图,我们先来看承载用户详细信息的 UserDetails 接口,如下所示:</p>
<pre><code>public interface UserDetails extends Serializable {
@@ -284,7 +284,7 @@ public final C withUser(UserDetails userDetails) {
</code></pre>
<p>而 withUser 方法返回的是一个 UserDetailsBuilder 对象,该对象内部使用了前面介绍的 UserBuilder 对象,因此可以实现类似 .withUser(&quot;spring_user&quot;).password(&quot;password1&quot;).roles(&quot;USER&quot;) 这样的链式语法,完成用户信息的设置。这也是上一讲中,我们在介绍基于内存的用户信息存储方案时使用的方法。</p>
<p>作为总结,我们也梳理了 Spring Security 中与用户对象相关的一大批实现类,它们之间的关系如下图所示:</p>
<p><img src="assets/Cgp9HWC5_sqAC7SUAACLftd0SYc321.png" alt="Drawing 1.png" /></p>
<p><img src="assets/Cgp9HWC5_sqAC7SUAACLftd0SYc321.png" alt="png" /></p>
<p>Spring Security 中用户对象相关类结构图</p>
<h4>Spring Security 中的认证对象</h4>
<p>有了用户对象,我们就可以讨论具体的认证过程了,首先来看认证对象 Authentication如下所示</p>
@@ -353,7 +353,7 @@ public final C withUser(UserDetails userDetails) {
</code></pre>
<p>可以看到,这里使用了 AuthenticationManager 而不是 AuthenticationProvider 中的 authenticate() 方法来执行认证。同时,我们也注意到这里出现了 UsernamePasswordAuthenticationToken 类,这就是 Authentication 接口的一个具体实现类,用来<strong>存储用户认证所需的用户名和密码信息</strong></p>
<p>同样作为总结,我们也梳理了 Spring Security 中与认证对象相关的一大批核心类,它们之间的关系如下所示:</p>
<p><img src="assets/CioPOWC5_tmAEUuLAABqhhFbRIw821.png" alt="Drawing 2.png" /></p>
<p><img src="assets/CioPOWC5_tmAEUuLAABqhhFbRIw821.png" alt="png" /></p>
<p>Spring Security 中认证的对象相关类结构图</p>
<h3>实现定制化用户认证方案</h3>
<p>通过前面的分析我们明确了用户信息存储的实现过程实际上是可以定制化的。Spring Security 所做的工作只是把常见的、符合一般业务场景的实现方式嵌入到了框架中。如果有特殊的场景,开发人员完全可以实现自定义的用户信息存储方案。</p>
@@ -436,7 +436,7 @@ public class SpringUserDetailsService
<p>我们知道 UserDetailsService 接口只有一个 loadUserByUsername 方法需要实现。因此,我们基于 SpringUserRepository 的 findByUsername 方法,根据用户名从数据库中查询数据。</p>
<h4>扩展 AuthenticationProvider</h4>
<p>扩展 AuthenticationProvider 的过程就是提供一个自定义的 AuthenticationProvider 实现类。这里我们以最常见的用户名密码认证为例,梳理自定义认证过程所需要实现的步骤,如下所示:</p>
<p><img src="assets/CioPOWC5_uiAEATKAACWyEa75HA969.png" alt="Drawing 3.png" /></p>
<p><img src="assets/CioPOWC5_uiAEATKAACWyEa75HA969.png" alt="png" /></p>
<p>自定义 AuthenticationProvider 的实现流程图</p>
<p>上图中的流程并不复杂,首先我们需要通过 UserDetailsService 获取一个 UserDetails 对象,然后根据该对象中的密码与认证请求中的密码进行匹配,如果一致则认证成功,反之抛出一个 BadCredentialsException 异常。示例代码如下所示:</p>
<pre><code>@Component
@@ -492,7 +492,7 @@ public class SpringSecurityConfig extends WebSecurityConfigurerAdapter {
<h3>小结与预告</h3>
<p>这一讲我们基于 Spring Security 提供的用户认证功能分析了其背后的实现过程。我们的切入点在于分析与用户和认证相关的各个核心类,并梳理它们之间的交互过程。另一方面,我们还通过扩展 UserDetailsService 和 AuthenticationProvider 接口的方式来实现定制化的用户认证方案。</p>
<p>本讲内容总结如下:</p>
<p><img src="assets/CioPOWC5_wKAct0XAACvS5HbogQ525.png" alt="Drawing 4.png" /></p>
<p><img src="assets/CioPOWC5_wKAct0XAACvS5HbogQ525.png" alt="png" /></p>
<p>最后给你留一道思考题:基于 Spring Security如何根据用户名和密码实现一套定制化的用户认证解决方案欢迎在留言区和我分享你的想法。</p>
</div>
</div>

View File

@@ -157,7 +157,7 @@ function hide_canvas() {
<p>通过前面两讲内容的学习,相信你已经掌握了 Spring Security 中的用户认证体系。用户认证的过程通常涉及密码的校验因此密码的安全性也是我们需要考虑的一个核心问题。Spring Security 作为一款功能完备的安全性框架,一方面提供了<strong>用于完成认证操作的 PasswordEncoder 组件</strong>,另一方面也包含一个独立而完整的<strong>加密模块</strong>,方便在应用程序中单独使用。</p>
<h3>PasswordEncoder</h3>
<p>我们先来回顾一下整个用户认证流程。在 AuthenticationProvider 中,我们需要使用 PasswordEncoder 组件验证密码的正确性,如下图所示:</p>
<p><img src="assets/Cgp9HWC5_4iAIGeQAABZX7ZLFlk655.png" alt="Drawing 0.png" /></p>
<p><img src="assets/Cgp9HWC5_4iAIGeQAABZX7ZLFlk655.png" alt="png" /></p>
<p>PasswordEncoder 组件与认证流程之间的关系</p>
<p>在“用户认证:如何使用 Spring Security 构建用户认证体系?”一讲中我们也介绍了基于数据库的用户信息存储方案:</p>
<pre><code>@Override
@@ -184,7 +184,7 @@ protected void configure(AuthenticationManagerBuilder auth) throws Exception {
}
</code></pre>
<p>Spring Security 内置了一大批 PasswordEncoder 接口的实现类,如下所示:</p>
<p><img src="assets/CioPOWC5_5KAdkT3AAQNHWtae5I850.png" alt="Drawing 1.png" /></p>
<p><img src="assets/CioPOWC5_5KAdkT3AAQNHWtae5I850.png" alt="png" /></p>
<p>Spring Security 中的 PasswordEncoder 实现类</p>
<p>我们对上图中比较常见的几个 PasswordEncoder 接口展开叙述。</p>
<ul>
@@ -262,7 +262,7 @@ PasswordEncoder p = new SCryptPasswordEncoder(16384, 8, 1, 32, 64);
<p>在前面的讨论中,我们都基于一个假设,即在对密码进行加解密过程中,只会使用到一个 PasswordEncoder如果这个 PasswordEncoder 不满足我们的需求,那么就需要替换成另一个 PasswordEncoder。这就引出了一个问题如何优雅地应对这种变化呢</p>
<p>在普通的业务系统中,由于业务系统也在不断地变化,替换一个组件可能并没有很高的成本。但对于 Spring Security 这种成熟的开发框架而言,在设计和实现上不能经常发生变化。因此,在新/旧 PasswordEncoder 的兼容性以及框架自身的稳健性和可变性之间需要保持一种平衡。为了实现这种平衡性Spring Security 提供了 DelegatingPasswordEncoder。</p>
<p>虽然 DelegatingPasswordEncoder 也实现了 PasswordEncoder 接口但事实上它更多扮演了一种代理组件的角色这点从命名上也可以看出来。DelegatingPasswordEncoder 将具体编码的实现根据要求代理给不同的算法,以此实现不同编码算法之间的兼容并协调变化,如下图所示:</p>
<p><img src="assets/Cgp9HWC5_6aACid_AACGDa8mfic454.png" alt="Drawing 2.png" /></p>
<p><img src="assets/Cgp9HWC5_6aACid_AACGDa8mfic454.png" alt="png" /></p>
<p>DelegatingPasswordEncoder 的代理作用示意图</p>
<p>下面我们来看一下 DelegatingPasswordEncoder 类的构造函数,如下所示:</p>
<pre><code>public DelegatingPasswordEncoder(String idForEncode,
@@ -395,7 +395,7 @@ byte [] decrypted = e.decrypt(encrypted);
<h3>小结与预告</h3>
<p>对于一个 Web 应用程序而言一旦需要实现用户认证势必涉及用户密码等敏感信息的加密。为此Spring Security 专门提供了 PasswordEncoder 组件对密码进行加解密。Spring Security 内置了一批即插即用的 PasswordEncoder并通过代理机制完成了各个组件的版本兼容和统一管理。这种设计思想也值得我们学习和借鉴。当然作为一款通用的安全性开发框架Spring Security 也提供了一个高度独立的加密模块应对日常开发需求。</p>
<p>本讲内容总结如下:</p>
<p><img src="assets/CioPOWC5_7OAC11AAABs6h6B9l8729.png" alt="Drawing 3.png" /></p>
<p><img src="assets/CioPOWC5_7OAC11AAABs6h6B9l8729.png" alt="png" /></p>
<p>这里给你留一道思考题:你能描述 DelegatingPasswordEncoder 所起到的代理作用吗?欢迎在留言区和我分享你的思考。</p>
</div>
</div>

View File

@@ -170,10 +170,10 @@ function hide_canvas() {
<p>同样,在 02 讲中我们也已经看到过上述代码,这是 Spring Security 中作用于访问授权的默认实现方法。</p>
<h4>基于权限进行访问控制</h4>
<p>我们先来回顾一下 03 讲“账户体系:如何深入理解 Spring Security 的认证机制?”中介绍的用户对象以及它们之间的关联关系:</p>
<p><img src="assets/Cgp9HWC99kyAPov7AABo-HUo8Yw816.png" alt="Drawing 0.png" /></p>
<p><img src="assets/Cgp9HWC99kyAPov7AABo-HUo8Yw816.png" alt="png" /></p>
<p>Spring Security 中的核心用户对象</p>
<p>上图中的 GrantedAuthority 对象代表的就是一种权限对象,而一个 UserDetails 对象具备一个或多个 GrantedAuthority 对象。通过这种关联关系,实际上我们就可以对用户的权限做一些限制,如下所示:</p>
<p><img src="assets/Cgp9HWC99lqANmBNAACOAwFM9XI522.png" alt="Drawing 1.png" /></p>
<p><img src="assets/Cgp9HWC99lqANmBNAACOAwFM9XI522.png" alt="png" /></p>
<p>使用权限实现访问控制示意图</p>
<p>如果用代码来表示这种关联关系,可以采用如下所示的实现方法:</p>
<pre><code>UserDetails user = User.withUsername(&quot;jianxiang&quot;)
@@ -208,7 +208,7 @@ http.authorizeRequests().anyRequest().access(expression);
<p>上述代码的效果是只有拥有“CREATE”权限且不拥有“Retrieve”权限的用户才能进行访问。</p>
<h4>基于角色进行访问控制</h4>
<p>讨论完权限,我们再来看角色,你可以把<strong>角色看成是拥有多个权限的一种数据载体</strong>如下图所示这里我们分别定义了两个不同的角色“User”和“Admin”它们拥有不同的权限</p>
<p><img src="assets/Cgp9HWC99muADJAVAACViruybHQ086.png" alt="Drawing 2.png" /></p>
<p><img src="assets/Cgp9HWC99muADJAVAACViruybHQ086.png" alt="png" /></p>
<p>使用角色实现访问控制示意图</p>
<p>讲到这里,你可能会认为 Spring Security 应该提供了一个独立的数据结构来承载角色的含义。但事实上,在 Spring Security 中并没有定义类似“GrantedRole”这种专门用来定义用户角色的对象而是<strong>复用了 GrantedAuthority 对象</strong>。事实上以“ROLE_”为前缀的 GrantedAuthority 就代表了一种角色,因此我们可以使用如下方式初始化用户的角色:</p>
<pre><code>UserDetails user = User.withUsername(&quot;jianxiang&quot;)
@@ -395,7 +395,7 @@ UserDetails user2 = User.withUsername(&quot;jianxiang2&quot;)
<h3>小结与预告</h3>
<p>这一讲我们关注的是对请求访问进行授权,而这个过程需要明确 Spring Security 中的用户、权限和角色之间的关联关系。一旦我们对某个用户设置了对应的权限和角色那么就可以通过各种配置方法来有效控制访问权限。为此Spring Security 也提供了 MVC 匹配器、Ant 匹配器以及正则表达式匹配器来实现复杂的访问控制。</p>
<p>本讲内容总结如下:</p>
<p><img src="assets/CioPOWC99pCADnIuAAB0dOuXd6o323.png" alt="Drawing 3.png" /></p>
<p><img src="assets/CioPOWC99pCADnIuAAB0dOuXd6o323.png" alt="png" /></p>
<p>最后我想给你留一道思考题:在 Spring Security 中,你知道用户角色与用户权限之间有什么区别和联系吗?欢迎你在留言区和我分享自己的观点。</p>
</div>
</div>

View File

@@ -168,7 +168,7 @@ function hide_canvas() {
<p>SecurityMetadataSource 接口定义了一组方法来操作这些权限配置,具体权限配置的表现形式是<strong>ConfigAttribute 接口</strong>。通过 ExpressionInterceptUrlRegistry 和 AuthorizedUrl我们能够把配置信息转变为具体的 ConfigAttribute。</p>
<p>当我们获取了权限配置信息后,就可以根据这些配置决定 HTTP 请求是否具有访问权限也就是执行授权决策。Spring Security 专门提供了一个 AccessDecisionManager 接口完成该操作。而在 AccessDecisionManager 接口中,又把具体的决策过程委托给了 AccessDecisionVoter 接口。<strong>AccessDecisionVoter 可以被认为是一种投票器,负责对授权决策进行表决</strong></p>
<p>以上三个步骤构成了 Spring Security 的授权整体工作流程,可以用如下所示的时序图表示:</p>
<p><img src="assets/CioPOWC99xWAASrVAABnJyNceVM498.png" alt="Drawing 0.png" /></p>
<p><img src="assets/CioPOWC99xWAASrVAABnJyNceVM498.png" alt="png" /></p>
<p>Spring Security 的授权整体工作流程</p>
<p>接下来,我们基于这张类图分别对拦截请求、获取权限配置、执行授权决策三个步骤逐一展开讲解。</p>
<h3>拦截请求</h3>
@@ -385,12 +385,12 @@ List&lt;AccessDecisionVoter&lt;?&gt;&gt; getDecisionVoters(H http) {
</code></pre>
<p>显然,最终的评估过程只是简单使用了 Spring 所提供的 SpEL 表达式语言。</p>
<p>作为总结,我们把这一流程中涉及的核心组件以类图的形式进行了梳理,如下图所示:</p>
<p><img src="assets/Cgp9HWC99zqAGUOYAACtn7EjrO0914.png" alt="Drawing 1.png" /></p>
<p><img src="assets/Cgp9HWC99zqAGUOYAACtn7EjrO0914.png" alt="png" /></p>
<p>Spring Security 授权相关核心类图</p>
<h3>小结与预告</h3>
<p>这一讲我们关注的是 Spring Security 授权机制的实现原理,我们把整个授权过程拆分成<strong>拦截请求、获取访问策略和执行授权决策</strong>这三大步骤。针对每一个步骤,都涉及了一组核心类及其它们之间的交互关系。针对这些核心类的讲解思路是围绕着上一讲介绍的基本配置方法展开讨论的,确保实际应用能与源码分析衔接在一起。</p>
<p>本讲内容总结如下:</p>
<p><img src="assets/CioPOWC990GAfLaXAACI8-bD1xs493.png" alt="Drawing 2.png" /></p>
<p><img src="assets/CioPOWC990GAfLaXAACI8-bD1xs493.png" alt="png" /></p>
<p>最后给你留一道思考题:在 Spring Security 中,你能简要描述整个授权机制的执行过程吗?</p>
</div>
</div>

View File

@@ -161,7 +161,7 @@ function hide_canvas() {
<p>这个 Web 应用程序将采用经典的三层架构,即<strong>Web 层、服务层和数据访问层</strong>,因此我们会存在 HealthRecordController、HealthRecordService 以及 HealthRecordRepository这是一条独立的代码流程用来完成<strong>系统业务逻辑处理</strong></p>
<p>另一方面,本案例的核心功能是<strong>实现自定义的用户认证流程</strong>,所以我们需要构建独立的 UserDetailsService 以及 AuthenticationProvider这是另一条独立的代码流程。而在这条代码流程中势必还需要 User 以及 UserRepository 等组件。</p>
<p>我们可以把这两条代码线整合在一起,得到案例的整体设计蓝图,如下图所示:</p>
<p><img src="assets/CioPOWDJyDOAKosUAACCwzt5aFc611.png" alt="Drawing 0.png" /></p>
<p><img src="assets/CioPOWDJyDOAKosUAACCwzt5aFc611.png" alt="png" /></p>
<p>案例中的业务代码流程和用户认证流程</p>
<h4>系统初始化</h4>
<p>要想实现上图中的效果,我们需要先对系统进行初始化。这部分工作涉及<strong>领域对象的定义、数据库初始化脚本的整理以及相关依赖组件的引入</strong></p>
@@ -489,16 +489,16 @@ public class HealthRecordController {
<p>这里我们从 Model 对象中获取了认证用户信息以及健康档案信息,并渲染在页面上。</p>
<h3>案例演示</h3>
<p>现在,让我们启动 Spring Boot 应用程序,并访问<a href="http://localhost:8080/?fileGuid=xxQTRXtVcqtHK6j8">http://localhost:8080</a>端点。因为访问系统的任何端点都需要认证,所以 Spring Security 会自动跳转到如下所示的登录界面:</p>
<p><img src="assets/CioPOWDJyFCAWC1SAAA5aEhV0-A820.png" alt="Drawing 1.png" /></p>
<p><img src="assets/CioPOWDJyFCAWC1SAAA5aEhV0-A820.png" alt="png" /></p>
<p>用户登录界面</p>
<p>我们分别输入用户名“jianxiang”和密码“12345”系统就会跳转到健康档案主页</p>
<p><img src="assets/CioPOWDJyFeAUaWEAAB7TB8wdKw208.png" alt="Drawing 2.png" /></p>
<p><img src="assets/CioPOWDJyFeAUaWEAAB7TB8wdKw208.png" alt="png" /></p>
<p>健康档案主页</p>
<p>在这个主页中,我们正确获取了登录用户的用户名,并展示了个人健康档案信息。这个结果也证实了自定义用户认证体系的正确性。你可以根据示例代码做一些尝试。</p>
<h3>小结与预告</h3>
<p>这一讲我们动手实践了“利用 Spring Security 基础功能保护 Web 应用程序”。综合第 2 讲到 6 讲中的核心知识点,我们设计了一个简单而又完整的案例,并通过构建用户管理和认证流程讲解了实现自定义用户认证机制的过程。</p>
<p>本讲内容总结如下:</p>
<p><img src="assets/Cgp9HWDJyF6AAUtVAACuoKgyiho485.png" alt="Drawing 3.png" /></p>
<p><img src="assets/Cgp9HWDJyF6AAUtVAACuoKgyiho485.png" alt="png" /></p>
<p>最后给你留一道思考题:在 Spring Security 中,实现一套自定义的用户认证体系需要哪些开发步骤?</p>
</div>
</div>

View File

@@ -159,7 +159,7 @@ function hide_canvas() {
<p>过滤器是一种通用机制,在处理 Web 请求的过程中发挥了重要作用。可以说,目前市面上所有的 Web 开发框架都或多或少使用了过滤器完成对请求的处理Spring Security 也不例外。Spring Security 中的过滤器架构是<strong>基于 Servlet</strong>构建的,所以我们先从 Servlet 中的过滤器开始说起。</p>
<h4>Servlet 与管道-过滤器模式</h4>
<p>和业界大多数处理 Web 请求的框架一样Servlet 中采用的最基本的架构就是管道-过滤器Pipe-Filter架构模式。管道-过滤器架构模式的示意图如下所示:</p>
<p><img src="assets/Cgp9HWDJ2q6ADHioAABYcUjpFY4513.png" alt="Drawing 0.png" /></p>
<p><img src="assets/Cgp9HWDJ2q6ADHioAABYcUjpFY4513.png" alt="png" /></p>
<p>管道-过滤器架构模式示意图</p>
<p>结合上图我们可以看到,处理业务逻辑的组件被称为过滤器,而处理结果通过相邻过滤器之间的管道进行传输,这样就构成了一个过滤器链。</p>
<p>在 Servlet 中,代表过滤器的 Filter 接口定义如下:</p>
@@ -181,7 +181,7 @@ function hide_canvas() {
<p>请注意,<strong>过滤器链中的过滤器是有顺序的</strong>,这点非常重要,我们在本讲后续内容中会针对这点展开讲解。</p>
<h4>Spring Security 中的过滤器链</h4>
<p>在 Spring Security 中,其核心流程的执行也是依赖于一组过滤器,这些过滤器在框架启动后会自动进行初始化,如图所示:</p>
<p><img src="assets/Cgp9HWDJ2rmAfrXzAABUC_CslH8843.png" alt="Drawing 1.png" /></p>
<p><img src="assets/Cgp9HWDJ2rmAfrXzAABUC_CslH8843.png" alt="png" /></p>
<p>Spring Security 中的过滤器链示意图</p>
<p>在上图中,我们看到了几个常见的 Filter比如 BasicAuthenticationFilter、UsernamePasswordAuthenticationFilter 等,这些类都直接或间接实现了 Servlet 中的 Filter 接口,并完成某一项具体的认证机制。例如,上图中的 BasicAuthenticationFilter 用来验证用户的身份凭证;而 UsernamePasswordAuthenticationFilter 会检查输入的用户名和密码,并根据认证结果决定是否将这一结果传递给下一个过滤器。</p>
<p>请注意,<strong>整个 Spring Security 过滤器链的末端是一个 FilterSecurityInterceptor它本质上也是一个 Filter</strong>。但与其他用于完成认证操作的 Filter 不同,它的核心功能是<strong>实现权限控制</strong>,也就是用来判定该请求是否能够访问目标 HTTP 端点。FilterSecurityInterceptor 对于权限控制的粒度可以到方法级别,能够满足前面提到的精细化访问控制。我们在 06 讲“权限管理:如何剖析 Spring Security 的授权原理?”中已经对这个拦截器做了详细的介绍,这里就不再展开了。</p>
@@ -210,7 +210,7 @@ function hide_canvas() {
</code></pre>
<p>这里我们定义了一个 LoggingFilter用来记录已经通过用户认证的请求中包含的一个特定的消息头“UniqueRequestId”通过这个唯一的请求 Id我们可以对请求进行跟踪、监控和分析。在实现一个自定义的过滤器组件时我们通常会从 ServletRequest 中获取请求数据,并在 ServletResponse 中设置响应数据,然后通过 filterChain 的 doFilter() 方法将请求继续在过滤器链上进行传递。</p>
<p>接下来,我们想象这样一种场景,业务上我们需要根据客户端请求头中是否包含某一个特定的标志位,来决定请求是否有效。如图所示:</p>
<p><img src="assets/CioPOWDJ2seAeDCHAABb0EbFFhw357.png" alt="Drawing 2.png" /></p>
<p><img src="assets/CioPOWDJ2seAeDCHAABb0EbFFhw357.png" alt="png" /></p>
<p>根据标志位设计过滤器示意图</p>
<p>这在现实开发过程中也是一种常见的应用场景,可以实现定制化的安全性控制。针对这种应用场景,我们可以实现如下所示的 RequestValidationFilter 过滤器:</p>
<pre><code>public class RequestValidationFilter implements Filter {
@@ -234,11 +234,11 @@ function hide_canvas() {
<p>现在,我们已经实现了几个有价值的过滤器了,下一步就是将这些过滤器整合到 Spring Security 的整个过滤器链中。这里,我想特别强调一点,和 Servlet 中的过滤器一样,<strong>Spring Security 中的过滤器也是有顺序的</strong>。也就是说,将过滤器放置在过滤器链的具体位置需要符合每个过滤器本身的功能特性,不能将这些过滤器随意排列组合。</p>
<p>我们来举例说明合理设置过滤器顺序的重要性。在[“用户认证:如何使用 Spring Security 构建用户认证体系?”]一讲中我们提到了 HTTP 基础认证机制,而在 Spring Security 中,实现这一认证机制的就是 BasicAuthenticationFilter。</p>
<p>如果我们想要实现定制化的安全性控制策略,就可以实现类似前面介绍的 RequestValidationFilter 这样的过滤器,并放置在 BasicAuthenticationFilter 前。这样,在执行用户认证之前,我们就可以排除掉一批无效请求,效果如下所示:</p>
<p><img src="assets/CioPOWDJ2tWAOEqvAABaXzH63gI541.png" alt="Drawing 3.png" /></p>
<p><img src="assets/CioPOWDJ2tWAOEqvAABaXzH63gI541.png" alt="png" /></p>
<p>RequestValidationFilter 的位置示意图</p>
<p>上图中的 RequestValidationFilter 确保那些没有携带有效请求头信息的请求不会执行不必要的用户认证。基于这种场景,把 RequestValidationFilter 放在 BasicAuthenticationFilter 之后就不是很合适了,因为用户已经完成了认证操作。</p>
<p>同样,针对前面已经构建的 LoggingFilter原则上我们可以把它放在过滤器链的任何位置因为它只记录了日志。但有没有更合适的位置呢结合 RequestValidationFilter 来看,同样对于一个无效的请求而言,记录日志是没有什么意义的。所以 LoggingFilter 应该放置在 RequestValidationFilter 之后。另一方面,对于日志操作而言,通常只需要记录那些已经通过认证的请求,所以也推荐将 LoggingFilter 放在 BasicAuthenticationFilter 之后。最终,这三个过滤器之间的关系如下图所示:</p>
<p><img src="assets/CioPOWDJ2t-AMX8kAAAyTVuPA_s000.png" alt="Drawing 4.png" /></p>
<p><img src="assets/CioPOWDJ2t-AMX8kAAAyTVuPA_s000.png" alt="png" /></p>
<p>三个过滤器的位置示意图</p>
<p>在 Spring Security 中,提供了一组可以往过滤器链中添加过滤器的工具方法,包括 addFilterBefore()、addFilterAfter()、addFilterAt() 以及 addFilter() 等,它们都定义在 HttpSecurity 类中。这些方法的含义都很明确,使用起来也很简单,例如,想要实现如上图所示的效果,我们可以编写这样的代码:</p>
<pre><code>@Override
@@ -257,7 +257,7 @@ protected void configure(HttpSecurity http) throws Exception {
<p>这里,我们使用了 addFilterBefore() 和 addFilterAfter() 方法在 BasicAuthenticationFilter 之前和之后分别添加了 RequestValidationFilter 和 LoggingFilter。</p>
<h3>Spring Security 中的过滤器</h3>
<p>下表列举了 Spring Security 中常用的过滤器名称、功能以及它们的顺序关系:</p>
<p><img src="assets/CioPOWDP_VCAc51eAADyJ6yBpaY941.png" alt="image.png" /></p>
<p><img src="assets/CioPOWDP_VCAc51eAADyJ6yBpaY941.png" alt="png" /></p>
<p>Spring Security 中的常见过滤器一览表</p>
<p>这里以最基础的 UsernamePasswordAuthenticationFilter 为例,该类的定义及核心方法 attemptAuthentication 如下所示:</p>
<pre><code>public class UsernamePasswordAuthenticationFilter extends
@@ -294,7 +294,7 @@ protected void configure(HttpSecurity http) throws Exception {
}
</code></pre>
<p>围绕上述方法,我们结合前面已经介绍的认证和授权相关实现原理,可以引出该框架中一系列核心类并梳理它们之间的交互结构,如下图所示:</p>
<p><img src="assets/CioPOWDP_VuAIAgyAAFOMaUuRl4162.png" alt="image" /></p>
<p><img src="assets/CioPOWDP_VuAIAgyAAFOMaUuRl4162.png" alt="png" /></p>
<p>UsernamePasswordAuthenticationFilter 相关核心类图</p>
<p>上图中的很多类,我们通过名称就能明白它的含义和作用。以位于左下角的 SecurityContextHolder 为例,它是一个典型的 Holder 类,存储了应用的安全上下文对象 SecurityContext而这个上下文对象中就包含了用户的认证信息。</p>
<p>我们也可以大胆猜想,它的内部应该使用 ThreadLocal 确保线程访问的安全性。更具体的,我们已经在“权限管理:如何剖析 Spring Security 的授权原理?”中讲解过 SecurityContext 的使用方法。</p>
@@ -302,7 +302,7 @@ protected void configure(HttpSecurity http) throws Exception {
<h3>小结与预告</h3>
<p>这一讲我们关注于 Spring Security 中的一个核心组件——过滤器。在请求-响应式处理框架中,过滤器发挥着重要的作用,它用来实现对请求的拦截,并定义认证和授权逻辑。同时,我们也可以根据需要实现各种自定义的过滤器组件,从而实现对 Spring Security 的动态扩展。本讲对 Spring Security 中的过滤器架构和开发方式都做了详细的介绍,你可以反复学习。</p>
<p>本讲内容总结如下:</p>
<p><img src="assets/CioPOWDJ2viAN_UiAAB6xt7iJjo286.png" alt="Drawing 6.png" /></p>
<p><img src="assets/CioPOWDJ2viAN_UiAAB6xt7iJjo286.png" alt="png" /></p>
<p>最后,给你留一道思考题:在 Spring Security 中,你能简单描述使用过滤器实现用户认证的操作过程吗?欢迎你在留言区和我分享自己的观点。</p>
</div>
</div>

View File

@@ -159,7 +159,7 @@ function hide_canvas() {
<p>我们先来看 CSRF。CSRF 的全称是 Cross-Site Request Forgery翻译成中文就是<strong>跨站请求伪造</strong>。那么,究竟什么是跨站请求伪造,面对这个问题我们又该如何应对呢?请继续往下看。</p>
<h4>什么是 CSRF</h4>
<p>从安全的角度来讲,你可以将 CSRF 理解为一种攻击手段,即攻击者盗用了你的身份,然后以你的名义向第三方网站发送恶意请求。我们可以使用如下所示的流程图来描述 CSRF</p>
<p><img src="assets/CioPOWDJ3xCAWERSAACPjO6QDvw503.png" alt="Drawing 0.png" /></p>
<p><img src="assets/CioPOWDJ3xCAWERSAACPjO6QDvw503.png" alt="png" /></p>
<p>CSRF 运行流程图</p>
<p>具体流程如下:</p>
<ul>
@@ -340,7 +340,7 @@ protected void configure(HttpSecurity http) throws Exception {
</code></pre>
<hr />
<p>作为总结,我们可以用如下所示的示意图来梳理整个定制化 CSRF 所包含的各个组件以及它们之间的关联关系:</p>
<p><img src="assets/Cgp9HWDJ3yyAFoiLAABaQZ0qSQE415.png" alt="Drawing 1.png" /></p>
<p><img src="assets/Cgp9HWDJ3yyAFoiLAABaQZ0qSQE415.png" alt="png" /></p>
<p>定制化 CSRF 的相关组件示意图</p>
<h3>使用 Spring Security 实现 CORS</h3>
<p>介绍完 CSRF我们继续来看 Web 应用程序开发过程中另一个常见的需求——CORS即跨域资源共享Cross-Origin Resource Sharing。那么问题来了什么叫跨域</p>
@@ -415,7 +415,7 @@ public class TestController {
<p>这一讲关注的是对 Web 请求安全性的讨论,我们讨论了日常开发过程中常见的两个概念,即 CSRF 和 CORS。这两个概念有时候容易混淆但应对的是完全不同的两种场景。</p>
<p>CSRF 是一种攻击行为,所以我们需要对系统进行保护,而 CORS 更多的是一种前后端开发模式上的约定。在 Spring Security 中,针对这两个场景都提供了对应的过滤器,我们只需要通过简单的配置方法就能在系统中自动集成想要的功能。</p>
<p>本讲主要内容如下:</p>
<p><img src="assets/CioPOWDJ3zuAS8aXAADIA10IfrQ469.png" alt="Drawing 2.png" /></p>
<p><img src="assets/CioPOWDJ3zuAS8aXAADIA10IfrQ469.png" alt="png" /></p>
<p>最后我想给你留一道思考题:在 Spring Security 中,如何定制化一套对 CsrfToken 的处理机制?欢迎你在留言区和我分享观点。</p>
</div>
</div>

View File

@@ -171,7 +171,7 @@ public class SecurityConfig
<p>针对方法级别授权Spring Security 提供了 @PreAuthorize 和 @PostAuthorize 这两个注解,分别用于预授权和后授权。</p>
<h4>@PreAuthorize 注解</h4>
<p>先来看 @PreAuthorize 注解的使用场景。假设在一个基于 Spring Boot 的 Web 应用程序中,存在一个 Web 层组件 OrderController该 Controller 会调用 Service 层的组件 OrderService。我们希望对访问 OrderService 层中方法的请求添加权限控制能力即只有具备“DELETE”权限的请求才能执行 OrderService 中的 deleteOrder() 方法,而没有该权限的请求将直接抛出一个异常,如下图所示:</p>
<p><img src="assets/CioPOWDUPfSAe8iSAABgIKGs6vA100.png" alt="Drawing 0.png" /></p>
<p><img src="assets/CioPOWDUPfSAe8iSAABgIKGs6vA100.png" alt="png" /></p>
<p>Service 层组件预授权示意图</p>
<p>显然,上述流程针对的是预授权的应用场景,因此我们可以使用 @PreAuthorize 注解,</p>
<p>该注解定义如下:</p>
@@ -238,7 +238,7 @@ public List&lt;Order&gt; getOrderByUser(String user) {
<p>这里我们将输入的“user”参数与通过 SpEL 表达式从安全上下文中获取的“authentication.principal.username”进行比对如果相同就执行正确的方法逻辑反之将直接抛出异常。</p>
<h4>@PostAuthorize 注解</h4>
<p>相较 @PreAuthorize 注解,@PostAuthorize 注解的应用场景可能少见一些。有时我们<strong>允许调用者正确调用方法,但希望该调用者不接受返回的响应结果</strong>。这听起来似乎有点奇怪,但在那些访问第三方外部系统的应用中,我们并不能完全相信返回数据的正确性,也有对调用的响应结果进行限制的需求,@PostAuthorize 注解为我们实现这类需求提供了很好的解决方案,如下所示:</p>
<p><img src="assets/Cgp9HWDUPgKAEHdqAABip-uyyzg495.png" alt="Drawing 1.png" /></p>
<p><img src="assets/Cgp9HWDUPgKAEHdqAABip-uyyzg495.png" alt="png" /></p>
<p>Service 层组件后授权示意图</p>
<p>为了演示 @PostAuthorize 注解,我们先来设定特定的返回值。假设我们存在如下所示的一个 Author 对象,保存着该作者的姓名和创作的图书作品:</p>
<pre><code>public class Author {
@@ -304,7 +304,7 @@ public List&lt;Product&gt; findProducts() {
<h3>小结与预告</h3>
<p>这一讲我们关注的重点从 HTTP 端点级别的安全控制转换到了普通方法级别的安全控制。Spring Security 内置了一组非常实用的注解,方便开发人员实现全局方法安全机制,包括用于实现方法级别授权的 @PreAuthorize 和 @PostAuthorize 注解,以及用于实现方法级别过滤的 @PreFilter 注解和 @PostFilter 注解。我们针对这些注解的使用方法也给出了相应的描述和示例代码。</p>
<p>本讲内容总结如下:</p>
<p><img src="assets/Cgp9HWDUQa2Ac-ruAAKPFQeQ-YU960.png" alt="Drawing 2.png" /></p>
<p><img src="assets/Cgp9HWDUQa2Ac-ruAAKPFQeQ-YU960.png" alt="png" /></p>
<p>这里给你留一道思考题:针对 Spring Security 提供的全局方法安全机制,你能描述方法级别授权和方法级别过滤的区别以及它们各自的应用场景吗?欢迎在留言区写下你的想法。</p>
</div>
</div>

View File

@@ -160,7 +160,7 @@ function hide_canvas() {
<h4>多因素认证设计</h4>
<p>多因素认证是一种安全访问控制的方法,基本的设计理念在于<strong>用户想要访问最终的资源,至少需要通过两种以上的认证机制</strong></p>
<p>那么我们如何实现多种认证机制呢一种常见的做法是分成两个步骤第一步通过用户名和密码获取一个认证码Authentication Code第二步基于用户名和这个认证码进行安全访问。基于这种多因素认证的基本执行流程如下图所示</p>
<p><img src="assets/Cgp9HWDcHxyASKleAAC5AmJ3Hwc285.png" alt="Drawing 0.png" /></p>
<p><img src="assets/Cgp9HWDcHxyASKleAAC5AmJ3Hwc285.png" alt="png" /></p>
<p>多因素认证的实现方式示意图</p>
<h4>系统初始化</h4>
<p>为了实现多因素认证,我们需要构建一个独立的认证服务 Auth-Service该服务<strong>同时提供了基于用户名+密码以及用户名+认证码的认证形式</strong>。当然,实现认证的前提是构建用户体系,因此我们需要提供如下所示的 User 实体类:</p>
@@ -201,7 +201,7 @@ CREATE TABLE IF NOT EXISTS `spring_security_demo`.`auth_code` (
PRIMARY KEY (`id`));
</code></pre>
<p>有了认证服务,接下来我们需要构建一个业务服务 Business-Service该业务服务通过集成认证服务完成具体的认证操作并返回访问令牌Token给到客户端系统。因此从依赖关系上讲Business-Service 会调用 Auth-Service如下图所示</p>
<p><img src="assets/Cgp9HWDcHyuAT3auAACddvz56Cc067.png" alt="Drawing 2.png" /></p>
<p><img src="assets/Cgp9HWDcHyuAT3auAACddvz56Cc067.png" alt="png" /></p>
<p>Business-Service 调用 Auth-Service 关系图</p>
<p>接下来,我们分别从这两个服务入手,实现多因素认证机制。</p>
<h3>实现多因素认证机制</h3>
@@ -431,7 +431,7 @@ public class CustomAuthenticationFilter extends OncePerRequestFilter {
</code></pre>
<p>CustomAuthenticationFilter 的实现过程比较简单,代码也都是自解释的,唯一需要注意的是<strong>在基于认证码的认证过程通过之后我们会在响应中添加一个“Authorization”消息头并使用 UUID 值作为 Token 进行返回</strong></p>
<p>针对上述代码,我们可以通过如下所示的类图进行总结:</p>
<p><img src="assets/CioPOWDcH0SAWPjtAAB7UfIfAyk803.png" alt="Drawing 4.png" /></p>
<p><img src="assets/CioPOWDcH0SAWPjtAAB7UfIfAyk803.png" alt="png" /></p>
<p>多因素认证执行核心类图</p>
<p>最后,我们需要通过 Spring Security 中的配置体系确保各个类之间的有效协作。为此,我们构建了如下所示的 SecurityConfig 类:</p>
<pre><code>@Configuration
@@ -475,17 +475,17 @@ public class SecurityConfig extends WebSecurityConfigurerAdapter {
<p>关于案例的完整代码你可以在这里进行下载:<a href="https://github.com/lagouEdAnna/SpringSecurity-jianxiang/tree/main/MultiFactorAuthenticationDemo">https://github.com/lagouEdAnna/SpringSecurity-jianxiang/tree/main/MultiFactorAuthenticationDemo</a></p>
<h3>案例演示</h3>
<p>现在,让我们分别在本地启动认证服务和业务服务,请注意:认证服务的启动端口是 8080而业务服务的启动端口是 9090。然后我们打开模拟 HTTP 请求的 Postman 并输入相关参数,如下所示:</p>
<p><img src="assets/CioPOWDcH1CAWsVIAACxvLoOLdE765.png" alt="Drawing 5.png" /></p>
<p><img src="assets/CioPOWDcH1CAWsVIAACxvLoOLdE765.png" alt="png" /></p>
<p>多因素认证的第一步认证示意图:基于用户名+密码</p>
<p>显然,该请求只传入了用户名和密码,所以会基于 UsernamePasswordAuthenticationProvider 执行认证过程从而为用户“jianxiang”生成认证码。<strong>认证码是动态生成的,所以每次请求对应的结果都是不一样的</strong>我通过查询数据库获取该认证码为“9750”你也可以自己做一些尝试。</p>
<p>有了认证码,相当于完成了多因素认证机制的第一步。接下来,我们再次基于这个认证码构建请求并获取响应结果,如下所示:</p>
<p><img src="assets/Cgp9HWDcH1mACUXeAADWBUPX0po513.png" alt="Drawing 7.png" /></p>
<p><img src="assets/Cgp9HWDcH1mACUXeAADWBUPX0po513.png" alt="png" /></p>
<p>多因素认证的第二步认证示意图:基于用户名+认证码</p>
<p>可以看到,通过传入正确的认证码,我们基于 AuthCodeAuthenticationProvider 完成了多因素认证机制中的第二步认证,并最终在 HTTP 响应中生成了一个“Authorization”消息头。</p>
<h3>小结与预告</h3>
<p>这一讲我们基于多因素认证机制展示了如何利用 Spring Security 中的一些高级主题保护 Web 应用程序的实现方法。多因素认证机制的实现需要构建多个自定义的 AuthenticationProvider并通过拦截器完成对请求的统一处理。相信案例中展示的这些开发技巧会给你的日常开发工作带来帮助。</p>
<p>本讲内容总结如下:</p>
<p><img src="assets/Cgp9HWDcH2KAfoU3AAHK941R6Dk155.png" alt="Drawing 9.png" /></p>
<p><img src="assets/Cgp9HWDcH2KAfoU3AAHK941R6Dk155.png" alt="png" /></p>
<p>这里给你留一道思考题:在 Spring Security 中,如何利用过滤器实现对用户请求的定制化认证?</p>
</div>
</div>

View File

@@ -160,7 +160,7 @@ function hide_canvas() {
<h4>OAuth2 协议的应用场景</h4>
<p>在常见的电商系统中,通常会存在类似工单处理的系统,而工单的生成在使用用户基本信息的同时,势必也依赖于用户的订单记录等数据。为了降低开发成本,假设我们的整个商品订单模块并不是自己研发的,而是集成了外部的订单管理平台,此时为了生成工单记录,就必须让工单系统读取用户在订单管理平台上的订单记录。</p>
<p>在这个场景中,难点在于<strong>只有得到用户的授权</strong>,才能同意工单系统读取用户在订单管理平台上的订单记录。那么问题就来了,工单系统如何获得用户的授权呢?一般我们想到的方法是用户将自己在订单管理平台上的用户名和密码告诉工单系统,然后工单系统通过用户名和密码登录到订单管理平台并读取用户的订单记录,整个过程如下图所示:</p>
<p><img src="assets/Cgp9HWDewCGALXXGAABc1HLmoQs176.png" alt="Drawing 0.png" /></p>
<p><img src="assets/Cgp9HWDewCGALXXGAABc1HLmoQs176.png" alt="png" /></p>
<p>案例系统中用户认证和授权交互示意图</p>
<p>上图中的方案虽然可行,但显然存在几个严重的缺点:</p>
<ul>
@@ -170,11 +170,11 @@ function hide_canvas() {
</ul>
<p>既然这个方案存在如此多的问题,那么有没有更好的办法呢?答案是肯定的,<strong>OAuth2 协议</strong>的诞生就是为了解决这些问题。</p>
<p>首先,针对密码的安全性,在 OAuth2 协议中密码还是由用户自己保管避免了敏感信息的泄露其次OAuth2 协议中提供的授权具有明确的应用范围和有效期,用户可以根据需要限制工单系统所获取授权信息的作用效果;最后,如果用户对自己的密码等身份凭证信息进行了修改,只需通过 OAuth2 协议重新进行一次授权即可,不会影响到相关联的其他第三方应用程序。</p>
<p><img src="assets/CioPOWDewCqACykjAABqSG9fZc8154.png" alt="Drawing 1.png" /></p>
<p><img src="assets/CioPOWDewCqACykjAABqSG9fZc8154.png" alt="png" /></p>
<p>传统认证授权机制与 OAuth2 协议的对比图</p>
<h4>OAuth2 协议的角色</h4>
<p>OAuth2 协议之所有能够具备这些优势一个主要的原因在于它把整个系统涉及的各个角色及其职责做了很好地划分。OAuth2 协议中定义了四个核心的角色:<strong>资源、客户端、授权服务器和资源服务器</strong></p>
<p><img src="assets/CioPOWDewDCAIiUOAAB24Bx2cBU249.png" alt="Drawing 2.png" /></p>
<p><img src="assets/CioPOWDewDCAIiUOAAB24Bx2cBU249.png" alt="png" /></p>
<p>OAuth2 协议中的角色定义</p>
<p>我们可以把 OAuth2 中的角色与现实中的应用场景对应起来。</p>
<ul>
@@ -201,7 +201,7 @@ function hide_canvas() {
<li>scope指定了可访问的权限范围这里指定的是访问 Web 资源的“webclient”。</li>
</ul>
<p>现在我们已经介绍完令牌,你可能会好奇这样一个令牌究竟有什么用?接下来,我们就来看如何使用令牌完成基于 OAuth2 协议的授权工作流程。整个流程如下图所示:
<img src="assets/CioPOWDewDyAFnrhAABs_ltusFE367.png" alt="Drawing 3.png" /></p>
<img src="assets/CioPOWDewDyAFnrhAABs_ltusFE367.png" alt="png" /></p>
<p>基于 OAuth2 协议的授权工作流程图</p>
<p>我们可以把上述流程进一步展开梳理。</p>
<ul>
@@ -213,26 +213,26 @@ function hide_canvas() {
<h4>OAuth2 协议的授权模式</h4>
<p>在整个工作流程中,最为关键的是第二步,即获取用户的有效授权。那么如何获取用户授权呢?在 OAuth 2.0 中,定义了四种授权方式,即<strong>授权码模式Authorization Code、简化模式Implicit、密码模式Password Credentials和客户端模式Client Credentials</strong></p>
<p>我们先来看最具代表性的授权码模式。<strong>当用户同意授权后,授权服务器返回的只是一个授权码,而不是最终的访问令牌</strong>。在这种授权模式下,需要客户端携带授权码去换令牌,这就需要客户端自身具备与授权服务器进行直接交互的后台服务。</p>
<p><img src="assets/CioPOWDewEWARSNPAACJjFnruAQ327.png" alt="Drawing 4.png" /></p>
<p><img src="assets/CioPOWDewEWARSNPAACJjFnruAQ327.png" alt="png" /></p>
<p>授权码模式工作流程图</p>
<p>我们简单梳理一下授权码模式下的执行流程。</p>
<p>首先,用户在访问客户端时会被客户端导向授权服务器,此时用户可以选择是否给予客户端授权。一旦用户同意授权,授权服务器会调用客户端的后台服务提供的一个回调地址,并在调用过程中将一个授权码返回给客户端。客户端收到授权码后进一步向授权服务器申请令牌。最后,授权服务器核对授权码并向客户端发送访问令牌。</p>
<p>这里要注意的是,<strong>通过授权码向授权服务器申请令牌的过程是系统自动完成的,不需要用户的参与,用户需要做的就是在流程启动阶段同意授权</strong></p>
<p>接下来,我们再来看另一种比较常用的密码模式,其授权流程如下图所示:</p>
<p><img src="assets/Cgp9HWDewE2Ab2vhAABPDH7Zxjo001.png" alt="Drawing 5.png" /></p>
<p><img src="assets/Cgp9HWDewE2Ab2vhAABPDH7Zxjo001.png" alt="png" /></p>
<p>密码模式工作流程图</p>
<p>可以看到,密码模式比较简单,也更加容易理解。用户要做的就是提供自己的用户名和密码,然后客户端会基于这些用户信息向授权服务器请求令牌。授权服务器成功执行用户认证操作后将会发放令牌。</p>
<p>OAuth2 中的客户端模式和简化模式因为在日常开发过程中应用得不是很多,这里就不详细介绍了。</p>
<p>你可能注意到了,虽然 OAuth2 协议解决的是授权问题,但它也应用到了认证的概念,这是因为只有验证了用户的身份凭证,我们才能完成对他的授权。所以说,<strong>OAuth2 实际上是一款技术体系比较复杂的协议,综合应用了信息摘要、签名认证等安全性手段,并需要提供令牌以及背后的公私钥管理等功能</strong></p>
<h3>OAuth2 协议与微服务架构</h3>
<p>对应到微服务系统中,<strong>服务提供者充当的角色就是资源服务器,而服务消费者就是客户端</strong>。所以每个服务本身既可以是客户端,也可以作为资源服务器,或者两者兼之。当客户端拿到 Token 之后,该 Token 就能在各个服务之间进行传递。如下图所示:
<img src="assets/CioPOWDewFaAKXrYAABQvvLUy-Q531.png" alt="Drawing 6.png" /></p>
<img src="assets/CioPOWDewFaAKXrYAABQvvLUy-Q531.png" alt="png" /></p>
<p>OAuth2 协议在服务访问场景中的应用</p>
<p>在整个 OAuth2 协议中,最关键的问题就是如何获取客户端授权。就目前主流的微服架构来说,当我们发起 HTTP 请求时,关注的是如何通过 HTTP 协议透明而高效地传递令牌,此时授权码模式下通过回调地址进行授权管理的方式就不是很实用,密码模式反而更加简洁高效。因此,在本专栏中,我们将使用<strong>密码模式</strong>作为 OAuth2 协议授权模式的默认实现方式。</p>
<h3>小结与预告</h3>
<p>今天我们进入微服务安全性领域展开了探讨,在这个领域中,认证和授权仍然是最基本的安全性控制手段。通过系统分析微服务架构中的认证和授权解决方案,我们引入了 OAuth2 协议,这也是微服务架构体系下主流的授权协议。我们对 OAuth2 协议具备的角色、授权模式以及与微服务架构之间的集成关系做了详细展开。</p>
<p>本讲内容总结如下:</p>
<p><img src="assets/CioPOWDewF2AG8SOAAGkQKcZcsU075.png" alt="Drawing 7.png" /></p>
<p><img src="assets/CioPOWDewF2AG8SOAAGkQKcZcsU075.png" alt="png" /></p>
<p>最后给你留一道思考题:你能描述 OAuth2 协议中所具备的四大角色以及四种授权模式吗?欢迎在留言区和我分享你的收获。</p>
</div>
</div>

View File

@@ -177,7 +177,7 @@ public class AuthorizationServer {
<h3>设置客户端和用户认证信息</h3>
<p>上一讲我们提到 OAuth2 协议存在四种授权模式,并提到在微服务架构中,密码模式以其简单性得到了广泛的应用。在接下来的内容中,我们就以密码模式为例展开讲解。</p>
<p>在密码模式下,用户向客户端提供用户名和密码,并将用户名和密码发给授权服务器从而请求 Token。授权服务器首先会对密码凭证信息进行认证确认无误后向客户端发放 Token。整个流程如下图所示</p>
<p><img src="assets/Cgp9HWDwB2GATe64AAEacKakyHk379.jpg" alt="Drawing 0.png" /></p>
<p><img src="assets/Cgp9HWDwB2GATe64AAEacKakyHk379.jpg" alt="png" /></p>
<p>密码模式授权流程示意图</p>
<p>请注意,授权服务器在这里执行认证操作的目的是<strong>验证传入的用户名和密码是否正确</strong>。在密码模式下,这一步是必需的,如果采用其他授权模式,不一定会有用户认证这一环节。</p>
<p>确定采用密码模式后,我们来看为了实现这一授权模式,需要对授权服务器做哪些开发工作。首先我们需要设置一些基础数据,包括客户端信息和用户信息。</p>
@@ -270,11 +270,11 @@ public class SpringWebSecurityConfigurer extends WebSecurityConfigurerAdapter {
<h3>生成 Token</h3>
<p>现在OAuth2 授权服务器已经构建完毕,启动这个授权服务器,我们就可以获取 Token。我们在构建 OAuth2 服务器时已经提到授权服务器中会暴露一批端点供 HTTP 请求进行访问,而获取 Token 的端点就是<a href="http://localhost:8080/oauth/token">http://localhost:8080/oauth/token</a>。在使用该端点时,我们需要提供前面配置的客户端信息和用户信息。</p>
<p>这里使用 Postman 来模拟 HTTP 请求,客户端信息设置方式如下图所示:</p>
<p><img src="assets/Cgp9HWDwB76Ad4WHAAFWqbAkk6I359.jpg" alt="Drawing 1.png" /></p>
<p><img src="assets/Cgp9HWDwB76Ad4WHAAFWqbAkk6I359.jpg" alt="png" /></p>
<p>客户端信息设置示意图</p>
<p>我们在“Authorization”请求头中指定认证类型为“Basic Auth”然后设置客户端名称和客户端安全码分别为“spring”和“spring_secret”。</p>
<p>接下来我们指定针对授权模式的专用配置信息。首先是用于指定授权模式的 grant_type 属性,以及用于指定客户端访问范围的 scope 属性,这里分别设置为 “password”和“webclient”。既然设置了密码模式所以也需要指定用户名和密码用于识别用户身份这里我们以“spring_user”这个用户为例进行设置如下图所示</p>
<p><img src="assets/Cgp9HWDwB_CAVjFCAAEqxkNRiNY244.jpg" alt="Drawing 2.png" /></p>
<p><img src="assets/Cgp9HWDwB_CAVjFCAAEqxkNRiNY244.jpg" alt="png" /></p>
<p>用户信息设置示意图</p>
<p>在 Postman 中执行这个请求,会得到如下所示的返回结果:</p>
<pre><code>{
@@ -289,7 +289,7 @@ public class SpringWebSecurityConfigurer extends WebSecurityConfigurerAdapter {
<h3>小结与预告</h3>
<p>对微服务访问进行安全性控制的首要条件是<strong>生成一个访问 Token</strong>。这一讲我们从构建 OAuth2 服务器讲起,基于密码模式给出了如何设置客户端信息、用户认证信息以及最终生成 Token 的实现过程。这个过程中需要开发人员熟悉 OAuth2 协议的相关概念以及 Spring Security 框架中提供的各项配置功能。</p>
<p>本讲内容总结如下:</p>
<p><img src="assets/CioPOWDwCAOAI2-LAAGPXTcuzx0579.jpg" alt="13-2.jpg" /></p>
<p><img src="assets/CioPOWDwCAOAI2-LAAGPXTcuzx0579.jpg" alt="png" /></p>
<p>最后给你留一道思考题:基于密码模式,你能说明生成 Token 需要哪些具体的开发步骤吗?</p>
</div>
</div>

View File

@@ -210,7 +210,7 @@ user.getUserAuthentication().getAuthorities()));
<p>我们知道“0efa61be-32ab-4351-9dga-8ab668ababae”这个 Token 是由“spring_user”这个用户生成的可以看到该结果中包含了用户的用户名、密码以及该用户名所拥有的角色这些信息与我们在上一讲中初始化的“spring_user”用户信息保持一致。我们也可以尝试使用“spring_admin”这个用户来重复上述过程。</p>
<h3>在微服务中嵌入访问授权控制</h3>
<p>在一个微服务系统中,每个微服务作为独立的资源服务器,对自身资源的保护粒度并不是固定的,可以根据需求对访问权限进行精细化控制。在 Spring Security 中,对访问的不同控制层级进行了抽象,形成了<strong>用户、角色和请求方法</strong>这三种粒度,如下图所示:</p>
<p><img src="assets/CioPOWDwBtmAMiWZAAFQfgXxt_0021.jpg" alt="image-2.png" /></p>
<p><img src="assets/CioPOWDwBtmAMiWZAAFQfgXxt_0021.jpg" alt="png" /></p>
<p>用户、角色和请求方法三种控制粒度示意图</p>
<p>基于上图,我们可以对这三种粒度进行排列组合,形成用户、用户+角色以及用户+角色+请求方法这三种层级,这三种层级能够访问的资源范围逐一递减。用户层级是指只要是认证用户就能访问服务内的各种资源;而用户+角色层级在用户层级的基础上,还要求用户属于某一个或多个特定角色;最后的用户+角色+请求方法层级要求最高,能够对某些 HTTP 操作进行访问限制。接下来我们针对这三个层级展开讨论。</p>
<h4>用户层级的权限访问控制</h4>
@@ -263,7 +263,7 @@ public class ResourceServerConfiguration extends ResourceServerConfigurerAdapter
<p>现在我们使用普通“USER”角色生成的 Token并调用&quot;/order/&quot;端点中的 Update 操作同样会得到“access_denied”的错误信息。而尝试使用“ADMIN”角色生成的 Token 进行访问,就可以得到正常响应。</p>
<h3>在微服务中传播 Token</h3>
<p>我们知道一个微服务系统势必涉及多个服务之间的调用,并形成一个链路。因为访问所有服务的过程都需要进行访问权限的控制,所以我们需要确保生成的 Token 能够在服务调用链路中进行传播,如下图所示:</p>
<p><img src="assets/Cgp9HWDwBumAe_JvAAEWjcq9xMI017.jpg" alt="image-3.png" /></p>
<p><img src="assets/Cgp9HWDwBumAe_JvAAEWjcq9xMI017.jpg" alt="png" /></p>
<p>微服务中 Token 传播示意图</p>
<p>那么,如何实现上图中的 Token 传播效果呢Spring Security 基于 RestTemplate 进行了封装,专门提供了一个用在 HTTP 请求中传播 Token 的 OAuth2RestTemplate 工具类。想要在业务代码中构建一个 OAuth2RestTemplate 对象,可以使用如下所示的示例代码:</p>
<pre><code>@Bean

View File

@@ -276,7 +276,7 @@ public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws E
<li>第三步,也是最关键的一步,就是在通过 RestTemplate 发起请求时,能够把这个 Token 自动嵌入到所发起的每一个 HTTP 请求中。</li>
</ul>
<p>整个实现思路如下图所示:</p>
<p><img src="assets/Cgp9HWDwBeGACDQUAAFfi6HuFQU869.jpg" alt="1.png" />
<p><img src="assets/Cgp9HWDwBeGACDQUAAFfi6HuFQU869.jpg" alt="png" />
在服务调用链中传播 JWT Token 的三个实现步骤</p>
<p>实现这一思路需要你对 HTTP 请求的过程和原理有一定的理解,在代码实现上也需要有一些技巧,下面我一一展开。</p>
<p>首先,在 HTTP 请求过程中,我们可以通过过滤器 Filter 对所有请求进行过滤。Filter 是 Servlet 中的一个核心组件其基本原理就是构建一个过滤器链并对经过该过滤器链的请求和响应添加定制化的处理机制。Filter 接口的定义如下所示:</p>

View File

@@ -362,10 +362,10 @@ public @interface EnableOAuth2Sso {
<h3>案例演示</h3>
<p>最后,让我们演示一下整个单点登录过程。依次启动 SSO 服务器以及 app1 和 app2然后在浏览器中访问 app1 地址<a href="http://localhost:8080/app1/system/profile">http://localhost:8080/app1/system/profile</a>,这时候浏览器就会重定向到 SSO 服务器登录页面。</p>
<p>请注意如果我们在访问上述地址时打开了浏览器的“网络”标签并查看其访问路径就可以看到确实是先跳转到了app1的登录页面<a href="http://localhost:8080/app1/login">http://localhost:8080/app1/login</a>),然后又重定向到 SSO 服务器。由于用户处于未登录状态,所以最后又重定向到 SSO 服务器的登录界面(<a href="http://localhost:8888/login">http://localhost:8888/login</a>),整个请求的跳转过程如下图所示:</p>
<p><img src="assets/Cgp9HWD2g5SAV9ZCAACNhBC8lAY290.png" alt="17-2.png" /></p>
<p><img src="assets/Cgp9HWD2g5SAV9ZCAACNhBC8lAY290.png" alt="png" /></p>
<p>未登录状态访问 app1 时的网络请求跳转流程图</p>
<p>我们在 SSO 服务器的登录界面输入正确的用户名和密码之后就可以认证成功了,这时候我们再看网络请求的过程,如下所示:</p>
<p><img src="assets/Cgp9HWD2g4OAY_pLAACZ0h8qrkQ895.png" alt="17-3png.png" /></p>
<p><img src="assets/Cgp9HWD2g4OAY_pLAACZ0h8qrkQ895.png" alt="png" /></p>
<p>登录 app1 过程的网络请求跳转流程图</p>
<p>可以看到,在成功登录之后,授权系统重定向到 app1 中配置的回调地址(<a href="http://localhost:8080/app1/login">http://localhost:8080/app1/logi</a><a href="http://localhost:8080/app1/login">n</a>)。与此同时,我们在请求地址中还发现了两个新的参数 code 和 state。app1 客户端就会根据这个 code 来访问 SSO 服务器的/oauth/token 接口来申请 token。申请成功后重定向到 app1 配置的回调地址。</p>
<p>现在,如果你访问 app2与第一次访问 app1 相同,浏览器先重定向到 app2 的登录页面,然后又重定向到 SSO 服务器的授权链接,最后直接就重新重定向到 app2 的登录页面。不同之处在于,此次访问并不需要再次重定向到 SSO 服务器进行登录,而是成功访问 SSO 服务器的授权接口,并携带着 code 重定向到 app2 的回调路径。然后 app2 根据 code 再次访问 /oauth/token 接口拿到 token这样就可以正常访问受保护的资源了。</p>

View File

@@ -160,7 +160,7 @@ function hide_canvas() {
<h3>安全性测试与 Mock 机制</h3>
<p>正如前面提到的,验证安全性功能正确性的难点在于组件与组件之间的依赖关系,为了弄清楚这个关系,这里就需要引出测试领域非常重要的一个概念,即 Mock模拟<strong>针对测试组件涉及的外部依赖,我们的关注点在于这些组件之间的调用关系,以及返回的结果或发生的异常等,而不是组件内部的执行过程</strong>。因此常见的技巧就是使用 Mock 对象来替代真实的依赖对象,从而模拟真实的调用场景。</p>
<p>我们以一个常见的三层 Web 服务架构为例来进一步解释 Mock 的实施方法。Controller 层会访问 Service 层,而 Service 层又会访问 Repository 层,我们对 Controller 层的端点进行验证时,就需要模拟 Service 层组件的功能。同样,对 Service 层组件进行测试时,也需要假定 Repository 层组件的结果是可以获取的,如下所示:</p>
<p><img src="assets/Cgp9HWD_wkmAWv3aAADg8flYF9Q967.jpg" alt="2.jpg" /></p>
<p><img src="assets/Cgp9HWD_wkmAWv3aAADg8flYF9Q967.jpg" alt="png" /></p>
<p>Web 服务中各层组件与 Mock 对象示意图</p>
<p>对于 Spring Security 而言,上图所展示的原理同样适用,例如我们可以通过模拟用户的方式来测试用户认证和授权功能的正确性。在本讲后面的内容中,我们会给出相关的代码示例。</p>
<h3>Spring Security 中的测试解决方案</h3>