Spring Security怎么防住各种漏洞?

713 阅读25分钟

前言

Spring Security的先天优势是安全, 只要导入了 Spring security 依赖, 就已经防备住了很多的漏洞, 这是 Shiro和sa-token无法超越的优势, 但同时 Spring Security 也带来了"复杂", 只能说各有优点吧

本章也很无聊, 不过有点干货, 需要注意

漏洞保护

CSRF攻击与防御

csrf是什么?

跨站点请求伪造(Cross Site Request Forgery)又被称作 CSRF,攻击者诱导受害者进入第三方网站,在第三方网站中,向被攻击网站发送跨站请求。利用受害者在被攻击网站已经获取的注册凭证,绕过后台的用户验证,达到冒充用户对被攻击的网站执行某项操作的目的。

通俗易懂:

犯罪嫌疑人创造了天地银行的转账地址, 转账地址包含: 转账目标, 转账金额

转账目标: 犯罪嫌疑人自己的支付宝账号

转账金额: 9999999RMB

前提:

  1. 受害者事先已经登录过天地银行, 并且登录信息还没失效
  2. 天地银行未做任何csrf攻击防护

开始:

这个地址给不同的用户有不同的效果:

  • 给犯罪嫌疑人使用: 犯罪嫌疑人天地银行将转账 9999999RMB 给 犯罪嫌疑人自己的支付宝账户
  • 给受害者使用: 受害者天地银行将转账 9999999RMB 给 犯罪嫌疑人的支付宝账户, 通常受害者还不知情

说白了就是诈骗份子给了个账户, 叫跟受害者说, 这是妹子微信二维码, 你扫描下, 然后钱就被转走了

诈骗份子根本就不知道受害者的 cookie 是什么, 就创建了个"二维码", 受害者自己就把钱转给诈骗份子了

image-20221128160041164

攻击演示可以看spring_security分支的

image-20221128160720565

这个项目

地址在这里:

springcloud_study_parent: 微服务学习 - Gitee.com

如何防护?

文字版 csrf 过程

  1. 用户正常认证天地银行, 用户本地存放有效 cookie
  2. 攻击者伪造天地银行请求地址, 放置在第三方网站地址
  3. 攻击者诱惑用户访问他伪造好的请求地址
  4. 用户访问恶意地址, cookie 中的 sessionid 刚好在服务端找到对应 session
  5. 验证通过, 钱转给攻击者

防护方式:

  1. 用户不被诱惑(用户能做到)
  2. sessionid 不存放在 cookie 上, 而是放在另外的位置, 等到验证时 拿出来验证(程序员能做到)
  3. 第三方地址访问 cookie 的时候, 不给 cookie(浏览器才能做到)
  4. 转钱的时候, 需要手机验证码(验证码方案)
  5. 网站这么重要的转钱地址, 怎么可以伪造呢? 不应该这么简单, 需要有效验证参数(同上验证码方案)
  6. 转账地址怎么能这么简单, 不应该再次数据密码么? (同上验证码方案)

无论是那种方式, 前提都是请求方法幂等, 即HTTP请求中的GET, HEAD, OPTIONS, TRACE方法不应该改变用户的状态

一个HTTP 方法是幂等的,指的是同样的请求被执行一次与连续执行多次的效果是一样的,服务器的状态也是一样的。 换句话说就是,幂等方法不应该具有副作用(统计用途除外)。 在正确实现的条件下, GET , HEAD , PUT 和 DELETE 等方法都是幂等的,而 POST 方法不是

验证码

给关键的地址添加验证码功能

比如:

  • 转账
  • 登录
  • 注册

优点: 特点简单

缺点: 不够通用; 用户体验不好

同源检测

借助

Origin Header
Referer Header

这两个Header在浏览器发起请求时,大多数情况会自动带上,并且不能由前端修改,服务器接收到后可以根据这两个Header确定来源的域名;

特殊情况: 如果OriginReferer都不存在,建议直接进行阻止,特别是如果您没有使用随机 CSRF Token 作为第二次检查。

另外,CSRF大多数情况下来自第三方域名,但并不能排除本域发起。如果攻击者有权限在本域发布评论(含链接、图片等),那么它可以直接在本域发起攻击,这种情况下同源策略无法达到防护的作用。

犯罪嫌疑人借助天地银行地址伪造针对天地银行的请求, 然后借助天地银行网页的评论(含链接、图片等), 引导受害者访问伪造的地址

**综上所述:**同源验证是一个相对简单的防范方法,能够防范绝大多数的CSRF攻击。但这并不是万无一失的,对于安全性要求较高,或者有较多用户输入内容的网站,我们就要对关键的接口做额外的防护措施。

优点: 比较简单

