跳到主要内容

01、Spring Security 速成 - 前言以及框架介绍

一、前言

谈起Spring Security,我不知道大家是什么印象,对于我来说,我觉得非常复杂,尤其配合Oauth2以及微服务使用,我曾经看过黑马的,看过乐字节的,都觉得还是太浅了,后面还花200多在网上买了相关教程,虽然这个教程实践性代码讲的比较好,但是对源码相关内容毫无涉及,而且所用框架比较繁琐,以至于真正要开发很多细节还是没有搞懂,自己也一直没有去记录相关的笔记。

所以这次想通过阅读《Spring Security实战》这本书去弥补对Spring Security的这个漏洞,对这个安全框架整个体系要一个更加全面的认识和学习实践,在此也通过博客记录相关知识,我会结合书本以及相关博客将内容体系尽量全面的公布。

在我看到该书前言的时候,有句话让我有点印象深刻,它说当我想和使用JWT的人交流的时候,我发现他们对Spring Security没有一个很好的认知,甚至基本知识也不清楚。这点和我当时的情形非常相似,我一直认为安全框架它虽然难,但是如果能学会一个固定使用体系,它可以活用到所有的框架当中去,这使得我的注意力很容易被security其他的内容所吸引,例如JWT,Oauth2,然后跟着别人项目手打发现他们基于Spring Security的这个基础自己也只是了解过,但并不了解它是怎么运作的,我只是将它们看作死代码,这样其实是非常不好的,最后导致Spring Security,Oauth2,JWT什么都没学会,知识也是非常零散。该书也在前言强调了一个顺序,Spring Security百度一搜一堆资料,但是唯一的漏洞就是太过于零散,使得大家没有一个正确的顺序去学习,知识体系不完整,导致很多人(包括我)去使用Spring Security去开发的时候暴露出一堆漏洞,最后就算是靠博客去抄都抄不出一个正确的安全系统,报错一堆,想必应该不少人和我初学security都出现过这样的问题,所以在后面的学习中我会根据代码和知识体系合并按照正确的顺序去慢慢深入spring Security这个框架,希望大家和我共同进步,早日征服Spring Security。

二、安全性现状

可能你像我一样,考虑到Security的验证等麻烦,往往把逻辑代码开发的差不多了再去考虑嵌入Security,这种态度应该得到改变,开发程序的一开始就必须将安全性考虑进去。如果先进行其他业务开发,后面嵌入安全,例如对token的处理,对接口的权限处理将会变得非常复杂。所以安全性的考虑需要我们在项目的一开始就考虑到安全性。如果过度注重功能(可能您会额外关注性能,但是它并不属于安全性,因为非功能需求往往比功能性需求更容易被无视),可能会引发系统的故障甚至遭受网络攻击进而极大的影响程序所有者的收益。

三、Spring Security的定义和用途

本节将讨论Spring Security和Spring家族的联系,通过官网https://spring.io/projects/spring-security可以得知,Spring Security被描述为一种用于身份验证访问控制的强大且高度可定制的框架,简言之,它就是一个极大简化了的让Spring程序具备安全性保障的框架。你可以在https://github.com/spring-projects/spring-security获得其源码。

 

ps:如果要使用Spring Security,至少需要Java 8.

如果您正在开发Spring的程序,那么Spring Security可能会成为你实现应用程序级别安全性的最佳解决方案,不过Spring Security并不会自动对应用程序进行保护,它取决于我们对Spring Secutiry的配置以及定制,这是我们需要取了解的,并且取决于从功能需求到架构的多种因素。

学习过Spring或者Springboot的应该知道,框架的原理是从Spring上下文的管理开始的,我们在context中定义bean,并且根据config来管理这些bean,且我们只需要通过注解来处理这些config即可,无需通过老套的XML.

我们使用注解告诉Spring要做什么,比如公开端点,在事务中包装方法,拦截切面中的方法等等。Spring Security也是如此,我们同样可以通过注解,bean和Spring风格的配置样式自由定制使用Spring Security。

那如何在Spring中使用Spring Security呢,我们最常遇到的案例之一就是当我们决定是否允许某人执行操作或使用某些数据的情形。根据上面提到的配置,我们要编写拦截请求以及确保发出请求的人有权访问受保护资源的Spring Security组件,我们需要配置这些组件精确的完成所需的工作。

Spring Security组件的其他职责与数据存储以及系统不同部分之间的数据传输有关。通过拦截对这些不同部分的调用,组件可以对数据进行操作。例如,在存储数据时,这些组件可以应用加密或哈希算法数据编码使数据只能被授权实体访问。在Spring应用程序中,开发人员必须添加和配置组件,以便在需要的地方完成这部分工作。

