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信息,进而完成一次转账操作。
普通项目(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项目的控制台会有日志打印,这就是一个跨站请求伪造。
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";
}
}
针对静态的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
可能会因为超时而失效,导致前端请求传来的令牌无法得到验证,解决这一问题的方式:
- 最佳方案是在表单提交时,通过JS获取CSRF令牌,然后将获取到的令牌跟随表单一起提交。
- 当会话快要过期时,前端通过JS提醒用户刷新页面,以给会话续命。
- 将令牌存储在cookie中而不是
HttpSession
中。
登录和注销:为了保护用户的敏感信息,登录请求和注销请求需要注意CSRF攻击防护。
文件上传:这类请求比较特殊,因此需要额外注意。如果将CSRF放在请求体中,就会面临一个"鸡和蛋"的问题。服务端需要先验证CSRF令牌以确认请求是否合法,而这也意味着需要先读取请求体以获取CSRF令牌,这就陷入一个死循环了。
一般来说,将CSRF防御与multipart/form-data
一起使用,有两种不同的策略:
- 将CSRF令牌放在请求体中。
- 将CSRF令牌放在请求URL中。
将令牌放在请求体中,意味着任何人都可以向服务器上传临时文件,但是只有令牌验证通过的用户,才能真正提交一个文件,这也是目前推荐的方案,因为上传临时文件对服务器的影响可以忽略不计。如果不希望未经授权的用户上传临时文件,那么可以将令牌放在请求URL地址中,但是这种方式可能带来令牌泄露的风险。
CsrfToken
Spring security中提供了
CsrfToken
接口用来描述令牌信息:
public interface CsrfToken extends Serializable {
// 当CSRF令牌被放置在请求头时,获取参数名
String getHeaderName();
// 当CSRF令牌被当作请求参数传递时,获取参数名
String getParameterName();
// 获取具体的CSRF令牌
String getToken();
}
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);
}
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
加载,如果加载出来的CsrfToken
为null
,则立即生成一个CsrfToken
并保存起来,由于默认的tokenRepository
类型是LazyCsrfTokenRepository
,所以这里的保存并不是真正的保存,之所以这么做的原因在于,如果请求方法是GET
、HEAD
、TRACE
以及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);
}
}
}
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);
}
}
}
和缓存控制相关的响应头一共有三个:
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();
}
}
X-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();
}
Strict-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()
方法移除该响应头。
X-Frame-Options
用来告诉浏览器是否允许一个页面在
、
、
或者
中展现,通过该响应头可以确保网站没有被嵌入到其他站点里面,进而避免发生单击劫持。
X-Frame-Options
响应头有三种不同的取值:
deny
(默认值):表示该页面不允许在frame
中展示,即便是在相同域名的页面中嵌套也不允许。sameorigin
:表示该页面可以在相同域名页面的frame
中展示。allow-from uri
:表示该页面可以在指定来源的frame
中展示。
如果项目需要,开发者也可以对此进行修改:
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.anyRequest().authenticated()
.and()
.formLogin()
.and()
.csrf().disable();
.headers()
.frameOptions()
.sameOrigin();
}
}
当然也可以直接调用
.disable()
方法移除该响应头。
单击劫持是一种视觉上的欺骗手段。攻击者将被劫持的网页放在一个
iframe
标签中,设置该iframe
标签透明不可见,然后将标签覆盖在另一个网页上,最后诱使用户在该网页上进行操作,通过调整iframe
页面的位置,可以诱使用户恰好单击在iframe
页面的一些功能性按钮上。
X-XSS-Protection
该响应头告诉浏览器,当检测到跨站脚本攻击(XSS)时,浏览器将停止加载页面,有四种不同的取值:
0
表示禁止XSS过滤。1
表示启用XSS过滤(通常浏览器是默认的)。如果检测到跨站脚本攻击,浏览器将清除页面(删除不安全的部分)。1;mode=block
表示启用XSS过滤。如果检测到攻击,浏览器将不会清除页面,而是阻止页面加载。1;report=
表示启用XSS过滤。如果检测到跨站脚本攻击,浏览器将清除页面,并使用CSP report-uri指令的功能发送违规报告(chrome支持)。Spring security中设置的X-XSS-Protection响应头如下:
X-XSS-Protection: 1; mode=block
当然开发者也可以对此进行配置,例如想移除
mode=block
部分:
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.anyRequest().authenticated()
.and()
.formLogin()
.and()
.csrf().disable();
.headers()
.xssProtection()
.block(false);
}
}
当然也可以直接调用
.disable()
方法移除该响应头。
跨站脚本攻击(Cross-Site Scripting, XSS)是一种安全漏洞,攻击者可以利用这种漏洞在网站上注入恶意的JS代码,而浏览器无法区分出来这是恶意的还是正常的代码。当被攻击者登录网站时,就会自动运行这些恶意代码,攻击者可以利用这些恶意代码去窃取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 cdn.javaboy.org;img-src *;child-src https:
响应头含义如下:
default-src 'self'
:默认情况下所有资源只能从当前域中加载。接下来细化的配置会覆盖default-src
,没有细化的选项则使用default-src
。script-src 'self'
:表示脚本文件只能从当前域名加载。object-src 'none'
:表示object
标签不加载任何资源。style-src cdn.javaboy.org
表示只加载来自cdn.javaboy.org
的样式表。img-src *
:表示可以从任意地址加载图片。child-src https:
:表示必须使用HTTPS来加载frame
。其他选项可以自行搜索。
Spring security为
Content-Security-Policy
提供了配置方法:
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.anyRequest().authenticated()
.and()
.formLogin()
.and()
.csrf().disable()
.headers()
// 配置之后默认登录页面从外部加载的样式表会失效
.contentSecurityPolicy("default-src 'self'; script-src 'self'; object-src 'none'; style-src cdn.javaboy.org; img-src *; child-src https:");
}
}
CSP还有一种报告模式——
report-only
。在此模式下,CSP策略不是强制性的,如果出现违规行为,还是会继续加载相应的脚本或者样式表,但是会将违规行为报告给一个指定的URI地址,开发者收到违规报告后可以自行处理:
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.anyRequest().authenticated()
.and()
.formLogin()
.and()
.csrf().disable()
.headers()
.contentSecurityPolicy(contentSecurityPolicyConfig -> {
contentSecurityPolicyConfig.policyDirectives("default-src 'self'; script-src 'self'; object-src 'none';
style-src cdn.javaboy.org; img-src *; child-src https:; report-uri http://localhost:8081/report");
contentSecurityPolicyConfig.reportOnyly();
});
}
}
Referrer-Policy
描述用户从哪里进入到当前网页。浏览器默认的取值:
Referrer Policy: no-referrer-when-downgrade
这个表示如果是从HTTPS网址链接到HTTP网址,就不发送
Referer
字段,其他情况发送。开发者可以对此进行修改:
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.anyRequest().authenticated()
.and()
.formLogin()
.and()
.csrf().disable()
.headers()
.referrerPolicy()
.policy(ReferrerPolicyHeaderWriter.ReferrerPolicy.ORIGIN);
}
}
配置的取值有:
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-uri
:表示无论是同源请求还是非同源请求,都发送完整的URL(移除参数信息之后)作为引用地址。
Feature-Policy
提供了一种可以在本页面或包含的
iframe
上启动或禁止浏览器特性的机制(移动端开发使用较多)。例如,如果想要禁用震动和定位API:
Feature-Policy: vibrate 'none'; geolocation 'none'
Spring security配置如下:
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.anyRequest().authenticated()
.and()
.formLogin()
.and()
.csrf().disable()
.headers()
.featurePolicy("vibrate 'none'; geolocation 'none'");
}
}
Clear-Site-Data
一般用在注销登录响应头中,表示告诉浏览器清除当前网站相关的数据(cookie、cache、storage等)。可以通过具体的参数指定想要清除的数据,也可以通过"*"表示清除所有数据。
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.anyRequest().authenticated()
.and()
.formLogin()
.and()
.logout()
// 清除浏览器所有和当前网站相关的数据
.addLogoutHandler(new HeaderWriterLogoutHandler(new ClearSiteDataHeaderWriter(ClearSiteDataHeaderWriter.Directive.ALL)))
.and()
.csrf().disable();
}
}
HTTP通信安全主要从三个方面入手:
- 使用HTTPS代替HTTP。
Strict-Transport-Security
配置。- 代理服务器配置。
具体的配置可以后期来了解。