缺点:

  • 不太全面
  • 防护力度不行
  • 可能有些合法的用户根本就没有OriginReferer
  • 302重定向没有Origin
  • Referer各个浏览器自己实现的可能存在漏洞, 说白点犯罪嫌疑人自己也能实现一个浏览器, 所以也不是很安全的样子

另外在以下情况下Referer没有或者不可信:

  1. IE6、7下使用window.location.href=url进行界面的跳转,会丢失Referer
  2. IE6、7下使用window.open,也会缺失Referer
  3. HTTPS页面跳转到HTTP页面,所有浏览器Referer都丢失。
  4. 点击Flash上到达另外一个网站的时候,Referer的情况就比较杂乱,不太可信。

因此,服务器的策略是优先判断 Origin,如果请求头中没有包含 Origin 属性,再根据实际情况判断是否使用 Referer 值,从而增加攻击难度。

添加token

不要用 cookie 作为身份认证的工具了, 直接使用 token

这种方式比 cookie 更加的安全些, 需要找到除 cookie 的地方存放 token (比如前端网页 表单, 比如localstore等方式), 然后在访问请求时, 浏览器不会自动带上 token, 这样也就不会出现问题

只有在需要token时, 由程序员主动读取 token 并添加到 header 中, 传递给后端进行进一步处理

单机情况下, 服务端的 token 可能还是存放在 session 中, 只不过客户端的 cookie 内存放的 sessionid 改了个名字叫 token 放到 cookie 之外了

但是这种方式会带来别的问题, 比如 分布式情况下, 服务端session存放的 token 存在问题

nginx 反向代理的情况下, 存放到单机的 session 下肯定不行, 所以此时需要用上 redis

另外由于对 session 的存储存在性能和比较复杂的问题, 很多网站的开发者不再使用 session 作为存储单位, 而是通过计算获得新的 token 然后将 token 存放在 redis

这里生成 token 的方式一般是 用户id + 时间戳 + 随机数 然后通过对称加密方式对其进行加密

token 解密之后, 服务端可以拿到 上面的数据 进行 token 有效性验证, 将UserID与当前登录的UserID进行比较,并将时间戳与当前时间进行比较。

Token是一个比较有效的CSRF防护方法,只要页面没有XSS漏洞泄露Token,那么接口的CSRF攻击就无法成功。

但是此方法的实现比较复杂,需要给每一个页面都写入Token(前端无法使用纯静态页面),每一个FormAjax请求都携带这个Token,后端对每一个接口都进行校验,并保证页面Token及请求Token一致。这就使得这个防护策略不能在通用的拦截上统一拦截处理,而需要每一个页面和接口都添加对应的输出和校验。这种方法工作量巨大,且有可能遗漏。

双重Cookie验证

在会话中存储CSRF Token比较繁琐,而且不能在通用的拦截上统一处理所有的接口。

那么另一种防御措施是使用双重提交Cookie。利用CSRF攻击不能获取到用户Cookie的特点,我们可以要求Ajax和表单请求携带一个Cookie中的值。

利用诈骗份子根本就不知道受害者 cookie 内容的情况, 在 cookie 中存放后端传递给cookie的密文, 最重要的来了, 在请求敏感数据前, cookie 中的内容取出来, 当作参数提交给后端验证数据是否相同