所以呢,通过上面的讲述,我们都离不开两个字-组件,Spring Security提供了预定义的功能来帮助我们免去编写样板代码或者在应用之间重复编写相同逻辑的枯燥工作,但是即便如此我们也可以取配置它的任何一个组件,因而也就为我们提供了很大的灵活性,而我们后面的一个非常重要的学习就是取认知并且使用其各个主要的组件。

3.1、安全性引发的相关问题

安全性是一个复杂的主题。在软件系统中,安全性并不仅仅适用于应用程序级别。例如对于网络通信而言,需要考虑一些问题和特定的实践,而对于存储,则完全是另一回事。类似的,在部署等方面也有不同的思想体系,这些情形不一而足。Spring Security是一个属于应用程序级别的安全性框架,我们后面也只介绍这样的安全级别及其影响。

应用程序级别安全性是指应用程序为保护其执行所处的环境以及其处理和存储的数据而应该做的事。请注意,这不仅与应用程序所影响和使用的数据有关。应用程序可能包含允许恶意人员损害整个系统的漏洞。

 

随着系统变得越来越复杂,我们会发现需要与身份验证和授权相关的特定实现的不同情况。

例如,如果希望代表用户的数据子集对系统特定组件进行授权,该怎么办呢?假设打印机需要访问来读取用户的文档。是否应该直接与打印机共享用户的凭据?这样做就会允许打印机获取到超过所需的更多权利!并且还会暴露用户的凭据。有没有一种合适的方法在不模拟用户的情况下做到这一点?这些都是重要的问题。

对于Spring Security,我们有时甚至更愿意对同一组件的不同层使用授权,比如后面我们会学到关于全局方法安全性的内容,当拥有一组预定义的角色和权限时,设计就会变得更复杂。

此外,还需要请你注意数据存储,持久化数据安全性也是应用程序的职责。有时程序需要保存私钥用来加密数据或哈希数据。像凭据和私钥这样的加密数据应该被谨慎的存储,通常我们会将它存在一个私密的数据库中。

通过对这些情况的简短描述,希望能够概述我们所说的应用程序安全性以及这个主题的复杂性。软件安全性是一个复杂的问题,但是本文将只介绍Spring Security特别需要理解的所以细节内容及相关安全性问题我们将了解它适用的地方以及不适用的地方以及如何定制使用它。

3.2、Web应用程序中的常见安全漏洞

在讨论如何应用安全性之前,首先应知道需要保护应用程序免受哪方面的损害。要做一些恶意的事情,攻击者就需要识别并利用应用程序的漏洞。我们经常将漏洞描述为允许执行非预期操作的缺陷,通常这类操作都是带有恶意意图的。

