9.Spring security漏洞保护

文章目录

      • *漏洞保护*
        • *9.1CSRF攻击与防御*
          • *9.1.1CSRF简介*
          • *9.1.2CSRF攻击演示*
          • *9.1.3CSRF防御*
            • *令牌同步模式*
            • *`SameSite`*
            • *需要注意的问题*
          • *9.1.4源码分析*
            • *`CsrfToken`*
            • *`CsrfTokenRepository`*
            • *`CsrfFilter`*
            • *`CsrfAuthenticationStrategy`*
        • *9.2HTTP响应头处理*
          • *9.2.1缓存控制*
            • *`Cache-Control`*
            • *`Pragma`*
            • *`Expires`*
          • *9.2.2`X-Content-Type-Options`*
          • *9.2.3`Strict-Transport-Security`*
          • *9.2.4`X-Frame-Options`*
          • *9.2.5`X-XSS-Protection`*
          • *9.2.6`Content-Security-Policy`*
          • *9.2.7`Referrer-Policy`*
          • *9.2.8`Feature-Policy`*
          • *9.2.9`Clear-Site-Data`*
        • *9.3HTTP通信安全*

漏洞保护

9.1CSRF攻击与防御

9.1.1CSRF简介

CSRF(Cross-Site Request Forgery,跨站请求伪造),也可称为一键式攻击(one-click attack)。
CSRF攻击是一种挟持用户在当前已登录的浏览器上发送恶意请求的攻击方法。相对于XSS利用用户对指定网站的信任,CSRF则是利用网站对用户网页浏览器的信任。简单来说,CSRF是攻击者通过一些技术手段欺骗用户的浏览器,去访问一个用户曾经认证过的网站并执行恶意请求,例如发送邮件甚至财产操作。由于客户端(浏览器)已经在该网站上认证过,所以该网站会认为是真正的用户在操作而执行请求。
例如,假设javaboy现在登录了某银行的网站准备完成一项转账操作,相关链接为:https://bank.xxx.com/withdraw?account=javaboy&amount=1000&for=zhangsan。假设javaboy没有注销登录该银行的网站,就在同一个浏览器新的选项卡中打开了一个危险网站,这个危险网站中有一幅图片:

<img src="https://bank.xxx.com/withdraw?account=javaboy&amount=1000&for=lisi"/>

一旦用户打开了这个网站,这个图片链接中的请求就会自动发送出去。由于是同一个浏览器并且用户尚未注销登录,所以该请求会自动携带上对应的有效的cookie信息,进而完成一次转账操作。

9.1.2CSRF攻击演示

普通项目(csrf-1)的转账接口,项目默认端口号为8080:

@RestController
public class HelloController {
    @PostMapping("/withdraw")
    public void withdraw() {
        System.out.println("执行了一次转账操作");
    }
}

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.inMemoryAuthenticaiton()
            .withUser("javaboy")
            .password("{noop}123")
            .roles("admin");
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .anyRequest().authenticated()
                .and()
                .formLogin()
                .and()
                // 由于spring security默认开启了CSRF攻击防御,所以要手动将其禁止
                .csrf().disable();
    }
}

危险网站项目(csrf-2),仅引入web依赖即可。不过需要修改项目端口号为8081,然后在resources/static目录下新建index.html文件(仅列出核心代码):

<form action="http://localhost:8080/withdraw" method="post">
    <input type="hidden" value="javaboy" name="name">
    <input type="hidden" value="10000" name="money">
    <input type="submit" value="点我">
form>

需要启动两个项目进行测试。输入http://localhost:8080访问csrf-1,并完成登录操作,登录成功后,不要注销登录,继续打开一个新的选项卡访问csrf-2,单击表单按钮时发起请求,csrf-1项目的控制台会有日志打印,这就是一个跨站请求伪造。

9.1.3CSRF防御

CSRF攻击的根源在于浏览器默认的身份验证机制(自动携带当前网站的cookie信息),这种机制虽然可以保证请求时来自用户的某个浏览器,但是无法确保该请求是用户授权发送的。攻击者和用户发送的请求一模一样,这意味着没有办法去直接拒绝这里的某一个请求。如果能在合法请求中额外携带一个攻击者无法获取的参数,就可以成功区分出两种不同的请求,进而拒绝掉恶意请求。
Spring中提供了两种机制来防御CSRF攻击:

  • 令牌同步模式。
  • 在cookie上指定SameSite属性。

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