双重Cookie采用以下流程:

  • 在用户访问网站页面时,向请求域名注入一个Cookie,内容为随机字符串(例如csrfcookie=v8g9e4ksfhw)。
  • 在前端向后端发起请求时,取出Cookie,并添加到URL的参数中(接上例POST https://www.a.com/comment?csrfcookie=v8g9e4ksfhw)。
  • 后端接口验证Cookie中的字段与URL参数中的字段是否一致,不一致则拒绝。

由于任何跨域都会导致前端无法获取Cookie中的字段(包括子域名之间),于是发生了如下情况:

  • 如果用户访问的网站为www.a.com,而后端的api域名为api.a.com。那么在www.a.com下,前端拿不到api.a.comCookie,也就无法完成双重Cookie认证。
  • 于是这个认证Cookie必须被种在a.com下,这样每个子域都可以访问。
  • 任何一个子域都可以修改a.com下的Cookie
  • 某个子域名存在漏洞被XSS攻击(例如upload.a.com)。虽然这个子域下并没有什么值得窃取的信息。但攻击者修改了a.com下的Cookie
  • 攻击者可以直接使用自己配置的Cookie,对XSS中招的用户再向www.a.com下,发起CSRF攻击。

总结:

用双重Cookie防御CSRF的优点:

  • 无需使用Session,适用面更广,易于实施。
  • Token储存于客户端中,不会给服务器带来压力。
  • 相对于Token,实施成本更低,可以在前后端统一拦截校验,而不需要一个个接口和页面添加。

缺点:

  • Cookie中增加了额外的字段。
  • 如果有其他漏洞(例如XSS),攻击者可以注入Cookie,那么该防御方式失效。
  • 难以做到子域名的隔离。
  • 为了确保Cookie传输安全,采用这种防御方式的最好确保用整站HTTPS的方式,如果还没切HTTPS的使用这种方式也会有风险。

Samesite Cookie属性

Google起草了一份草案来改进HTTP协议,那就是为Set-Cookie响应头新增Samesite属性,它用来标明这个 Cookie是个“同站 Cookie”,同站Cookie只能作为第一方Cookie,不能作为第三方CookieSamesite 有两个属性值,分别是 StrictLax,下面分别讲解:

Samesite=Strict

这种称为严格模式. 如果淘宝配置了上面这种严格模式, 那么在百度搜索到的淘宝地址, 即便用户在前面已经登录过淘宝, 从百度点击进入的淘宝都不会有登录状态, 需要用户重新登录

Samesite=Lax

异步请求或者 post 请求时, 不会有 cookie

如果SamesiteCookie被设置为Strict,浏览器在任何跨域请求中都不会携带Cookie,新标签重新打开也不携带,所以说CSRF攻击基本没有机会。

但是跳转子域名或者是新标签重新打开刚登陆的网站,之前的Cookie都不会存在。尤其是有登录的网站,那么我们新打开一个标签进入,或者跳转到子域名的网站,都需要重新登录。对于用户来讲,可能体验不会很好。

如果SamesiteCookie被设置为Lax,那么其他网站通过页面跳转过来的时候可以使用Cookie,可以保障外域连接打开页面时用户的登录状态。但相应的,其安全性也比较低。

另外一个问题是Samesite的兼容性不是很好,现阶段除了从新版ChromeFirefox支持以外,Safari以及iOS Safari都还不支持,现阶段看来暂时还不能普及。

而且,SamesiteCookie目前有一个致命的缺陷:**不支持子域。**例如,种在topic.a.com下的Cookie,并不能使用a.com下种植的SamesiteCookie。这就导致了当我们网站有多个子域名时,不能使用SamesiteCookie在主域名存储用户登录信息。每个子域名都需要用户重新登录一次。

总之,SamesiteCookie是一个可能替代同源验证的方案,但目前还并不成熟,其应用场景有待观望。

总结

后端接口防止XSRF漏洞

  • 严格管理所有的上传接口,防止任何预期之外的上传内容(例如HTML)。
  • 添加Header X-Content-Type-Options: nosniff 防止黑客上传HTML内容的资源(例如图片)被解析为网页。
  • 对于用户上传的图片,进行转存或者校验。不要直接使用用户填写的图片链接。

简单总结一下上文的防护策略:

  • CSRF自动防御策略:同源检测(OriginReferer 验证)。
  • CSRF主动防御措施:Token验证 或者 双重Cookie验证 以及配合Samesite Cookie
  • 保证页面的幂等性,后端接口不要在GET页面中做用户操作。

为了更好的防御CSRF,最佳实践应该是结合上面总结的防御措施方式中的优缺点来综合考虑,结合当前Web应用程序自身的情况做合适的选择,才能更好的预防CSRF的发生。

落地实现

令牌同步模式

具体的操作方式就是在每一个HTTP请求中,除了默认自动携带的Cookie参数之外,再额外提供一个安全的、随机生成的字符串,我们称之为CSRF令牌。这个CSRF令牌由服务端生成,生成后在HttpSession中保存一份。当前端请求到达后,将请求携带的CSRF令牌信息和服务端中保存的令牌进行对比,如果两者不相等,则拒绝掉该HTTP请求。 考虑到会有一些外部站点链接到我们的网站,所以我们要求请求是幂等的,这样对于GET、HEAD、OPTIONS、TRACE等方法就没有必要使用CSRF令牌了,强行使用可能会导致令牌泄漏。

注意这句话: 对于GET、HEAD、OPTIONS、TRACE等方法就没有必要使用CSRF令牌了,强行使用可能会导致令牌泄漏

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-devtools</artifactId>
    <scope>provided</scope>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <scope>provided</scope>
</dependency>
server:
  port: 8080
spring:
  application:
    name: csrf-spring-security
  security:
    user:
      name: zhazha
      password: 123456
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.ResponseBody;

@Controller
@Slf4j
public class HelloController {
	
	@PostMapping("/hello")
	@ResponseBody
	public String hello() {
		return "hello, world";
	}
	
	@GetMapping("/index.html")
	public String index() {
		return "index";
	}
	
}

index.html

<!DOCTYPE html>
<html lang="en">
<head>
	<meta charset="UTF-8">
	<title>Title</title>
</head>
<body>
	<form action="/hello" method="post">
		<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}">
		<input type="submit" value="hello">
	</form>
