实验0:SpringSecurity
默认开启CSRF
防护
现在我们在springboot-security
项目的HelloController.java
中新增一个POST
接口:/ok
。
@RestController
public class HelloController {
@GetMapping("/hello")
public String hello(){
return "hello springsecurity";
}
@PostMapping("/ok")
public String ok(){
return "ok";
}
}
当然这个POST接口无法直接在浏览器中发起请求,我们需要借助PostMan来实现POST请求的发送。把浏览器中的Cookie复制到PostMan中。
- 先发GET /hello,正常
- 再发POST /ok,403了。。
那么,问题来了,两个请求都是在登录状态下进行的,为什么GET成功,POST返回403了?
其实SpringSecurity
默认就开启了CSRF
防护,这在上一篇及官网中关于SpringBoot自动配置项那里可以看到。并且SpringSecurity
默认忽略"GET", “HEAD”, “TRACE”, "OPTIONS"等请求,源码如下:
/**
* Specify the {@link RequestMatcher} to use for determining when CSRF should be
* applied. The default is to ignore GET, HEAD, TRACE, OPTIONS and process all other
* requests.
*
* @param requireCsrfProtectionMatcher the {@link RequestMatcher} to use
* @return the {@link CsrfConfigurer} for further customizations
*/
public CsrfConfigurer<H> requireCsrfProtectionMatcher(
RequestMatcher requireCsrfProtectionMatcher) {
Assert.notNull(requireCsrfProtectionMatcher,
"requireCsrfProtectionMatcher cannot be null");
this.requireCsrfProtectionMatcher = requireCsrfProtectionMatcher;
return this;
}
private static final class DefaultRequiresCsrfMatcher implements RequestMatcher {
private final HashSet<String> allowedMethods = new HashSet<>(
Arrays.asList("GET", "HEAD", "TRACE", "OPTIONS"));
/*
* (non-Javadoc)
*
* @see
* org.springframework.security.web.util.matcher.RequestMatcher#matches(javax.
* servlet.http.HttpServletRequest)
*/
@Override
public boolean matches(HttpServletRequest request) {
return !this.allowedMethods.contains(request.getMethod());
}
}
实验1:CSRF GET攻击
CSRF: Cross-Site Request Forgery 跨站请求伪造。一些网站比如知乎、简书中的外部链接,点击之后会有提示(免责声明),此操作有风险是否继续,这便与CSRF密切相关。
结合上篇文章,新建一个SpringBoot
项目,起名spring-security-csrf
,核心依赖为Web
与Thymeleaf
,模拟一个钓鱼网站。
<dependencies>
<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>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
建好项目后,创建一个简单的HelloController.java,包含一个/
的GET
请求,返回一个页面index.html
:
- 后端接口
@Controller
public class HelloController {
@RequestMapping("/")
public String hello(){
return "index";
}
}
- 前端模板
<!DOCTYPE html>
<html >
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
Fishing: <img src="http://hello:8080/hello">
</body>
</html>
Note:
- 以下步骤在Firefox浏览器完成;
- 修改
springboot-security
的后端接口,增加输出打印,方便后续确定请求是否进入后端;
@RestController
@Slf4j
public class HelloController {
@GetMapping("/hello")
public String hello(){
log.info("Hello ");
return "hello";
}
@PostMapping("/ok")
public String ok(){
log.info("ok");
return "ok";
}
}
- 前提:为了模拟不同域名下的请求(即CSRF),我们在本地的
hosts
文件添加如下内容:
127.0.0.1 hello
127.0.0.1 world
实验步骤:
- 启动两个项目:
springboot-security
在8080端口、spring-security-csrf
在8081端口; - 打开浏览器,访问
http://hello:8080
,并完成登录,访问http://hello:8080/hello
接口,观察项目springboot-security
后台打印输出; - 在同一个浏览器,访问
http://world:8081
,默认进入index.html
,同时观察项目springboot-security
后台打印输出;
实验结果:
- 在同一个浏览器(此处为Firefox,Chrome内核的浏览器未成功)的不同Tab下,在
world
域名下请求hello
域名下的GET接口/hello:<img src="http://hello:8080/hello">
,请求成功到达源站后端,实现了CSRF:跨站请求伪造。 - 验证了
SpringSecurity
虽然默认开启CSRF防护,但是幂等请求诸如"GET", “HEAD”, “TRACE”, "OPTIONS"被忽略。
实验2:CSRF POST攻击
在spring-security-csrf
项目中模拟一个按钮操作,发起POST
请求,这里采用原生JavaScript
发起Ajax
的POST
请求:http://hello:8080/ok
<!DOCTYPE html>
<html >
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
Fishing: <img src="http://hello:8080/hello">
<script language="JavaScript"> function click() {
let xhr = new XMLHttpRequest();
xhr.open("POST", "http://hello:8080/ok", true);
xhr.onload = function (e) {
console.log("response: ", e.target);
}
xhr.onerror = function (e) {
console.log("error: ", e)
}
xhr.send(null);
}
click(); </script>
</body>
</html>
实验步骤:
- 启动两个项目:
springboot-security
在8080端口、spring-security-csrf
在8081端口; - 打开浏览器,访问
http://hello:8080
,并完成登录,PostMan访问http://hello:8080/ok
接口,观察项目springboot-security
后台打印输出; - 在同一个浏览器,访问
http://world:8081
,默认进入index.html
,同时观察项目springboot-security
后台打印输出;
实验结果:
- 在同一个浏览器(此处为Firefox,Chrome内核的浏览器未成功)的不同Tab下,在
world
域名下请求hello
域名下的POST接口/ok:http://hello:8080/ok"
,Ajax请求受浏览器同源策略限制,被拦截。CSRF攻击失败。 - 即使通过
PostMan
访问:http://hello:8080/ok"
,附带Cookie
,结果也是被拦截,返回403; - 验证了
SpringSecurity
默认开启CSRF防护,对于非幂等请求诸如"POST", “PUT”, "DELETE"等请求进行拦截。
因此,相比GET
请求,POST
请求相对更安全。