令牌同步模式

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

接下来通过一个案例感受一下。首先创建一个spring boot项目,引入web、spring security和thymeleaf依赖。项目创建成功后,方便起见,在application.properties中配置登录用户名/密码:

spring.security.user.name=javaboy
spring.security.user.password=123

然后在resources/templates目录下新建一个index.html页面(仅列出核心代码):


DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>indextitle>
head>
<body>
<form action="/hello" method="post">
    
    
    <input type="hidden" th:value="${_csrf.token}" th:name="${_csrf.parameterName}">
    <input type="submit" value="hello">
form>
body>
html>
@Controller
public class HelloController {
    // 需要注意的是,在spring security中,默认不会对GET、HEAD、OPTIONS、TRACE请求进行CSRF令牌校验。
    @PostMapping("/hello")
    @ResponseBody
    public String hello() {
        return "hello csrf!";
    }

    @GetMapping("/index.html")
    public String index() {
        return "index";
    }
}

可以在浏览器中按F12键,查看登录请求:
9.Spring security漏洞保护_第1张图片

针对静态的AJAX请求,spring security也提供了相应的方案:即将CSRF令牌放在响应头cookie中,开发者自行从cookie中提取出令牌信息,然后再作为参数提交到服务器。

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.inMemoryAuthentication()
                .withUser("javaboy")
                .password("{noop}123")
                .roles("admin");
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .anyRequest().authenticated()
                .and()
                .formLogin()
                .loginProcessingUrl("/login.html")
                .successHandler((req, resp, auth) -> {
                    resp.getWriter().write("login success");
                })
                .permitAll()
                .and()
                .csrf()
                // 设置httpOnly属性为false,否则前端将无法读取到cookie中的CSRF令牌
                .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse());
    }
}

DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>logintitle>
    <script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.6.0/jquery.min.js">script>
    <script src="https://cdn.bootcdn.net/ajax/libs/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>

CSRF攻击的根源在于浏览器默认的身份认证机制,即发送请求时会自动携带上网站的cookie,但是cookie的内容是什么黑客是不知道的,所以即使非法请求携带了含有CSRF令牌的cookie也没用,只有将CSRF令牌从cookie中解析出来,并放到请求头或者请求参数中,才有用。

SameSite

目前使用较少,后期有需求的话再来了解。

需要注意的问题

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

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

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

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

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

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

9.1.4源码分析
CsrfToken

Spring security中提供了CsrfToken接口用来描述令牌信息:

public interface CsrfToken extends Serializable {
    // 当CSRF令牌被放置在请求头时,获取参数名
    String getHeaderName();

    // 当CSRF令牌被当作请求参数传递时,获取参数名
    String getParameterName();

    // 获取具体的CSRF令牌
    String getToken();
}

9.Spring security漏洞保护_第2张图片

  • DefaultCsrfToken是一个默认的实现类,该类为三个接口提供了对应的属性,属性值通过构造方法传入,再通过各自的get方法返回。
  • SaveOnAccessCsrfToken是一个代理类,由于CsrfToken只有两个实现类,所以正常来说SaveOnAccessCsrfToken代理的就是DefaultCsrfToken。代理类中主要是对getToken方法做了改变,当调用getToken方法时,才去执行令牌的保存操作,这样可以避免很多无用的保存操作。
CsrfTokenRepository

CsrfToken的保存接口。

public interface CsrfTokenRepository {
    // 用来生成一个CSRF令牌
    CsrfToken generateToken(HttpServletRequest request);

    // 用来保存CSRF令牌
    void saveToken(CsrfToken token, HttpServletRequest request, HttpServletResponse response);

    // 用来读取一个CSRF令牌
    CsrfToken loadToken(HttpServletRequest request);
}

9.Spring security漏洞保护_第3张图片

HttpSessionCsrfTokenRepository是将CsrfToken保存在HttpSession中:

public final class HttpSessionCsrfTokenRepository implements CsrfTokenRepository {
    // 如果传入的CsrfToken为null,就从HttpSession中移除CsrfToken令牌;否则将其保存到HttpSession中
    @Override
	public void saveToken(CsrfToken token, HttpServletRequest request, HttpServletResponse response) {
		if (token == null) {
			HttpSession session = request.getSession(false);
			if (session != null) {
				session.removeAttribute(this.sessionAttributeName);
			}
		}
		else {
			HttpSession session = request.getSession();
			session.setAttribute(this.sessionAttributeName, token);
		}
	}

    // 返回HttpSession中保存的令牌信息
    @Override
	public CsrfToken loadToken(HttpServletRequest request) {
		HttpSession session = request.getSession(false);
		if (session == null) {
			return null;
		}
		return (CsrfToken) session.getAttribute(this.sessionAttributeName);
	}

    // 生成一个默认的DefaultCsrfToken令牌,headerName和parameterName都是默认的,而具体的令牌则是一个UUID字符串
    @Override
	public CsrfToken generateToken(HttpServletRequest request) {
		return new DefaultCsrfToken(this.headerName, this.parameterName, createNewToken());
	}

    private String createNewToken() {
		return UUID.randomUUID().toString();
	}
}

CookieCsrfTokenRepository则是将CsrfToken保存在cookie中:

public final class CookieCsrfTokenRepository implements CsrfTokenRepository {
    public CookieCsrfTokenRepository() {
	}
    
    // 逻辑和HttpSessionCsrfTokenRepository一致
    @Override
	public CsrfToken generateToken(HttpServletRequest request) {
		return new DefaultCsrfToken(this.headerName, this.parameterName, createNewToken());
	}

    // 保存令牌,具体方式就是生成cookie并添加到响应头中
    @Override
	public void saveToken(CsrfToken token, HttpServletRequest request, HttpServletResponse response) {
		String tokenValue = (token != null) ? token.getToken() : "";
		Cookie cookie = new Cookie(this.cookieName, tokenValue);
		cookie.setSecure((this.secure != null) ? this.secure : request.isSecure());
		cookie.setPath(StringUtils.hasLength(this.cookiePath) ? this.cookiePath : this.getRequestContext(request));
		cookie.setMaxAge((token != null) ? this.cookieMaxAge : 0);
		cookie.setHttpOnly(this.cookieHttpOnly);
		if (StringUtils.hasLength(this.cookieDomain)) {
			cookie.setDomain(this.cookieDomain);
		}
		response.addCookie(cookie);
	}

    // 从请求头中提取出cookie,进而解析出令牌信息
    @Override
	public CsrfToken loadToken(HttpServletRequest request) {
		Cookie cookie = WebUtils.getCookie(request, this.cookieName);
		if (cookie == null) {
			return null;
		}
		String token = cookie.getValue();
		if (!StringUtils.hasLength(token)) {
			return null;
		}
		return new DefaultCsrfToken(this.headerName, this.parameterName, token);
	}

    public static CookieCsrfTokenRepository withHttpOnlyFalse() {
		CookieCsrfTokenRepository result = new CookieCsrfTokenRepository();
		result.setCookieHttpOnly(false);
		return result;
	}
}

CookieCsrfTokenRepository可以通过两种方式获取其实例,第一种方式是直接新建一个实例,这种情况下生成的cookie中的HttpOnly属性默认为true,即前端不能通过JS操作cookie;第二种方式是调用静态方法withHttpOnlyFalse,设置HttpOnly属性默认为false,即允许前端通过JS操作cookie。

LazyCsrfTokenRepository是一个代理类,可以代理HttpSessionCsrfTokenRepository或者CookieCsrfTokenRepository,代理的目的是延迟保存生成的CsrfToken

public final class LazyCsrfTokenRepository implements CsrfTokenRepository {
	public LazyCsrfTokenRepository(CsrfTokenRepository delegate) {
		this.delegate = delegate;
	}

	/**
	 * 在生成CsrfToken时,代理类生成的CsrfToken类型是DefaultCsrfToken,这里将DefaultCsrfToken装饰为
	 * SaveOnAccessCsrfToken,当调用SaveOnAccessCsrfToken中的getToken方法时,才会去保存CsrfToken。
	 */
	@Override
	public CsrfToken generateToken(HttpServletRequest request) {
		return wrap(request, this.delegate.generateToken(request));
	}