</body>
</html>

根据前面所讲,请求方法要幂等,所以在 Spring Security中,默认不会对GET、HEAD、OPTIONS 以及 TRACE请求进行CSRF令牌校验,这也是/hello接口是Post 请求的原因。

image-20221128210938578

如果此时, 提交表单 form , 并且, 带有 _csrf 参数的 input 标签被删除, 在提交表单时, 就会报错

image-20221128211200170

上面的方法使用的是 form 表单的方式, 但如果使用的是 Ajax 呢?

明显上面这种方式是不行的, 没有地方存储 _csrf 的数值

cookie 中也没有匹配的数值:

image-20221128211908899

image-20221128211928646

针对Ajax
@GetMapping("/login.html")
public String login() {
   return "login";
}
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.web.csrf.CookieCsrfTokenRepository;

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
   
   @Override
   protected void configure(HttpSecurity http) throws Exception {
      http
            .authorizeRequests()
            .anyRequest().authenticated()
            .and()
            .formLogin()
            .loginProcessingUrl("/login")
            .successHandler((request, response, authentication) -> {
               response.getWriter().write("login success");
            })
            .permitAll()
            .and()
            .csrf()
            .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse());
            
   }
}

关键代码在这里:

image-20221128212354090

需要注意的是,这里将csrfTokenRepository配置为CookieCsrfTokenRepository,并设置httpOnly 属性为false,否则前端将无法读取到Cookie中的CSRF令牌。

login.html

<!DOCTYPE html>
<html lang="en">
<head>
   <meta charset="UTF-8">
   <title>Title</title>
   <script src="https://cdn.staticfile.org/jquery/3.4.0/jquery.min.js"></script>
   <script src="https://cdn.staticfile.org/jquery-cookie/1.4.1/jquery.cookie.min.js"></script>
</head>
<body>
<div>
   <input type="text" id="username">
   <input type="password" id="password">
   <input type="button" value="登录" id="loginBtn">
</div>
<script>
   $("#loginBtn").click(function () {
      let csrf = $.cookie("XSRF-TOKEN");
      $.post('/login.html', {
         username: $('#username').val(),
         password: $("#password").val(),
         _csrf: csrf
      }, function (data) {
         alert(data);
      })
   });
</script>
</body>
</html>

启动项目后访问/login.html页面

image-20221128214549890

前面存放在 表单的 _csrf 现在转移到 sessionXSRF-TOKEN中了

image-20221128214647802

登录成功

image-20221128214721203

token放在 cookie 中不会造成 csrf 攻击, 因为 攻击者 根本不知道 cookie 中保存的内容, 还有在登录的时候, 请求参数中还得带上 _csrf 参数, 否则请求还会失败

这两个案例, 前面一个将 _csrf token 保存到 HttpSession 中, 另一个将 _csrf 保存到 cookie

SameSite防御方案

SameSite是最近几年才出现的一个解决方案,是 Chrome 51开始支持的一个属性,用来防止 CSRF攻击和用户追踪。 这种方式通过在Cookie上指定SameSite属性,要求浏览器从外部站点发送请求时,不应携带Cookie信息,进而防止CSRF 攻击。添加了SameSite属性的响应头类似下面这样:

image-20221128215612739

SameSite属性值有三种:

  • Strict: 只有同一站点发送的请求才包含Cookie信息,不同站点发送的请求将不会包含Cookie信息。
  • Lax: 同一站点发送的请求或者导航到目标地址的GET 请求会自动包含Cookie信息,否则不包含Cookie信息。
  • None: Cookie将在所有上下文中发送,即允许跨域发送。

Spring Security不支持 SameSite , 但是 Spring Session 支持

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.session</groupId>
    <artifactId>spring-session-data-redis</artifactId>
    <version>2.7.0</version>
</dependency>
redis:
  host: 192.168.0.155
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.session.web.http.CookieSerializer;
import org.springframework.session.web.http.DefaultCookieSerializer;

@Configuration
public class SpringSessionConfig {
   
   @Bean
   public CookieSerializer httpSessionIdResolver() {
      DefaultCookieSerializer cookieSerializer = new DefaultCookieSerializer();
      cookieSerializer.setSameSite("strict");
      return cookieSerializer;
   }
   
}

image-20221128220802945

会话超时

