①②③④⑤⑥⑦⑧⑨⑩⑪⑫⑬⑭⑮⑯⑰⑱⑲⑳✕✓✔✖
[TOC]
密码存储设计,经过了以下过程。
代理型密码编码器。可以适用老的密码系统以及新的密码系统。密码的格式如下: 通过 id 知道那种具体的加密方法。
{id}encodedPassword
您可以使用 PasswordEncoderFactories
轻松构造 DelegatingPasswordEncoder
的实例。
PasswordEncoder passwordEncoder =PasswordEncoderFactories.createDelegatingPasswordEncoder();
或者,您可以创建自己的自定义实例。例如:
// 在这里可以定义自己的加密算法,例如将Keycloak中的密码转换成可以在自己系统中用的密码String idForEncode = "bcrypt";Map encoders = new HashMap<>();encoders.put(idForEncode, new BCryptPasswordEncoder());encoders.put("noop", NoOpPasswordEncoder.getInstance());encoders.put("pbkdf2", new Pbkdf2PasswordEncoder());encoders.put("scrypt", new SCryptPasswordEncoder());encoders.put("sha256", new StandardPasswordEncoder());PasswordEncoder passwordEncoder =new DelegatingPasswordEncoder(idForEncode, encoders);
为了更好的理解,这里分析一下代码。系统中的带部分类都继承了PasswordEncoder
接口
public interface PasswordEncoder {String encode(CharSequence rawPassword);boolean matches(CharSequence rawPassword, String encodedPassword);default boolean upgradeEncoding(String encodedPassword) {return false;}}
{id}encodedPassword
id
是一个标识符,用来查找那个 PasswordEncoder
进行加密以及进行密码比较。下面是具体的例子:
{bcrypt}$2a$10$dXJ3SW6G7P50lGmMkkmwe.20cQQubK3.HZWzG3YB1tlRy.fqvM/BG{noop}password{pbkdf2}5d923b44a6d129f3ddf3e3c8d29412723dcbde72445e8ef6bf3b508fbf17fa4ed4d6b99ca763d8dc{scrypt}$e0801$8bWJaSu2IKSn9Z9kM+TPXfOc/9bdYSrN1oD9qfVThWEwdRTnO7re7Ei+fUZRJ68k9lTyuTeUp4of4g24hHnazw==$OAOec05+bXxvuu/1qZ6NUR+xQYvYv7BeL1QxwRpY5Pc={sha256}97cde38028ad898ebc02e690819fa220e88c62e0699403e94fff291cfffaf8410849f27605abcbc0
这是一个自适应单向函数,大概需要 1 秒的时间校验密码,系统默认设置强度是 10,你可以按照自己系统,来调整强度,让校验在 1 秒内。
// Create an encoder with strength 16BCryptPasswordEncoder encoder = new BCryptPasswordEncoder(16);String result = encoder.encode("myPassword");assertTrue(encoder.matches("myPassword", result));
这是一个自适应单向函数,大概需要 1 秒的时间校验密码,这个算法是【哈希式密码竞赛Password Hashing Competition】的获胜者 。为了避免被特殊硬件设备破解,被设计出来,会消耗大量内存。
// Create an encoder with all the defaultsArgon2PasswordEncoder encoder = new Argon2PasswordEncoder();String result = encoder.encode("myPassword");assertTrue(encoder.matches("myPassword", result));
④⑤⑥
这是一个自适应单向函数,大概需要 1 秒的时间校验密码,适合 FIPS 认证的系统 ,联邦信息处理标准 (FIPS) 是美国政府适用于信息技术和计算机安全的标准。
// Create an encoder with all the defaultsPbkdf2PasswordEncoder encoder = new Pbkdf2PasswordEncoder();String result = encoder.encode("myPassword");assertTrue(encoder.matches("myPassword", result));
这是一个自适应单向函数,大概需要 1 秒的时间校验密码。为了避免被特殊硬件设备破解,被设计出来,会消耗大量内存。
// Create an encoder with all the defaultsSCryptPasswordEncoder encoder = new SCryptPasswordEncoder();String result = encoder.encode("myPassword");assertTrue(encoder.matches("myPassword", result));
系统中还有其他的密码编码器,都是为了遗留系统而保留下来的,那些密码编码器都认为不安全了,不推荐在新的系统中使用。
系统默认的是 DelegatingPasswordEncoder,当然你可以换成你想要的,但是推荐使用默认的。
@Beanpublic static PasswordEncoder passwordEncoder() {return NoOpPasswordEncoder.getInstance();}
大多数系统,都会让用户修改密码。网上有人出了一个规范:A Well-Know URL for Changing Passwords 。统一了修改密码地址路径。
也就是/.well-known/change-password
会自动转向你自己定义的地址,例如:
http.passwordManagement(Customizer.withDefaults())
如果密码管理导航到/.well-known/change-password
时,Spring Security 会重新定位到默认端点 /change-password
。当然也可以定位到你指定的端点,例如下面的例子/update-password
.
http.passwordManagement((management) -> management.changePasswordPage("/update-password"))
针对常见的漏洞 Spring Security 都默认开启了防护,下面有几个高级的防护漏洞的描述方案。
Cross Site Request Forgery (CSRF) 跨站请求伪造
通过一个具体的例子就比较容易理解。假设您的银行网站提供了一个表格,允许将资金从当前登录的用户转移到另一个银行帐户。例如,转账表格可能如下所示:
<form method="post" action="/transfer"><input type="text" name="amount" /><input type="text" name="routingNumber" /><input type="text" name="account" /><input type="submit" value="Transfer" /></form>
相应的 HTTP 请求可能如下所示:
POST /transfer HTTP/1.1Host: bank.example.comCookie: JSESSIONID=randomidContent-Type: application/x-www-form-urlencodedamount=100.00&routingNumber=1234&account=9876
现在假装你对你的银行网站进行了身份验证,然后在不注销的情况下访问一个邪恶的网站。邪恶网站包含一个 HTML 页面,格式如下:
Evil transfer form
<form method="post" action="https://bank.example.com/transfer"><input type="hidden" name="amount" value="100.00" /><input type="hidden" name="routingNumber" value="evilsRoutingNumber" /><input type="hidden" name="account" value="evilsAccountNumber" /><input type="submit" value="Win Money!" /></form>
你想赢钱,所以点击提交按钮。在此过程中,您无意中将 100 美元转移给了恶意用户。这是因为,虽然邪恶网站无法看到您的 cookie,但与您的银行相关的 cookie 仍会随请求一起发送。
最糟糕的是,整个过程本可以使用 JavaScript 实现自动化。这意味着你甚至不需要点击按钮。此外,当访问一个遭受 XSS attack的诚实网站时,这种情况也很容易发生。那么,我们如何保护用户免受此类攻击呢?
为了防止 CSRF 攻击,需要区分正常提交的请求与恶意网站提供的请求。Spring 提供了两种机制来防止 CSRF 攻击:
Both protections require that Safe Methods Must be Idempotent
两种保护都要求安全方法必须是幂等的
申请必须确保 "safe" HTTP methods are idempotent. 这意味着使用 HTTP 方法 GET、HEAD、OPTIONS 和 TRACE 的请求不应更改应用程序的状态。
幂等性,是指该方法多次调用返回的效果(形式)一致,客户端可以重复调用并且期望同样的结果。幂等的含义类似于编程语言中的 setter 方法[1],一次调用和多次调用产生的效果是一致的,都是对一个变量进行赋值。安全性和幂等性含义有些接近,容易搞混。
安全的方法都是只读的方法(GET, HEAD, OPTIONS),不会改变资源状态,显然,这三个方法也是幂等的。
这是占主导地位并且最全面的防止 CSRF 攻击的方法了,这个解决方案是为了确保每个 HTTP 请求除了我们的会话 cookie 之外,还需要一个名为 CSRF 令牌的安全随机生成值必须存在于 HTTP 请求中。(那些 REACT 应用程序怎么办呢?)
提交 HTTP 请求时,服务器必须查找预期的 CSRF 令牌并将其与 HTTP 请求中的实际 CSRF 令牌进行比较。如果值不匹配,则应拒绝 HTTP 请求。
这种保护措施的关键是不能让浏览器自动包含 CSRF token,例如:HTTP parameter 或者 HTTP header 会防御 CSRF 攻击。但是 cookie 就不行。
可以降低标准,仅仅让那些可能更改状态的 request 携带 CSRF token,这样会提高了可用性,因为很多情况下需要外部网上上的连接连到自己网站上的。此外,我们不想在 HTTP GET 中包含随机令牌,因为这会导致令牌泄露。
让我们看看我们的示例在使用 Synchronizer Token Pattern 时会发生怎样的变化。假设实际的 CSRF 令牌需要位于名为 _csrf 的 HTTP 参数中。我们的应用程序的传输表格如下所示:
<form method="post"action="/transfer"><input type="hidden"name="_csrf"value="4bfd1575-3ad1-4d21-96c7-4ef2d9f86721"/><input type="text"name="amount"/><input type="text"name="routingNumber"/><input type="hidden"name="account"/><input type="submit"value="Transfer"/></form>
该表单现在包含一个隐藏输入,其中包含 CSRF 令牌的值。外部站点无法读取 CSRF 令牌,因为同源策略确保恶意站点无法读取响应。
相应的转帐 HTTP 请求如下所示:
POST /transfer HTTP/1.1Host: bank.example.comCookie: JSESSIONID=randomidContent-Type: application/x-www-form-urlencodedamount=100.00&routingNumber=1234&account=9876&_csrf=4bfd1575-3ad1-4d21-96c7-4ef2d9f86721
您会注意到 HTTP 请求现在包含带有安全随机值的_csrf 参数。邪恶网站将无法为_csrf 参数提供正确的值(必须在邪恶网站上明确提供),当服务器将实际的 csrf 令牌与预期的 csrf 令牌进行比较时,传输将失败。
这是一种新兴的方法,在 cookies 中设定一个 SameSite Attribute ,服务器会根据这个值来判断是不是外部的连接。
备注:Spring Security 不直接控制 session cookie,所以需要通过其他方式支持
SameSite
。
- 基于 servlet 的应用程序中, Spring Session支持
SameSite
- 在基于 WebFlux 的应用程序中,Spring Framework 中的CookieWebSessionIdResolver支持
SameSite
例如,具有 SameSite 属性的 HTTP 响应标头可能如下所示:
SameSite HTTP response
Set-Cookie: JSESSIONID=randomid; Domain=bank.example.com; Secure; HttpOnly; SameSite=Lax
SameSite 属性的有效值为:
Strict
- 来自同一站点的任何请求都将包含 cookie。否则,cookie 将不会包含在 HTTP 请求中。Lax
- 当来自同一个站点或请求来自顶级导航并且该方法是幂等的时,将发送指定的 cookie。否则,cookie 将不会包含在 HTTP 请求中。让我们看看如何使用 SameSite 属性保护我们的示例。银行应用程序可以通过在会话 cookie 上指定 SameSite 属性来防止 CSRF。
在我们的会话 cookie 上设置 SameSite 属性后,浏览器将继续发送来自银行网站的请求的 JSESSIONID cookie。浏览器将不再发送带有来自恶意网站的传输请求的 JSESSIONID cookie。由于会话不再存在于来自恶意网站的传输请求中,因此应用程序免受 CSRF 攻击。
在使用SameSite
去保护网站时,有一些考虑因素 considerations 需要认真阅读。
将 SameSite
属性设置为 Strict
可提供更强大的防御,但可能会使用户感到困惑。假设用户已经登陆到了 https://social.example.com,用户在 https://email.example.org 收到一封电子邮件,其中包含指向社交媒体网站的链接。如果用户点击链接,他们理所当然地期望通过社交媒体网站的身份验证。但是,如果 SameSite
属性为 Strict
,则不会发送 cookie
,因此不会对用户进行身份验证。
我们可以通过实施来提高 SameSite 保护对 CSRF 攻击的保护和可用性gh-7537.
另一个明显的考虑是,为了让 SameSite 属性保护用户,浏览器必须支持 SameSite 属性。大多数现代浏览器都支持 SameSite 属性。但是,仍在使用的旧浏览器可能不会。
出于这个原因,通常建议使用 SameSite 属性作为纵深防御,而不是针对 CSRF 攻击的唯一保护。
我们的建议是对普通用户可以通过浏览器处理的任何请求使用 CSRF 保护。如果您只创建非浏览器客户端使用的服务,您可能希望禁用 CSRF 保护。
一个常见的问题是“我需要保护由 javascript 发出的 JSON 请求吗?”,简短的回答是.您必须非常小心,因为存在可能影响 JSON 请求的 CSRF 漏洞。例如,恶意用户可以使用以下形式创建带有 JSON 的 CSRF:
CSRF with JSON form
<formaction="https://bank.example.com/transfer"method="post"enctype="text/plain"><inputname='{"amount":100,"routingNumber":"evilsRoutingNumber","account":"evilsAccountNumber", "ignore_me":"'value='test"}'type="hidden"/><input type="submit" value="Win Money!" /></form>
这将产生以下 JSON 结构
CSRF with JSON request
{ "amount": 100,"routingNumber": "evilsRoutingNumber","account": "evilsAccountNumber","ignore_me": "=test"}
如果应用程序没有验证 Content-Type,那么它将暴露于此漏洞。根据设置,验证 Content-Type 的 Spring MVC 应用程序仍然可以通过将 URL 后缀更新为以 .json 结尾来利用此漏洞,如下所示:
<formaction="https://bank.example.com/transfer.json"method="post"enctype="text/plain"><inputname='{"amount":100,"routingNumber":"evilsRoutingNumber","account":"evilsAccountNumber", "ignore_me":"'value='test"}'type="hidden"/><input type="submit" value="Win Money!" /></form>
如果我的应用程序是无状态的怎么办?这并不一定意味着您受到保护。如果用户不需要在 Web 浏览器中针对给定请求执行任何操作,他们可能仍然容易受到 CSRF 攻击。
例如,考虑一个使用自定义 cookie 的应用程序,该 cookie 包含其中用于身份验证的所有状态,而不是 JSESSIONID。当进行 CSRF 攻击时,自定义 cookie 将与请求一起发送,其方式与我们之前示例中发送 JSESSIONID cookie 的方式相同。此应用程序将容易受到 CSRF 攻击。
使用 basic authentication 验证的应用程序也容易受到 CSRF 攻击。该应用程序很容易受到攻击,因为浏览器会自动在任何请求中包含用户名和密码,其方式与我们之前示例中发送 JSESSIONID cookie 的方式相同。
不将要携带的密钥放到 cookie 等会被浏览器自动传递的存储中。
在实施针对 CSRF 攻击的保护时,需要考虑一些特殊的注意事项。
为了防止伪造登录请求forging log in requests,应保护 HTTP 请求中的登录免受 CSRF 攻击。防止伪造登录请求是必要的,这样恶意用户就无法读取受害者的敏感信息。攻击执行如下:
使用会话超时可以预防 CSRF 攻击,更多的信息可以参考CSRF and Session Timeouts.
有关攻击的详细信息,请参阅此博客文章
通常,预期的 CSRF 令牌存储在会话中。这意味着一旦会话过期,服务器将找不到预期的 CSRF 令牌并拒绝 HTTP 请求。有许多选项可以解决超时问题,每个选项都需要权衡取舍。
有人可能会问,为什么预期的 CSRF 令牌默认不存储在 cookie 中。这是因为存在已知的漏洞,headers(例如,指定 cookie)可以由另一个域设置。
保护多部分请求(文件上传)免受 CSRF 攻击会导致先有鸡还是先有蛋的问题。为了防止发生 CSRF 攻击,必须读取 HTTP 请求的主体以获取实际的 CSRF 令牌。但是,读取正文意味着将上传文件,这意味着外部站点可以上传文件。
将 CSRF 保护与 multipart/form-data 一起使用有两种选择。每个选项都有其权衡。
备注:
- 在将 Spring Security 的 CSRF 保护与多部分文件上传集成之前,确保您可以在没有 CSRF 保护的情况下进行上传。
- 有关在 Spring 中使用多部分表单的更多信息,请参见 1.1.11. Multipart Resolver与MultipartFilter javadoc
第一个选项是在请求正文中包含实际的 CSRF 令牌。通过将 CSRF 令牌放在正文中,将在执行授权之前读取正文。这意味着任何人都可以在您的服务器上放置临时文件。但是,只有授权用户才能提交由您的应用程序处理的文件。一般来说,这是推荐的方法,因为临时文件上传对大多数服务器的影响可以忽略不计。
如果不允许未经授权的用户上传临时文件,另一种方法是在表单的操作属性中包含预期的 CSRF 令牌作为查询参数。这种方法的缺点是查询参数可能会泄露。更一般地,将敏感数据放置在正文或标题中被认为是最佳实践,以确保它不会泄露。更多信息可以在RFC 2616 Section 15.1.3 Encoding Sensitive Information in URI’s.
HiddenHttpMethodFilter
在某些应用程序中,可以使用表单参数来覆盖 HTTP 方法。例如,下面的表单可用于将 HTTP 方法视为删除而不是发布。
CSRF Hidden HTTP Method Form
<form action="/process" method="post"><!-- ... --><input type="hidden" name="_method" value="delete" /></form>
覆盖 HTTP 方法发生在过滤器中。该过滤器必须放在 Spring Security 的支持之前。请注意,覆盖仅发生在post
上,因此这实际上不太可能导致任何实际问题。但是,确保将其放在 Spring Security 的过滤器之前仍然是最佳实践。
Security HTTP Response Headers:安全 HTTP 响应标头
有许多 HTTP 响应标头HTTP response headers 可用于提高 Web 应用程序的安全性。本节专门介绍 Spring Security 提供显式支持的各种 HTTP 响应标头。如有必要,还可以将 Spring Security 配置为提供自定义标头。
Spring Security 的默认设置是包含以下标头:
Example 1. Default Security HTTP Response Headers
Cache-Control: no-cache, no-store, max-age=0, must-revalidatePragma: no-cacheExpires: 0X-Content-Type-Options: nosniffStrict-Transport-Security: max-age=31536000 ; includeSubDomainsX-Frame-Options: DENYX-XSS-Protection: 1; mode=block
备注:
Strict-Transport-Security 仅在 HTTPS 请求时有。
Spring Security 的默认设置是禁用缓存以保护用户的内容。
如果用户通过身份验证查看敏感信息然后退出,我们不希望恶意用户能够单击返回按钮查看敏感信息。
Default Cache Control HTTP Response Headers
Cache-Control: no-cache, no-store, max-age=0, must-revalidatePragma: no-cacheExpires: 0
为了默认安全,然而,Spring Security 默认添加了这些标头。如果您的应用程序提供自己的缓存控制标头,Spring Security 将退出。这允许应用程序确保可以缓存 CSS 和 JavaScript 等静态资源。
过去,包括 Internet Explorer 在内的浏览器会尝试使用内容嗅探来猜测请求的内容类型。这允许浏览器通过猜测未指定内容类型的资源上的内容类型来改善用户体验。例如,如果浏览器遇到没有指定内容类型的 JavaScript 文件,它将能够猜测内容类型然后运行它。
允许上传内容时,还有很多额外的事情应该做(在不同的域中显示文档,确保设置 Content-Type 标头,清理文档等)。但是,这些措施超出了 Spring Security 提供的范围。同样重要的是要指出禁用内容嗅探时,您必须指定内容类型才能正常工作。
内容嗅探的问题在于这允许恶意用户使用多语言(即作为多种内容类型有效的文件)来执行 XSS 攻击。例如,某些网站可能允许用户向网站提交有效的 postscript 文档并进行查看。恶意用户可能会创建一个也是有效 JavaScript 文件的 postscript 文档( postscript document that is also a valid JavaScript file )并使用它执行 XSS 攻击。
Spring Security 默认通过向 HTTP 响应添加以下标头来禁用内容嗅探:
Example 3. nosniff HTTP Response Header
X-Content-Type-Options: nosniff
当您输入银行网站时,您输入 mybank.example.com 还是输入 https://mybank.example.com?如果省略 https 协议,则可能容易受到中间人攻击( Man in the Middle attacks)。即使网站重定向到 https://mybank.example.com,恶意用户也可以拦截初始 HTTP 请求并操纵响应(例如重定向到 https://mibank.example.com 并窃取他们的凭据)。
许多用户忽略了 https 协议,这就是创建 HTTP Strict Transport Security (HSTS)的原因,将 mybank.example.com 添加为 HSTS 主机后,浏览器可以提前知道对 mybank.example.com 的任何请求都应解释为 https://mybank.example.com。这大大降低了中间人攻击发生的可能性。
根据 RFC6797,HSTS header 仅注入到 HTTPS responses 中。为了让浏览器确认标头,浏览器必须首先信任签署用于建立连接的 SSL 证书的 CA(而不仅仅是 SSL 证书)。
将站点标记为 HSTS 主机的一种方法是将主机预加载到浏览器中。另一种是将 Strict-Transport-Security 标头添加到响应中。例如,Spring Security 的默认行为是添加以下标头,指示浏览器将域视为 HSTS 主机一年(一年大约有 31536000 秒)
Strict Transport Security HTTP Response Header
Strict-Transport-Security: max-age=31536000 ; includeSubDomains ; preload
可选的 includeSubDomains
指令指示浏览器子域(例如,secure.mybank.example.com)也应被视为 HSTS 域。
可选的 preload
指令指示浏览器应该在浏览器中预加载域作为 HSTS 域。有关 HSTS 预加载的更多详细信息,请参阅 https://hstspreload.org。
这个不推荐了。今后不用看了。
保护您的网站添加到 frame 中可能产生的安全问题。例如,使用巧妙的 CSS 样式可能会诱使用户点击他们不希望的内容。例如,登录到其银行的用户可能会单击授予其他用户访问权限的按钮。这种攻击被称为点击劫持( Clickjacking)。
另一种处理点击劫持的现代方法是使用内容安全策略 (CSP)。Content Security Policy (CSP)
有多种方法可以减轻点击劫持攻击。例如,为了保护旧版浏览器免受点击劫持攻击,您可以使用 frame breaking code。虽然不完美,但 frame breaking code 是您可以为旧版浏览器做的最好的代码。
解决点击劫持的一种更现代的方法是使用 X-Frame-Options header。默认情况下,Spring Security 使用以下标头禁用 iframe 内的渲染页面:
X-Frame-Options: DENY
一些浏览器内置了过滤反射 XSS 攻击( reflected XSS attacks)的支持。这绝不是万无一失的,但确实有助于 XSS 保护。
过滤通常默认启用,因此添加标头通常只是确保启用它并指示浏览器在检测到 XSS 攻击时该怎么做。例如,过滤器可能会尝试以侵入性最小的方式更改内容以仍然呈现所有内容。有时,这种类型的替换本身可能会成为 XSS 漏洞(XSS vulnerability in itself)。相反,最好阻止内容而不是尝试修复它。By default Spring Security blocks the content using the following header:
X-XSS-Protection: 1; mode=block
Content Security Policy (CSP) 内容安全策略 (CSP) 是一种机制,Web 应用程序可以利用它来缓解内容注入漏洞,例如跨站点脚本(XSS)。CSP 是一种声明性策略,它为 Web 应用程序作者提供了一种工具来声明并最终通知客户端(用户代理)有关 Web 应用程序期望从中加载资源的源。
内容安全策略并非旨在解决所有内容注入漏洞。可以利用 CSP 来帮助减少内容注入攻击造成的危害。作为第一道防线,Web 应用程序作者应该验证他们的输入并编码他们的输出。
Web 应用程序可以通过在响应中包含以下 HTTP 标头之一来使用 CSP:
Content-Security-Policy
Content-Security-Policy-Report-Only
这些标头中的每一个都用作向客户端传递安全策略的机制。安全策略包含一组安全策略指令,每个指令负责声明特定资源表示的限制。
例如,Web 应用程序可以通过在响应中包含以下标头来声明它希望从特定的可信来源加载脚本:
Example 5. Content Security Policy Example
Content-Security-Policy: script-src https://trustedscripts.example.com
用户代理将阻止从 script-src 指令中声明的内容以外的其他来源加载脚本的尝试。此外,如果在安全策略中声明了 report-uri 指令,则用户代理将向声明的 URL 报告违规行为。
Example 6. Content Security Policy with report-uri
Content-Security-Policy: script-src https://trustedscripts.example.com; report-uri /csp-report-endpoint/
违规报告(Violation reports)是标准 JSON 结构,可以由 Web 应用程序自己的 API 或公共托管的 CSP 违规报告服务(例如 https://report-uri.com/)捕获。
Content-Security-Policy-Report-Only
标头为 Web 应用程序作者和管理员提供了监控安全策略的能力,此标头通常在为站点试验和/或开发安全策略时使用。当一个策略被认为是有效的,它可以通过使用 Content-Security-Policy 头域来强制执行。
给定以下响应标头,该策略声明可以从两个可能的来源之一加载脚本。
Example 7. Content Security Policy Report Only
Content-Security-Policy-Report-Only: script-src 'self' https://trustedscripts.example.com; report-uri /csp-report-endpoint/
如果站点违反此政策,尝试从 evil.com 加载脚本,用户代理将向 report-uri 指令指定的声明 URL 发送违规报告,但仍然允许加载违规资源。
将内容安全策略应用于 Web 应用程序通常是一项不平凡的工作。以下资源可为您的站点制定有效的安全策略提供进一步帮助。
An Introduction to Content Security Policy
CSP Guide - Mozilla Developer Network
Referrer Policy 是 Web 应用程序可以用来管理 referrer 字段的一种机制,其中包含用户所在的最后一页。
Spring Security 的做法是使用 Referrer Policy 标头,它提供了不同的策略:
Example 8. Referrer Policy Example
Referrer-Policy: same-origin
Referrer-Policy 响应标头指示浏览器让目标知道用户之前所在的源。
Feature Policy功能策略是一种机制,允许 Web 开发人员有选择地启用、禁用和修改浏览器中某些 API 和 Web 功能的行为。
Example 9. Feature Policy Example
Feature-Policy: geolocation 'self'
借助功能策略,开发人员可以选择加入一组“策略”,以便浏览器强制执行整个站点中使用的特定功能。这些政策限制了站点可以访问的 API 或修改浏览器对某些功能的默认行为。
Permissions Policy权限策略是一种机制,允许 Web 开发人员有选择地启用、禁用和修改浏览器中某些 API 和 Web 功能的行为。
Example 10. Permissions Policy Example
Content-Security-Policy-Report-Only: script-src 'self' https://trustedscripts.example.com; report-uri /csp-report-endpoint/
使用权限策略,开发人员可以选择加入一组“策略”,以便浏览器强制执行整个站点中使用的特定功能。这些政策限制了站点可以访问的 API 或修改浏览器对某些功能的默认行为。
Clear Site Data 清除站点数据是一种机制,当 HTTP 响应包含此标头时,可以通过该机制删除任何浏览器端数据(cookie、本地存储等):
Clear-Site-Data: "cache", "cookies", "storage", "executionContexts"
这是在注销时执行的一个很好的清理操作。
Spring Security 有一些机制可以方便地将更常见的安全标头添加到您的应用程序中。但是,它还提供了挂钩以启用添加自定义标头。
所有基于 HTTP 的通信,包括静态资源static resources,都应该使用 TLS进行保护。
作为一个框架,Spring Security 不处理 HTTP 连接,因此不直接提供对 HTTPS 的支持。但是,它确实提供了许多有助于 HTTPS 使用的功能。
当客户端使用 HTTP 时,可以将 Spring Security 配置为重定向到 HTTPS Servlet 和 WebFlux 环境。
一般直接使用 Nginx 来处理这个问题。
Spring Security 提供对 Strict Transport Security 的支持并默认启用它。
使用代理服务器时,确保您已正确配置应用程序非常重要。
例如,许多应用程序将有一个负载均衡器,通过将请求转发到位于 https://192.168.1:8080 的应用程序服务器来响应对 https://example.com/ 的请求。如果没有适当的配置,应用服务器将不知道负载均衡器的存在,并将请求视为客户端请求 https://192.168.1:8080。
要解决此问题,您可以使用 RFC 7239 指定正在使用负载平衡器。要让应用程序意识到这一点,您需要配置您的应用程序服务器以识别 X-Forwarded headers。例如,Tomcat 使用 RemoteIpValve,Jetty 使用 ForwardedRequestCustomizer。或者,Spring 用户可以利用 ForwardedHeaderFilter。
Spring Boot 用户可以使用 server.use-forward-headers
属性来配置应用程序。有关详细信息,请参阅 Spring Boot 文档。
Spring Security 提供与众多框架和 API 的集成。在本节中,我们将讨论并非特定于 Servlet 或反应式环境的通用集成。
部分摘要
Spring Security
加密模块
Spring Security Crypto
模块提供对对称加密、密钥生成和密码编码的支持。该代码作为核心模块的一部分分发,使用时不用依赖于任何其他 Spring Security(或 Spring)
代码。
Encryptors
类提供了构造对称加密器的工厂方法。使用此类,您可以创建 ByteEncryptors
以加密原始byte[]
形式的数据。您还可以构造 TextEncryptors
来加密文本字符串。加密器是线程安全的。
使用 Encryptors.stronger
工厂方法构造一个 BytesEncryptor
:
Example 1. BytesEncryptor
Encryptors.stronger("password", "salt");
“更强”的加密方法使用 256 位 AES 加密和伽罗瓦计数器模式 (GCM) 创建加密器。它使用 PKCS #5 的 PBKDF2(基于密码的密钥派生函数 #2)派生密钥。此方法需要 Java 6。用于生成 SecretKey 的密码应保存在安全的地方,不得共享。如果您的加密数据被泄露,盐用于防止对密钥的字典攻击。还应用了一个 16-byte 的随机初始化向量,因此每条加密消息都是唯一的。
提供的盐应该是十六进制编码的字符串形式,是随机的,并且长度至少为 8 个字节。这样的盐可以使用 KeyGenerator 生成:
Example 2. Generating a key
// generates a random 8-byte salt that is then hex-encodedString salt = KeyGenerators.string().generateKey();
用户还可以使用标准加密方法,即密码块链接 (CBC) 模式下的 256 位 AES。此模式未经身份验证,不提供任何关于数据真实性的保证。对于更安全的替代方案,用户应该更喜欢 Encryptors.stronger
。
下面是spring
自带的测试例子:
@Testpublic void stronger() throws Exception {CryptoAssumptions.assumeGCMJCE();BytesEncryptor encryptor = Encryptors.stronger("password", "5c0744940b5c369b");byte[] result = encryptor.encrypt("text".getBytes("UTF-8"));assertThat(result).isNotNull();assertThat(new String(result).equals("text")).isFalse();assertThat(new String(encryptor.decrypt(result))).isEqualTo("text");assertThat(new String(result)).isNotEqualTo(new String(encryptor.encrypt("text".getBytes())));}
text 随机变化的,安全性更高
使用 Encryptors.text
工厂方法构造一个标准的 TextEncryptor
:
Example 3. TextEncryptor
Encryptors.text("password", "salt");
TextEncryptor
使用标准的 BytesEncryptor
来加密文本数据。加密结果以十六进制编码字符串的形式返回,以便于存储在文件系统或数据库中。
queryableText 固定不变的
使用 Encryptors.queryableText
工厂方法构造一个queryable TextEncryptor
:
Example 4. Queryable TextEncryptor
Encryptors.queryableText("password", "salt");
queryable TextEncryptor
和标准的 TextEncryptor
之间的区别与初始化向量 (iv) 处理有关。queryable TextEncryptor#encrypt
操作中使用的 iv
是共享的或恒定的,并且不是随机生成的。这意味着多次加密的相同文本将始终产生相同的加密结果。这不太安全,但对于需要查询的加密数据是必需的。queryable encrypted text
的一个应用场景是 OAuth apiKey
。
下面是 spring 自带的测试例子:
@Testpublic void text() {CryptoAssumptions.assumeCBCJCE();TextEncryptor encryptor = Encryptors.text("password", "5c0744940b5c369b");String result = encryptor.encrypt("text");assertThat(result).isNotNull();assertThat(result.equals("text")).isFalse();assertThat(encryptor.decrypt(result)).isEqualTo("text");assertThat(result.equals(encryptor.encrypt("text"))).isFalse();}@Testpublic void queryableText() {CryptoAssumptions.assumeCBCJCE();TextEncryptor encryptor = Encryptors.queryableText("password", "5c0744940b5c369b");String result = encryptor.encrypt("text");assertThat(result).isNotNull();assertThat(result.equals("text")).isFalse();assertThat(encryptor.decrypt(result)).isEqualTo("text");assertThat(result.equals(encryptor.encrypt("text"))).isTrue();}
KeyGenerators
类提供了许多方便的工厂方法来构造不同类型的密钥生成器。使用这个类,您可以创建一个 BytesKeyGenerator
来生成 byte[]
键。您还可以构造一个 StringKeyGenerator
来生成字符串键。 KeyGenerators
是线程安全的。
随机的
使用 KeyGenerators.secureRandom
工厂方法生成由 SecureRandom
实例支持的 BytesKeyGenerator
:
Example 5. BytesKeyGenerator
BytesKeyGenerator generator = KeyGenerators.secureRandom();byte[] key = generator.generateKey();
默认密钥长度为 8 个字节。还有一个 KeyGenerators.secureRandom
变体可以控制密钥长度:
KeyGenerators.secureRandom(16);
固定的,用途不大
使用 KeyGenerators.shared
工厂方法构造一个 BytesKeyGenerator
,它在每次调用时总是返回相同的键:
KeyGenerators.shared(16);
使用 KeyGenerators.string
工厂方法构造一个 8 字节的 SecureRandom KeyGenerator
,它将每个密钥十六进制编码为字符串:
KeyGenerators.string();
其实就是将一个byte[]
转换成string
//Hex.encode 将 byte[]转换成string 。return new String(Hex.encode(this.keyGenerator.generateKey()));//Hex.decode 将 CharSequence转换成转换成byte[]@Testpublic void decode() {assertThat(Hex.decode("41424344")).isEqualTo(new byte[] { (byte) 'A', (byte) 'B', (byte) 'C', (byte) 'D' });}
spring-security-crypto
模块的密码包提供了对密码编码的支持。 PasswordEncoder
是中央服务接口,具有以下签名:
public interface PasswordEncoder {String encode(String rawPassword);boolean matches(String rawPassword, String encodedPassword);}
如果 rawPassword
编码后等于 encodedPassword
,matches
方法返回 true
。此方法旨在支持基于密码的身份验证方案。
BCryptPasswordEncoder
实现使用广泛支持的bcrypt
算法来散列密码。Bcrypt
使用随机的 16 字节盐值,是一种故意缓慢的算法,以阻止密码破解者。它所做的工作量可以使用“强度”参数进行调整,该参数取值从 4 到 31。值越高,计算哈希所需的工作就越多。默认值为 10。您可以在部署的系统中更改此值,而不会影响现有密码,因为该值也存储在编码散列中。
Example 9. BCryptPasswordEncoder
// Create an encoder with strength 16BCryptPasswordEncoder encoder = new BCryptPasswordEncoder(16);String result = encoder.encode("myPassword");assertTrue(encoder.matches("myPassword", result));
Pbkdf2PasswordEncoder
实现使用 PBKDF2
算法对密码进行哈希处理。为了击败密码破解,PBKDF2
是一种故意缓慢的算法,应该调整为大约需要 0.5 秒来验证系统上的密码。
Example 10. Pbkdf2PasswordEncoder
// Create an encoder with all the defaultsPbkdf2PasswordEncoder encoder = new Pbkdf2PasswordEncoder();String result = encoder.encode("myPassword");assertTrue(encoder.matches("myPassword", result));
Spring Security 提供 Spring Data 集成,允许在查询中引用当前用户。将用户包含在查询中以支持分页结果不仅有用而且很有必要,因为过滤后的结果不会很多,以致影响性能。
要使用此支持,请添加 org.springframework.security:spring-security-data 依赖项并提供 SecurityEvaluationContextExtension 类型的 bean。在 Java 配置中,这看起来像:
@Beanpublic SecurityEvaluationContextExtension securityEvaluationContextExtension() {return new SecurityEvaluationContextExtension();}
现在可以在您的查询中使用 Spring Security。例如:
@Repositorypublic interface MessageRepository extends PagingAndSortingRepository<Message,Long> {@Query("select m from Message m where m.to.id = ?#{ principal?.id }")Page<Message> findInbox(Pageable pageable);}
这将检查 Authentication.getPrincipal().getId()
是否等于消息的接收者。请注意,此示例假定您已将主体自定义为具有 id 属性的对象。通过公开 SecurityEvaluationContextExtension bean,所有通用安全表达式都可以在查询中使用。
思考,这样做的好的,就简化了很多,但是有两点权衡的地方:
1、Spring 推荐在 Principal 不保存对象,而是使用名字。 通过名字再来查询出用户的其他信息,我正在想是不是按照这个来做。
2、是否使用 Spring Data,当前使用的是 Mybatis,使用 Spring Data 会不会有问题。
在大多数环境中,SecurityContext
被保存在每个线程中,这意味着当在新线程上完成工作时,SecurityContext
会丢失。Spring Security 提供了一些基础设施来帮助用户更轻松地完成此操作。Spring Security 为在多线程环境中使用 Spring Security 提供了最基础的抽象类。事实上,这就是 Spring Security 与 AsyncContext.start(Runnable) 和 Spring MVC Async Integration 集成的基础。
Spring Security
支持并发的最基本的类是 DelegatingSecurityContextRunnable
。它包装了一个委托 Runnable,以便使用指定的SecurityContext
初始化 SecurityContextHolder
,来实现这个delegate
。然后它调用delegate
的 Runnable
执行之后并清除 SecurityContextHolder
。
DelegatingSecurityContextRunnable
看起来像这样:
public void run() {try {SecurityContextHolder.setContext(securityContext);delegate.run();} finally {SecurityContextHolder.clearContext();}}
虽然非常简单,但它可以无缝地将 SecurityContext 从一个线程传输到另一个线程。这很重要,因为在大多数情况下,SecurityContextHolder 在每个线程的基础上起作用。例如,您可能已经使用 Spring Security 的 global-method-security 支持来保护您的一项服务。您现在可以轻松地将当前线程的 SecurityContext 传输到调用安全服务的线程。您可以在下面找到如何执行此操作的示例:
Runnable originalRunnable = new Runnable() {public void run() {// invoke secured service}};SecurityContext context = SecurityContextHolder.getContext();DelegatingSecurityContextRunnable wrappedRunnable =new DelegatingSecurityContextRunnable(originalRunnable, context);new Thread(wrappedRunnable).start();
上面的代码执行以下步骤:
SecurityContextHolder
中获取我们希望使用的 SecurityContext
并初始化 DelegatingSecurityContextRunnable
DelegatingSecurityContextRunnable
创建线程由于使用 SecurityContextHolder
中的 SecurityContext
创建 DelegatingSecurityContextRunnable
是很常见的,因此它有一个快捷构造函数。下面的代码与上面的代码相同:
Runnable originalRunnable = new Runnable() {public void run() {// invoke secured service}};DelegatingSecurityContextRunnable wrappedRunnable =new DelegatingSecurityContextRunnable(originalRunnable);new Thread(wrappedRunnable).start();
我们的代码使用起来很简单,但它仍然需要我们使用 Spring Security
的知识。在下一节中,我们将看看如何利用 DelegatingSecurityContextExecutor
来隐藏我们正在使用 Spring Security
的事实。
在上一节中,我们发现使用 DelegatingSecurityContextRunnable
很容易,但它并不理想,因为我们必须了解 Spring Security
才能使用它。让我们看看 DelegatingSecurityContextExecutor
如何保护我们的代码免受我们正在使用 Spring Security
的任何了解。
DelegatingSecurityContextExecutor
的设计与 DelegatingSecurityContextRunnable
的设计非常相似,只是它接受委托 Executor
而不是委托 Runnable
。您可以在下面看到如何使用它的示例:
SecurityContext context = SecurityContextHolder.createEmptyContext();Authentication authentication =new UsernamePasswordAuthenticationToken("user","doesnotmatter", AuthorityUtils.createAuthorityList("ROLE_USER"));context.setAuthentication(authentication);SimpleAsyncTaskExecutor delegateExecutor =new SimpleAsyncTaskExecutor();DelegatingSecurityContextExecutor executor =new DelegatingSecurityContextExecutor(delegateExecutor, context);Runnable originalRunnable = new Runnable() {public void run() {// invoke secured service}};executor.execute(originalRunnable);
该代码执行以下步骤:
DelegatingSecurityContextExecutor
的 SecurityContext
。请注意,在此示例中,我们只是手动创建了 SecurityContext
。然而,我们在哪里或如何获得 SecurityContext
并不重要(即,如果我们愿意,我们可以从 SecurityContextHolder
获得它)。Runnables
的 delegateExecutor
。DelegatingSecurityContextExecutor
,它负责用 DelegatingSecurityContextRunnable
包装任何传入执行方法的 Runnable
。然后它将包装好的 Runnable
传递给 delegateExecutor
。在这种情况下,相同的 SecurityContext
将用于提交给我们的 DelegatingSecurityContextExecutor
的每个 Runnable
。如果我们正在运行需要由具有提升权限的用户运行的后台任务,这很好。Spring Security
的任何知识?" 而不是在我们自己的代码中创建 SecurityContext
和 DelegatingSecurityContextExecutor
,我们可以注入一个已经初始化的 DelegatingSecurityContextExecutor
实例。@Autowiredprivate Executor executor; // becomes an instance of our DelegatingSecurityContextExecutorpublic void submitRunnable() {Runnable originalRunnable = new Runnable() {public void run() {// invoke secured service}};executor.execute(originalRunnable);}
现在我们的代码不知道 SecurityContext 正在传播到 Thread,然后运行 originalRunnable,然后清除 SecurityContextHolder。在此示例中,使用同一用户运行每个线程。如果我们想在调用 executor.execute(Runnable) 时使用来自 SecurityContextHolder 的用户(即当前登录的用户)来处理 originalRunnable 怎么办?这可以通过从我们的 DelegatingSecurityContextExecutor 构造函数中删除 SecurityContext 参数来完成。例如:
SimpleAsyncTaskExecutor delegateExecutor = new SimpleAsyncTaskExecutor();DelegatingSecurityContextExecutor executor =new DelegatingSecurityContextExecutor(delegateExecutor);
现在,无论何时执行 executor.execute(Runnable),SecurityContext 首先由 SecurityContextHolder 获取,然后 SecurityContext 用于创建我们的 DelegatingSecurityContextRunnable。
这意味着我们正在使用用于调用 executor.execute(Runnable) 代码的同一用户运行我们的 Runnable。
有关与 Java 并发 API 和 Spring Task 抽象的其他集成,请参阅 Javadoc。一旦你理解了前面的代码,它们就很容易解释了。
DelegatingSecurityContextCallable
DelegatingSecurityContextExecutor
DelegatingSecurityContextExecutorService
DelegatingSecurityContextRunnable
DelegatingSecurityContextScheduledExecutorService
DelegatingSecurityContextSchedulingTaskExecutor
DelegatingSecurityContextAsyncTaskExecutor
DelegatingSecurityContextTaskExecutor
DelegatingSecurityContextTaskScheduler
Spring Security 为持久化相关类提供 Jackson 支持。这可以提高在处理分布式会话(即会话复制、Spring Session 等)时序列化 Spring Security 相关类的性能。
要使用它,请将 SecurityJackson2Modules.getModules(ClassLoader) 注册到 ObjectMapper (jackson-databind):
ObjectMapper mapper = new ObjectMapper();ClassLoader loader = getClass().getClassLoader();List<Module> modules = SecurityJackson2Modules.getModules(loader);mapper.registerModules(modules);// ... use ObjectMapper as normally ...SecurityContext context = new SecurityContextImpl();// ...String json = mapper.writeValueAsString(context);
以下 Spring Security 模块提供 Jackson 支持:
CoreJackson2Module
)WebJackson2Module
, WebServletJackson2Module
, WebServerJackson2Module
)OAuth2ClientJackson2Module
)CasJackson2Module
)如果您需要支持其他语言环境,您需要了解的所有内容都包含在本节中。
所有异常消息都可以本地化,包括与身份验证失败和访问被拒绝(授权失败)相关的消息。专注于开发人员或系统开发人员的异常和日志消息(包括不正确的属性、违反接口契约、使用不正确的构造函数、启动时间验证、调试级日志记录)没有本地化,而是在 Spring Security 的代码中用英语硬编码。
在 spring-security-core-xx.jar 中发布,您会发现一个 org.springframework.security 包,该包又包含一个 messages.properties 文件,以及一些常用语言的本地化版本。这应该由您的 ApplicationContext 引用,因为 Spring Security 类实现 Spring 的 MessageSourceAware 接口并期望消息解析器在应用程序上下文启动时注入依赖项。通常,您需要做的就是在应用程序上下文中注册一个 bean 来引用消息。一个例子如下所示:
<bean id="messageSource"class="org.springframework.context.support.ReloadableResourceBundleMessageSource"><property name="basename" value="classpath:org/springframework/security/messages"/></bean>
messages.properties 根据标准资源包命名,并表示 Spring Security 消息支持的默认语言。这个默认文件是英文的。
如果您希望自定义 messages.properties 文件,或支持其他语言,您应该复制文件,相应地重命名它,然后在上面的 bean 定义中注册它。这个文件里面没有大量的消息键,因此本地化不应被视为一项重大举措。如果您执行此文件的本地化,请考虑通过记录 JIRA 任务并附加您适当命名的本地化版本的 messages.properties 来与社区分享您的工作。
Spring Security 依赖于 Spring 的本地化支持来实际查找适当的消息。为了让它工作,你必须确保来自传入请求的语言环境存储在 Spring 的 org.springframework.context.i18n.LocaleContextHolder 中。
Spring MVC 的 DispatcherServlet 会自动为您的应用程序执行此操作,但由于在此之前调用了 Spring Security 的过滤器,因此需要在调用过滤器之前设置 LocaleContextHolder 以包含正确的 Locale。
您可以自己在过滤器中执行此操作(必须在 web.xml 中的 Spring Security 过滤器之前)或者你可以使用 Spring 的 RequestContextFilter。有关使用 Spring 本地化的更多详细信息,请参阅 Spring Framework 文档。
“contacts”示例应用程序设置为使用本地化消息。