	// 只有当token为空时,才会去执行代理类的saveToken方法(相当于只执行移除CsrfToken操作)
	@Override
	public void saveToken(CsrfToken token, HttpServletRequest request, HttpServletResponse response) {
		if (token == null) {
			this.delegate.saveToken(token, request, response);
		}
	}

	@Override
	public CsrfToken loadToken(HttpServletRequest request) {
		return this.delegate.loadToken(request);
	}

	private CsrfToken wrap(HttpServletRequest request, CsrfToken token) {
		HttpServletResponse response = getResponse(request);
		return new SaveOnAccessCsrfToken(this.delegate, request, response, token);
	}
}
CsrfFilter

CsrfFilter用来校验客户端传来的CSRF令牌是否有效。CsrfFilter继承自OncePerRequestFilter,所以对它来说最重要的方法是doFilterInternal

public final class CsrfFilter extends OncePerRequestFilter {
	@Override
	protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
			throws ServletException, IOException {
		request.setAttribute(HttpServletResponse.class.getName(), response);
		// 加载CsrfToken实例,默认使用的是LazyCsrfTokenRepository,相关配置在CsrfConfigurer#configure中
		CsrfToken csrfToken = this.tokenRepository.loadToken(request);
		boolean missingToken = (csrfToken == null);
		if (missingToken) {
			// 如果CsrfToken不存在,则立马生成新的实例并保存起来。需要注意,如果tokenRepository
			// 类型是LazyCsrfTokenRepository,则这里并未真正将令牌保存起来
			csrfToken = this.tokenRepository.generateToken(request);
			this.tokenRepository.saveToken(csrfToken, request, response);
		}
		// 设置到request属性中,方便前端提取
		request.setAttribute(CsrfToken.class.getName(), csrfToken);
		request.setAttribute(csrfToken.getParameterName(), csrfToken);
		// 判断当前请求方法是否是GET、HEAD、TRACE以及OPTIONS,如果是,则请求直接过,不用进行令牌校验,
		// 这也意味着上一步没有必要进行令牌的保存操作。此时使用LazyCsrfTokenRepository的优势就体现出来了
		if (!this.requireCsrfProtectionMatcher.matches(request)) {
			if (this.logger.isTraceEnabled()) {
				this.logger.trace("Did not protect against CSRF since request did not match "
						+ this.requireCsrfProtectionMatcher);
			}
			filterChain.doFilter(request, response);
			return;
		}
		// 从请求头或者请求参数中提取出令牌信息,并和通过loadToken方法加载出来的令牌进行对比,判断是否合法
		String actualToken = request.getHeader(csrfToken.getHeaderName());
		if (actualToken == null) {
			actualToken = request.getParameter(csrfToken.getParameterName());
		}
		if (!equalsConstantTime(csrfToken.getToken(), actualToken)) {
			this.logger.debug(
					LogMessage.of(() -> "Invalid CSRF token found for " + UrlUtils.buildFullRequestUrl(request)));
			AccessDeniedException exception = (!missingToken) ? new InvalidCsrfTokenException(csrfToken, actualToken)
					: new MissingCsrfTokenException(actualToken);
			this.accessDeniedHandler.handle(request, response, exception);
			return;
		}
		filterChain.doFilter(request, response);
	}
}

具体的流程:请求到达后,会经过CsrfFilter,在该过滤器中,首先加载出保存的CsrfToken,可以是从HttpSession中加载,也可以是从请求头携带的Cookie中加载,默认是从HttpSession加载,如果加载出来的CsrfTokennull,则立即生成一个CsrfToken并保存起来,由于默认的tokenRepository类型是LazyCsrfTokenRepository,所以这里的保存并不是真正的保存,之所以这么做的原因在于,如果请求方法是GETHEADTRACE以及OPTIONS,就没有必要保存。然后将生成的CsrfToken放到请求对象中,以方便前端渲染。接下来判断请求方法是否是需要进行CSRF令牌校验的方法,如果不是,则直接执行后面的过滤器,否则就从请求中拿出CSRF令牌信息和一开始加载出来的令牌进行比对。