CSRF令牌生成后,往往都保存在 HttpSession中,但是HttpSession可能会因为超时而失效,导致前端请求传来的CSRF令牌无法得到验证,解决这一问题有如下几种方式:

  • 最佳方案是在表单提交时,通过js获取CSRF令牌,然后将获取到的CSRF令牌跟随表单一起提交。
  • 当会话快要过期时,前端通过js提醒用户刷新页面,以给会话“续命”。
  • 将令牌存储在Cookie 中而不是HttpSession 中。

登录和注销

为了保护用户的敏感信息,登录请求和注销请求需要注意CSRF 攻击防护。

文件上传

文件上传请求比较特殊,因此需要额外注意。如果将CSRF放在请求体中,就会面临一个“鸡和蛋”的问题。服务端需要先验证CSRF令牌以确认请求是否合法,而这也意味需要先读取请求体以获取CSRF令牌,这就陷入一个死循环了。 一般来说,将CSRF 防御与multipart/form-data一起使用,我们有两种不同的策略:

  • 将CSRF令牌放在请求体中。
  • 将CSRF令牌放在请求URL中。

CSRF令牌放在请求体中,意味着任何人都可以向我们的服务器上传临时文件,但是只有CSRF令牌验证通过的用户,才能真正提交一个文件,这也是目前推荐的方案,因为上传临时文件对服务器的影响可以忽略不计。如果不希望未经授权的用户上传临时文件,那么可以将CSRF令牌放在请求URL地址中,但是这种方式可能带来令牌泄漏的风险。

HTTP响应头处理

HTTP响应头中的许多属性都可以用来提高Web安全。本节我们来看一下Spring Security中提供显式支持的一些HTTP响应头。 Spring Security默认情况下,显式支持的HTTP响应头主要有如下几种:

* Cache-Control: no-cache, no-store, max-age=0, must-revalidate
* Pragma: no-cache
* Expires: 0
* X-Content-Type-Options: nosniff
* Strict-Transport-Security: max-age=31536000 ; includeSubDomains
* X-Frame-Options: DENY
* X-XSS-Protection: 1; mode=block
复制代码

这里一共有七个响应头,前三个都是与缓存相关的,因此一共可以分为五大类。 这些响应头都是在HeaderWriterFilter 中添加的,默认情况下,该过滤器就会添加到SpringSecurity过滤器链中,HeaderWriterFilter是通过HeadersConfigurer进行配置的,我们来看一下HeadersConfigurer 中几个关键的方法:

private List<HeaderWriter> getHeaderWriters() {
   List<HeaderWriter> writers = new ArrayList<>();
   addIfNotNull(writers, contentTypeOptions.writer);
   addIfNotNull(writers, xssProtection.writer);
   addIfNotNull(writers, cacheControl.writer);
   addIfNotNull(writers, hsts.writer);
   addIfNotNull(writers, frameOptions.writer);
   addIfNotNull(writers, hpkp.writer);
   addIfNotNull(writers, contentSecurityPolicy.writer);
   addIfNotNull(writers, referrerPolicy.writer);
   addIfNotNull(writers, featurePolicy.writer);
   writers.addAll(headerWriters);
   return writers;
}

默认前五个不为null

contentTypeOptions.writer:负责处理X-Content-Type-Options响应头

xssProtection.writer:负责X-XSS-Protection响应头

cacheControl.writer:负责处理Cache-Control Pragma Expires的响应头

hsts.writer:负载处理Strict-Transport-Security响应头

frameOptions.writer:负责处理X-Frame-Options响应头

缓存控制

和缓存控制相关响应头有三个

  • Cache-Control: no-cache, no-store, max-age=0, must-revalidate
  • Pragma: no-cache
  • Expires: 0

Cache-Control

HTTP /1.1引入,无论请求头还是响应头都支持该字段.

  • no-store表示不作任何缓存,每次请求都会从服务端完整地下载内容

  • no-cache表示缓存但需要重新验证,数据虽然缓存在客户端,但是当需要使用该数据时,还是会向服务端发送请求,服务端则验证请求中所描述的缓存是否过期,如果没有过期,则返回304,客户端使用缓存;如果已经过期,则返回最新数据。

  • max-age表示缓存的有效期,单位为秒

  • must-revalidate表示当缓存在使用一个陈旧的资源时,必须先验证它的状态,已过期的将不被使用。

Pragma

Cache-Control类似,兼容HTTP/1.0

Expires

Expires指在指定日期之后,缓存过期,如果日期为0,表示缓存已经过期。

Spring Security默认就是不做任何缓存,对于放行的url会缓存。

如果请求本身没有经过 Spring security , 那么就不会经过 Filter 过滤器, 也就会被缓存了

@Override
public void configure(WebSecurity web) throws Exception {
   web.ignoring()
         .antMatchers("/index.html");
}

要想经过过滤器的请求也开启缓存,需要禁用掉Security的cacheControl .headers().cacheControl().disable()