了解漏洞的一个好的起点就是了解开放式Web应用程序安全项目(Open Web Application Security Project),也称为OWASP(https://wwww.owasp.org).在OWASP中,可以找到应该在应用程序中避免的最常见漏洞的描述。我们会发现以下几点:

  • 不完整的身份验证
  • 会话固定
  • 跨站脚本(XSS)
  • 跨站请求伪造(CSRF)
  • 注入
  • 敏感数据暴露
  • 缺乏方法访问控制
  • 使用具有已知漏洞的依赖项

上面这几点都与应用程序级别的安全性相关,并且其中大多数也与使用Spring Security直接相关。

3.2.1、不完整的身份验证

前面我们在介绍SpringSecurity概念提起过身份验证和授权。通过身份验证,应用程序就可以标识一个用户(个人或另一个应用程序)。识别这些信息之后可以决定他们应该被允许做什么-这就是授权。

如果一个不怀好意的人以某种方式获得了不属于他的功能或数据的访问权,就可以说我们的授权被破坏了,虽说Spring Security可以有助于减少此漏洞出现的可能性,但是若没有正确的使用,仍然可能出现这种问题。例如,可以使用Spring Security为具有特定角色的经过身份验证的个人定义对特定端点的访问,如果在数据级别上没有限制,那么可能有些人会另辟蹊径获取属于其他人的数据。

请看下图,经过身份验证的用户可以访问/products/{name}的接口。而我们的应用程序可以调用这个接口以便从db中检索和显示该用户的产品。但是,如果应用程序在返回这些产品时没有验证产品属于谁,会发生什么呢?一些用户可以找到另一个用户详细信息的方法,这种情况在设计接口一开始就应该考虑到,才能避免类似情况的发生。

 

3.2.2、什么是会话固定

会话固定(session fixation)漏洞是Web应用程序的一个更为具体,更为严重的缺陷。利用服务器的 session 不变机制,如果存在该漏洞,攻击者就可以通过重用以前生成的会话ID来模拟有效用户。如果在认证过程中,Web应用程序没有分配唯一的会话ID,就会出现此漏洞。这可能会导致现有会话ID的重用。这个漏洞的利用过程包括获得一个有效的会话ID并且让目标受害者的浏览器使用它。

根据web应用程序实现方式的不同,恶意攻击者可以通过各种方式利用此漏洞。例如,如果应用程序在URL中提供了会话ID,受害者可能就会被诱骗单击恶意链接。如果应用程序使用了隐藏属性,攻击者就可以欺骗受害者使用外部表单,然后将其操作提交到服务器。如果应用程序将会话的值存储在cookie中,攻击者就可以注入一个脚本并强制受害者的浏览器执行它。

例如:攻击者首先在未登录状态下访问网站得到 sessionid,然后把带有 sessionid 的链接发给受害者,受害者点击链接并登录,而由于 sessionid 是不变的,攻击者就可以用这个 sessionid 来登录,获取受害者的页面。更形象一点就是,我在网吧上号,然而忘了取消记住密码,然后我号就没了。攻击的整个过程,会话ID是没变过的,所以导致此漏洞

攻击修复

1、登录重建会话

每次登录后都重置会话ID,并生成一个新的会话ID,这样攻击者就无法用自己的会话ID来劫持会话,核心代码如下:

// 会话失效
session.invalidate();
// 会话重建
session=request.getSession(true);

2、 禁用客户端访问Cookie;
此方法也避免了配合XSS攻击来获取Cookie中的会话信息以达成会话固定攻击。在Http响应头中启用HttpOnly属性,或者在tomcat容器中配置。关于HttpOnly更多详细说明大家可以自行百度。

3.2.3、什么是跨站脚本(XSS)

跨站脚本(cross-site-scripting)也称为XSS,是向真实网站添加恶意代码以便恶意收集用户信息的过程。XSS 攻击可能通过 Web 应用程序中的安全漏洞进行,并且通常通过注入客户端脚本来利用。其潜在的影响可能与账户模拟(结合会话固定)或参与分布式攻击(如DDoS)有关。

举例说明。用户在WEB应用程序中发布消息或者评论。在发布消息后,网站会显示它,以便访问该页面的每个用户都能看到它。每天可能有数百人访问这个页面,这取决于该站点的受欢迎程度。就此处的示例而言,我们将它看作一个知名站点,并且有相当多的人访问它的页面。如果该客户发布了一个恶意脚本,那么在Web页面上出现该脚本时,浏览器执行该脚本又会发生什么呢?

 

我们可以看到用户在网络论坛上发布包含脚本的评论,该脚本试图从另一个应用程序(也就是上图的应用程序X)发布或获取大量数据的请求,这个应用程序X就是此次攻击的受害者。如果该Web论坛应用程序允许跨站脚本XSS,那么所有显示带有该恶意评论的用户都会照单全收地接收这个脚本。

 

关于XSS更详细的可以浏览https://blog.csdn.net/m0_54020412/article/details/125289605,那么XSS我们可以在后面通过写拦截器配置进我们的Security,就可以有效拦截XSS攻击了。

3.2.4、什么是跨站请求伪造(CSRF)

跨站请求伪造(CSRF)漏洞在Web应用程序中也很常见。CSRF攻击会恶意利用可以从应用程序外部提取重复使用能够调用特定服务器上操作的URL。如果服务器信任该执行而不检查请求的来源,那么恶意攻击者就可以从任何其他地方执行其操作。借助CSRF,攻击者可以通过隐藏操作来让用户在服务器上执行非预期的操作。通常,使用这个漏洞,攻击者会将更改系统中数据的操作作为目标。

 

避免此漏洞的方法之一是使用令牌标识请求或使用跨资源共享(简称CORS)限制。换句话说,就是要验证请求的来源。这个我们同样会在后面Spring Security中讲到如何应对。

3.2.5、理解Web应用程序中的注入漏洞

注入攻击很普遍。在注入攻击中,攻击者会利用漏洞将特定数据引入系统。其目的是破坏系统、以非预期的方式更改数据,或者检索不该由攻击者访问的数据

注入攻击有很多种类型。甚至之前提到的XSS也可以归并为注入漏洞。归根结底,注入攻击会通过某种方式注入客户端脚本来破坏系统。其他的例子还包括如SQL注入、XPath注入、OS命令注入,LDAP注入等。

最古老的并且可能也是众所周知的注入漏洞类型之一就是SQL注入。如果应用程序存在SQL注入漏洞,攻击者可以尝试变更或运行不同SQL查询来更改,删除或者从系统从提取数据,在最高级的SQL注入攻击中,恶意攻击可以在系统上运行OS命令,从而导致整个系统遭受破坏。

3.2.6、应对敏感数据暴露

就复杂性而言,即便机密数据的泄露似乎也是最容易理解和最不复杂的漏洞,这一观点仍旧是最常见的错误之一。出现这种情况的原因可能是,网上所能找到的大多数教程和示例,为了简单起见,都是直接在配置文件中配置隐私数据,比如凭据,私钥等等。而我们程序员很多都是在不断的阅读这些示例去学习的,那么这种简化的缺点就会让很多程序员习惯性的忽略暴露隐私问题这个缺点。我们一般推荐将隐私数据存进db,因为至少对于生产环境来说,只有特定权限的人才能看到这些隐私键的值。

但假设我们直接在Spring boot项目中的application.properties或yaml文件中设置他们,那么只要是可以看到源代码的任何人都能访问这些隐私值,此外,还可能会发现这些值的所有变更历史都存储在源代码的版本管理系统中。

对于隐私数据暴露,还与之相关的还要应用程序把这些隐私值写入控制台或存储在elasticSearch,Splunk等数据库中的日志信息。我们常常会看到暴露了被开发人员所遗忘的敏感数据的日志。

例如:

[warning] Login failed for username X And password Y.User with username X has password Z

所以我这边建议永远不要记录非公开信息的内容。此处所说的公开是指任何人都可以看到或访问这些信息。像私钥,证书,密码等这些非公开内容,不应该与错误,警告,或者提示性消息一起被记录!

这类应用程序行为也是一个通过数据暴露而产生的漏洞。如果由于错误请求(例如请求的一部分数据缺失)而让应用程序遇到NPE,该异常就不应该出现在响应主体在,同时,HTTP状态应该是400,而不是500.4XX类型的HTTP状态码旨在用来表示客户端上的问题。错误请求归根结底是客户端的问题,因此应用程序应该相应的表示它。5XX类型的HTTP状态码旨在通知我们服务器上有一个问题。例如以下代码片段中给出的响应有什么错误:

{
   
     
    "status": 500,
    "error":"Internal Server error",
    "message":"Connection not found for IP Address 10.2.5.8/8080",
    "path":"/product/add"
}

这个异常信息似乎透露一个IP地址,攻击者可以使用这个IP了解网络配置,并最终找到控制基础设施中的虚拟机的方法。当然,,仅有这一点数据谁也不能对整个系统做出任何伤害,但若是这样的泄露片段多起来,就可以获得对系统产生不利影响所需要的一切内容。

此外,不推荐在响应中使用异常堆栈
例如:

at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java.1128)
~[na:na]
at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java.628) 
~[na:na]
at org.apache.tomcat.util.threads.TaskThreads$WrappingRunnable.run(TaskThread.java:61)
~[tomacat-embed-core-9.0.26.jar:9.0.26]