CsrfFilter过滤器是由CsrfConfigurer进行配置的,而CsrfConfigurer则是在WebSecurityConfigurerAdapter#getHttp方法中添加进HttpSecurity中的,其原理和SessionManagementConfigurer原理基本一致。

CsrfAuthenticationStrategy

CsrfAuthenticationStrategy实现了SessionAuthenticaitonStrategy接口,默认也是由CompositeSessionAuthenticationStrategy代理执行,在用户登录成功后触发执行。
主要用于在登录成功后,删除旧的CsrfToken并生成一个新的CsrfToken

public final class CsrfAuthenticationStrategy implements SessionAuthenticationStrategy {
	public CsrfAuthenticationStrategy(CsrfTokenRepository csrfTokenRepository) {
		this.csrfTokenRepository = csrfTokenRepository;
	}

	@Override
	public void onAuthentication(Authentication authentication, HttpServletRequest request,
			HttpServletResponse response) throws SessionAuthenticationException {
		boolean containsToken = this.csrfTokenRepository.loadToken(request) != null;
		// 如果加载到令牌,则先删除已经存在的令牌(saveToken方法第一个参数为null),然后生成新的令牌并重新保存
		if (containsToken) {
			this.csrfTokenRepository.saveToken(null, request, response);
			CsrfToken newToken = this.csrfTokenRepository.generateToken(request);
			this.csrfTokenRepository.saveToken(newToken, request, response);
			request.setAttribute(CsrfToken.class.getName(), newToken);
			request.setAttribute(newToken.getParameterName(), newToken);
		}
	}
}

9.2HTTP响应头处理

HTTP响应头中的许多属性都可以用来提高web安全。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中添加的,默认情况下,该过滤器就会添加到spring security过滤器链中,HeaderWriterFilter是通过HeadersConfigurer进行配置的。

public class HeadersConfigurer<H extends HttpSecurityBuilder<H>>
		extends AbstractHttpConfigurer<HeadersConfigurer<H>, H> {
	@Override
	public void configure(H http) {
		HeaderWriterFilter headersFilter = createHeaderWriterFilter();
		http.addFilter(headersFilter);
	}

	// 创建HeaderWriterFilter过滤器
	private HeaderWriterFilter createHeaderWriterFilter() {
		List<HeaderWriter> writers = getHeaderWriters();
		if (writers.isEmpty()) {
			throw new IllegalStateException(
					"Headers security is enabled, but no headers will be added. Either add headers or disable headers security");
		}
		HeaderWriterFilter headersFilter = new HeaderWriterFilter(writers);
		headersFilter = postProcess(headersFilter);
		return headersFilter;
	}

	// 获取到所有需要添加的响应头,并且只会添加不为null的实例,默认情况下,只有前五个不为null
	private List<HeaderWriter> getHeaderWriters() {
		List<HeaderWriter> writers = new ArrayList<>();
		// 负责处理X-Content-Type-Options响应头
		addIfNotNull(writers, this.contentTypeOptions.writer);
		// 负责处理X-XSS-Protection
		addIfNotNull(writers, this.xssProtection.writer);
		// 负责处理Cache-Control、Pragma以及Expires响应头
		addIfNotNull(writers, this.cacheControl.writer);
		// 负责处理Strict-Transport-Security响应头
		addIfNotNull(writers, this.hsts.writer);
		// 负责处理X-Frame-Options响应头
		addIfNotNull(writers, this.frameOptions.writer);
		addIfNotNull(writers, this.hpkp.writer);
		addIfNotNull(writers, this.contentSecurityPolicy.writer);
		addIfNotNull(writers, this.referrerPolicy.writer);
		addIfNotNull(writers, this.featurePolicy.writer);
		addIfNotNull(writers, this.permissionsPolicy.writer);
		writers.addAll(this.headerWriters);
		return writers;
	}

	private <T> void addIfNotNull(List<T> values, T value) {
		if (value != null) {
			values.add(value);
		}
	}
}
9.2.1缓存控制

和缓存控制相关的响应头一共有三个:

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