@Override
protected void configure(HttpSecurity http) throws Exception {
   http
         .authorizeRequests()
         .anyRequest().authenticated()
         .and()
         .headers()
         .cacheControl()
         .disable()

X-Content-Type-Options

X-Content-Type-Options: nosniff 表示禁用客户端的MIME类型的嗅探,即服务端告诉客户端对于MIME类型的设置没有任何问题,当Content-Type类型值缺失时,不需要客户端对响应报文进行自我解析。(防止被XSS攻击)

如果不想禁用MIME嗅探,配置 .headers().contentTypeOptions().disable()

Strict-Transport-Security

Strict-Transport-Security用来指定当前客户端只能通过HTTPS访问服务端,而不能通过HTTP访问。

strict-Transport-Security: max-age=31536000 ; includeSubDomains

(1) max-age:设置在浏览器收到这个请求后的多少秒的时间内,凡是访问这个域名下的请求都使用HTTPS请求。 (2) includeSubDomains:这个参数是可选的,如果被指定,表示第1条规则也适用于子域名。

这个响应头并非总是会添加,如果当前请求是HTTPS请求,这个请求头才会添加,否则该请求头就不会添加,具体实现逻辑在HstsHeaderWriter#writeHeaders方法中:

public void writeHeaders(HttpservletRequest request,
	                         HttpservletResponse response) {
		if (this.requestMatcher.matches(request)) {
			if (!response.containsHeader(HSTS_HEADER_NAME)) {
				response.setHeader(HSTS_HEADER_NAME, this.hstsHeaderValue);
			}
		}
}

可以看到,向response 中添加响应头之前,会先调用requestMatcher.matches方法对当前请求进行判断,判断当前请求是否是HTTPS请求,如果是HTTPS请求,则添加该响应头,否则不添加。

可以通过java自带的keytool来生成HTTPS证书。

keytool -genkey -alias tomcathttps -keyalg RSA -keysize 2048 -keystore zhazha.p1 -validity 365

命令含义如下:

  • genkey:表示要创建一个新的密钥。
  • alias:表示keystore 的别名。
  • keyalg:表示使用的加密算法是RSA ,一种非对称加密算法。
  • keysize:表示密钥的长度。
  • keystore:表示生成的密钥存放位置。
  • validity:表示密钥的有效时间,单位为天。

接下来将生成的zhazha.p1证书复制到Spring Boot 项目的resources目录下,并在application.properties中添加如下配置:

server.ssl.key-store=classpath:zhazha.p1
server.ssl.key-alias=tomcathttps
server.ssl.key-store-password-111111

配置完成后,启动项目。浏览器中输入https://localhost:8080/login进行访问,由于这个HTTPS证书是我们自己生成的,并不被浏览器认可,所以在访问的时候会有安全提示,允许即可.

如果需要对Strict-Transport-Security 的值进行具体配置,例如关闭includeSubDomains 属性并重新设置max-age,方式如下:

.and()
.formLogin()
.loginProcessingUrl("/login.html")
.successHandler((request, response, authentication) -> {
   response.getWriter().write("login success");
})
.permitAll()
.and()
.csrf()
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
.and()
.headers()
.httpStrictTransportSecurity()
.includeSubDomains(false)
.maxAgeInSeconds(3600);

注意位置, 如果放在上面, 那么就是报错哦~

X-Frame-Options

X-Frame-Options响应头用来告诉浏览器是否允许一个页面在<frame> <iframe> <embed> 或者 <object>中展现,通过该响应头可以确保网站没有被嵌入到其他站点里面,进而避免发生单击劫持。

  • deny 表示该页面不允许在frame中展现(默认)。
  • sameorigin:该页面可以在相同域名页面的frame中展示。
  • allow-from uri:表示该页面可以在指定来源的frame中展示。

所谓单击劫持是指攻击者被劫持的页面放在一个iframe标签中,设置该iframe不可见,然后将iframe标签覆盖在另一个页面上,诱使用户在该页面上进行操作,通过调制iframe页面位置,使用户单击iframe页面的按钮。

X-XSS-Protection

X-XSS-Protection响应头告诉浏览器当检测到跨站脚本攻击时,浏览器将停止加载页面

  • 0 表示禁止XSS过滤
  • 1 表示启用XSS过滤。如果检测到跨站脚本攻击,浏览器将清除页面。
  • 1:mode=block 表示启用XSS过滤。如果检测到攻击,浏览器不会清除页面,而是阻止页面加载。Spring Security设置是这个
  • 1:report=<reporting-URI>表示启用XSS过滤,如果检测到跨站脚本攻击,浏览器将清除页面并发送违规报告

所谓XSS攻击是Crocs-Site Scripting 跨站脚本攻击,攻击者在网站上注入恶意的JavaScript代码,窃取Cookie信息,监听用户行为,修改DOM结构

这里就不提供关闭的方式了, 最好不要

Content-Security-Policy

Content-Security-Policy 为内容安全策略,简称CSP,用于检测并削弱某些特定类型的攻击,例如跨站脚本(XSS)和数据注入攻击

CSP相当于通过一个白名单明确告诉客户端,哪些外部资源可以加载和执行

Content-Security-Policy: default-src 'self' ; script-src 'self'; object-src 'none'; style-src 'self' https://maxcdn.bootstrapcdn.com https://getbootstrap.com; img-src *; child-src https:

这个响应头含义如下:

  • default-src 'self':默认情况下所有资源只能从当前域中加载。接下来细化的配置会覆盖default-src,没有细化的选项则使用default-src
  • script-src 'self':表示脚本文件只能从当前域名加载。
  • object-src 'none':表示object标签不加载任何资源。
  • style-src cdn.zhazha.com:表示只加载来自cdn.zhazha.com 的样式表。
  • img-src *:表示可以从任意地址加载图片。
  • child-src https:表示必须使用HTTPS来加载frame。(已弃用)

Spring SecurityContent-Security-Policy提供了配置方法,如果我们需要配置,则方式如下:

csrf之后

.and() // 添加白名单
.headers()
.contentSecurityPolicy("default-src 'self' ; script-src 'self'; object-src 'none'; style-src 'self' https://maxcdn.bootstrapcdn.com/bootstrap https://getbootstrap.com/docs; img-src *;")

Referrer-Policy

Referrer-Policy描述用户从哪里进入到当前页面

这个表示如果是从HTTPS 网址链接到HTTP网址,就不发送 Referer字段,其他情况发送。开发者可以通过Spring Security中提供的方法对此进行修改,方式如下:

.and()
.headers()
.referrerPolicy()
.policy(ReferrerPolicyHeaderWriter.ReferrerPolicy.ORIGIN)

事先是这样:

image-20221129112815077

修改之后是这样:

image-20221129112944704

这个配置取值是origin,表示总是发送源信息(源信息仅包含请求协议和域名,不包含其他路径信息,与之相对的是完整的URL)。其他的取值还有:

  • no-referrer:表示从请求头中移除Referer字段。
  • same-origin:表示链接到同源地址时,发送文件源信息作为引用地址,否则不发送。
  • strict-origin:表示从HTTPS链接到HTTP时不发送源信息,否则发送。
  • origin-when-cross-origin:表示对于同源请求会发送完整的URL作为引用地址,但是对于非同源请求,则只发送源信息。
  • strict-origin-when-cross-origin:表示对于同源的请求,会发送完整的URL作为引用地址;跨域时,如果是从HTTPS链接到HTTP,则不发送Referer字段,否则发送文件的源信息
  • unsafe-url:表示无论是同源请求还是非同源请求,都发送完整的URL(移除参数信息之后)作为引用地址。

Feature-Policy

Feature-Policy响应头提供了一种可以在本页面或包含的iframe上启用或禁止浏览器特性的机制移动端开发使用较多)。举一个简单例子,如果想要禁用震动和定位API,那么可以在响应头中添加如下内容:

Feature-Policy: vibrate 'none' ; geolocation 'none'

Clear-Site-Data

Clear-Site-Data 一般用在注销登录响应头中,表示告诉浏览器清除当前网站相关的数据,可以通过具体参数指定想要清除的数据,比如cookies cache storage等,也可以通过"*"表示清除所有的数据

protected void configure(HttpSecurity http) throws Exception {
    http.authorizeRequests()
            .anyRequest().authenticated()
            .and()
            .formLogin()
            .and()
            .logout()
            .addLogoutHandler(new HeaderWriterLogoutHandler(new ClearSiteDataHeaderWriter(ClearSiteDataHeaderWriter.Directive.ALL)))
            .and()
            .headers().contentTypeOptions().disable()
            .csrf().disable()
            .headers()
            .featurePolicy("vibrate 'none'; geolocation 'none'")
            .and()
            .referrerPolicy()
            .policy(ReferrerPolicyHeaderWriter.ReferrerPolicy.ORIGIN_WHEN_CROSS_ORIGIN)
            .and()
            .contentSecurityPolicy(contentSecurityPolicyConfig -> {
                contentSecurityPolicyConfig.policyDirectives("default-src 'self' ; script-src 'self'; object-src 'none'; style-src 'self' https://maxcdn.bootstrapcdn.com/bootstrap https://getbootstrap.com/docs; img-src *;");
                contentSecurityPolicyConfig.reportOnly(); // 配置报告行为
            });

}