这种方法也暴露了应用程序的内部结构。从异常的堆栈中,我们可以看到命名规范以及用于特定操作的对象以及它们之间的关系。不过更糟的是,堆栈有时会泄露程序所使用的依赖的版本号。首先我们应当避免使用脆弱的依赖项,如果错误地使用了,至少不要暴露这个错误,像之前堆栈片段中的信息泄露可以促使攻击者找到版本中的漏洞,这是在邀请他们来破坏系统。

攻击者往往会利用最细微的信息细节来攻击系统,以下例子也是我组同事曾经被指出过的一个问题,请看:

Response A:
{
   
     
    "status": 401,
    "error":"Unauthorized",
    "message":"Username is not correct",
    "path":"/login"
}
Response B:
{
   
     
    "status": 401,
    "error":"Unauthorized",
    "message":"Password is not correct",
    "path":"/login"
}

在这个示例中,响应A和响应B是调用相同身份验证端点的不同结果,它们似乎没有暴露任何与类设计或者系统基础设施相关的信息,但它们隐含着另一个问题,如果信息暴露了上下文信息,那么这些信息可能隐含着漏洞。基于提供给端点的不同输入所产生的不同消息可以被用于理解执行过程的上下文。在这个示例中,这些上下文可以用来知悉,用户名是正确的,但密码是错误的。这使得系统更容易遭受暴力破解。所以提供给客户端的响应不应该帮助识别对特定输入的可能猜测。在这个示例中,响应应该在这两种情况下都提供相同的信息

{
   
     
    "status": 401,
    "error":"Unauthorized",
    "message":"Username or Password is not correct",
    "path":"/login"
}

这些预防措施看起来微不足道,但如果不进行预防,敏感数据的暴露则可能成为用于攻击系统的优良工具。