Cache-Control是HTTP/1.1中引入的缓存字段,无论是请求头还是响应头都支持该字段。

  • no-store表示不做任何缓存,每次请求都会从服务端完整地下载内容。
  • no-cache则表示缓存但是需要重新验证,这种情况下,数据虽然缓存在客户端,但是当需要使用该数据时,还是会向服务端发送请求,服务端则验证请求中所描述的缓存是否过期,如果没有过期,则返回304,客户端使用缓存;如果已经过期,则返回最新数据。
  • max-age则表示缓存的有效期,这个有效期并非一个时间戳,而是一个秒数,指从请求发起后多少秒内缓存有效。
  • must-revalidate表示当缓存在使用一个陈旧的资源时,必须先验证它的状态,已过期的将不被使用。
Pragma

Pragma是HTTP/1.0中定义的响应头,作用类似于Cache-Control: no-cache,但是并不能代替Cache-Control,该字段主要用来兼容HTTP/1.0的客户端。

Expires

Expires响应头指定了一个日期,即在指定日期之后,缓存过期。如果日期值为0的话,表示缓存已经过期。

Spring security默认就是不做任何缓存(例如Cache-Control: no-cache)。但是需要注意,这个是针对经过spring security过滤器的请求,如果请求本身都没经过spring security的过滤器,那么该缓存的还是会缓存的。

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
	@Override
	public void configure(WebSecurity web) throws Exception {
		web.ignoring().antMatchers("/hello.html");
	}
}
// 当访问/hello.html时,请求就不会经过spring security过滤器,所以该资源还是会缓存的

如果请求经过spring security过滤器,同时开发者又希望开启缓存功能,那么可以关闭关于缓存的默认配置:

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
	@Override
	public void configure(HttpSecurity http) throws Exception {
		http.authorizeRequests()
                .anyRequest().authenticated()
                .and()
                .headers()
				// 关闭后,spring security就不会配置Cache-Control、Pragma、Expires三个缓存相关的响应头了
				.cacheControl()
                .disable()
				.and()
                .formLogin()
				.and()
                .csrf().disable();
	}
}
9.2.2X-Content-Type-Options

要理解X-Content-Type-Options响应头,得先了解MIME嗅探。
一般来说,浏览器通过响应头Content-Type来确定响应报文类型,但是在早期浏览器中,为了提高用户体验,并不会严格根据Content-Type的值来解析响应报文,当Content-Type的值缺失,或者浏览器认为服务端给出了错误的Content-Type值,此时就会对响应报文进行自我解析,即自动判断报文类型然后进行解析,在这个过程中就有可能触发XSS攻击。
X-Content-Type-Options响应头相当于一个提示标志,被服务器用来提示客户端一定要遵循在Content-Type中对MIME类型的设定,而不能对其进行修改。这就禁用了客户端的MIME类型嗅探行为,换言之,就是服务端告诉客户端其对于MIME类型的设置没有任何问题。
如果开发者不想禁用MIME嗅探,可以通过配置从响应头中移除:

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
	 @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .anyRequest().authenticated()
                .and()
                .headers()
				// 移除X-Content-Type-Options响应头
				.contentTypeOptions()
                .disable()
				.and()
                .formLogin()
				.and()
                .csrf().disable();
}
9.2.3Strict-Transport-Security

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

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

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

@Override
public void writeHeaders(HttpServletRequest request, HttpServletResponse response) {
	// 判断当前是否是HTTPS请求,如果是,则添加该响应头
	if (!this.requestMatcher.matches(request)) {
		if (this.logger.isTraceEnabled()) {
			this.logger.trace(LogMessage.format("Not injecting HSTS header since it did not match request to [%s]",
					this.requestMatcher));
		}
		return;
	}
	if (!response.containsHeader(HSTS_HEADER_NAME)) {
		response.setHeader(HSTS_HEADER_NAME, this.hstsHeaderValue);
	}
}

也可以手动配置Strict-Transport-Security,例如关闭includeSubDomains属性并重新设置max-age

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
	@Override
    protected void configure(HttpSecurity http) throws Exception {
		http.authorizeRequests()
                .anyRequest().authenticated()
				.and()
                .formLogin()
				.and()
                .csrf().disable()
				.headers()
				.httpStrictTransportSecurity()
				.includeSubDomains(false)
				.maxAgeInSeconds(3600);
	}
}

当然也可以直接调用.disable()方法移除该响应头。

9.2.4X-Frame-Options

用来告诉浏览器是否允许一个页面在