HTTP通信安全

HTTP通信安全主要从三个方面入手:

(1)使用HTTPS代替HTTP。

(2)Strict-Transport-Security 配置。

(3)代理服务器配置。 其中第2点我们前面已经讲过,这里主要和大家分享第1点和第3点。

使用HTTPS

作为一个框架,Spring Security不处理HTTP连接问题,因此不直接提供对HTTPS的支持。但是,它提供了许多有助于HTTPS使用的功能。 接下来我们通过一个简单的案例来演示其具体用法。 首先创建一个Spring Boot项目,引入 Spring Security和 Web,然后参考Strict-Transport-Security小节中的方式创建HTTPS 证书,并配置到Spring Boot 项目中。 配置完成后,我们再在application.yml中添加如下配置修改项目端口号:

server:
  port: 8443 # 这个端口将被用于 https
  ssl:
    key-store: classpath:zhazha.p1 # ssl 证书创建
    key-alias: tomcathttps
    key-store-password: 112233
spring:
  security:
    user:
      name: 'zhazha'
      password: '{noop}123456'

此时我们的项目就支持HTTPS 访问了,HTTPS 的访问端口是8443。为了更好地演示Spring Security的功能,我们需要项目同时支持HTTPSHTTP,所以还需要在项目中添加

@Configuration
public class TomcatConfig {

   @Bean
   public TomcatServletWebServerFactory tomcatServletWebServerFactory() {
      TomcatServletWebServerFactory tomcatServletWebServerFactory = new TomcatServletWebServerFactory();
      tomcatServletWebServerFactory.addAdditionalTomcatConnectors(createTomcatConnector());
      return tomcatServletWebServerFactory;
   }
   
   private Connector createTomcatConnector() {
      Connector connector = new Connector("org.apache.coyote.http11.Http11NioProtocol");
      connector.setScheme("http");
      connector.setPort(8080);
      return connector;
   }
   
}

提供了两个url:

@RestController
@Slf4j
public class HelloController {
   
   /**
    * 只能通过 https 访问
    *
    * @return
    */
   @GetMapping("/https")
   public String https() {
      return "https";
   }
   
   /**
    * 只能通过 http 访问
    *
    * @return
    */
   @GetMapping("http")
   public String http() {
      return "http";
   }
   
}

紧接着开始 url 分配到 https 还是 http:

import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
   
   
   @Override
   protected void configure(HttpSecurity http) throws Exception {
      http.authorizeRequests()
            .anyRequest().authenticated()
            .and().formLogin()
            .and().requiresChannel()
            .antMatchers("/https").requiresSecure() // 只能 https 访问
            .antMatchers("/http").requiresInsecure()
            .and().csrf().disable()
      ;
   }
}

现在我们有两个端口的登录地址:

我们通过 8080 地址登录之后, 可以访问 /http, 但是在访问 /https 的时候会自动跳转到 8443路径

之所以重定向到8443端口,并非因为我们项目端口是8443,而是因为8443HTTPS的默认监听端口,无论项目端口号是多少,这里都会重定向到8443端口。

如果需要修改成其他端口, 比如: 8444, 则需要这么玩:

.and().portMapper().http(8080).mapsTo(8444) // 修改https默认监控端口为 8444

这里为什么是 80808444 呢? 看类的注释: image-20221129140728188

当在 Http 和 Https 之间重定向时, 允许为给定的Http端口指定Https端口

意思是说, 如果你在 8080 的 http 路径下访问http://localhost:8080/http, 如果访问了http://localhost:8080/https, 此时上面的配置就用上了, 直接从 8080 跳转到 8444

主要注意, 如果使用 https login 登录, 就会在 Cookie 中多出一个 Secure 的字符串, 此时如果访问 /http, 会重新跳转到 httplogin 登录接口, 因为 httpcookie 不存在 Secure字符串, 又 cookie 不区分端口号, 所以访问 http 地址时, cookie多出的字符串无法被 http 识别, 无法被识别那么表示cookie失效, 需要重新登录.

http login 登录 cookie 没有改字符串:

image-20221129141753612

https login登录 cookie 内容:

image-20221129141709300

代理服务配置

在分布式环境下, 一般使用Nginx进行负载均衡, 此时需要确保自己的代理服务器和Spring Security的配置是正确的, 以便Spring Security能够准确的获取请求的真实IP, 避免各种潜在的威胁和应用程序错误

为了在服务端能够真实的拿到客户端的IP信息等, 代理服务器需要配置 X-Forwarded-* 以便将信息从客户端转发到 服务端, 服务端接收到改请求后, 开发者可以借助 request.getXXX 方法拿到客户端真实的信息, 不会感到有代理服务器的存在

这点需要开发者注意