本文将详细介绍Spring Security中防范Csrf攻击的实现代码解析。
什么是Csrf攻击
Csrf全称为Cross-site request forgery,即跨站请求伪造。它利用用户在已经登录的网站中的权限来进行恶意攻击,而用户却毫不知情。攻击者可以通过各种方式获取并篡改用户的Cookie,再利用这些Cookie发起跨站请求伪造攻击,使得受害者被误导到执行某些意料之外的操作。
SpringSecurity中的Csrf攻击防御
SpringSecurity中提供了防范Csrf攻击的实现,包括Token的生成和验证。当用户访问页面时,服务器会将一个固定的Token放在该页面的表单中或者请求头中,在后续的请求中,服务器会校验这个Token是否匹配,如果不匹配,则拒绝该请求。这样就防止了Csrf攻击。
SpringSecurity中Csrf攻击防御的实现
1.启用Csrf攻击防御
@EnableWebSecurity
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
...
@Override protected void configure(HttpSecurity http) throws Exception {
...
http.csrf().csrfTokenRepository(csrfTokenRepository())
.and().addFilterAfter(csrfHeaderFilter(), CsrfFilter.class);
}
...
}
在Spring Security中启用Csrf攻击的防御,需要在configure方法中配置http.csrf()。需要注意的是,csrf()方法返回的是一个CsrfConfigurer对象。通过其csrfTokenRepository方法可以设置此对象用于持久化CsrfToken的CsrfTokenRepository,而通过csrfHeaderFilter方法可以指定当Csrf攻击发生时,Spring Security应该使用哪个Filter对它进行处理。
2.CsrfToken的生成和存储
public class HttpSessionCsrfTokenRepository implements CsrfTokenRepository {
...
public CsrfToken generateToken(HttpServletRequest request) {
String token = UUID.randomUUID().toString();
return new DefaultCsrfToken(this.headerName, this.parameterName, token);
}
...
}
Spring Security提供了默认的CsrfTokenRepository——HttpSessionCsrfTokenRepository。在此对象中,generateToken方法会生成一个固定的CsrfToken,并通过DefaultCsrfToken类进行封装,然后将封装后的CsrfToken存储在HttpSession中。
3.前端获取CsrfToken
var token = $("meta[name='_csrf']").attr("content");
var header = $("meta[name='_csrf_header']").attr("content");
$(document).ajaxSend(function(e, xhr, options) {
xhr.setRequestHeader(header, token);
});
为了获取服务器生成的CsrfToken,需要在前端页面中引入一个标签,标签的name属性为_csrf,在SpringSecurity中,会将CsrfToken存储到这个name属性对应的content中。
4.后端校验CsrfToken
public class CsrfFilter extends OncePerRequestFilter {
...
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
...
CsrfToken csrfToken = this.csrfTokenRepository.loadToken(request);
...
if (csrfToken == null) {
if ("GET".equals(request.getMethod())) {
...
} else {
...
response.sendError(HttpServletResponse.SC_FORBIDDEN, "Missing CSRF token");
return;
}
}
...
}
...
}
在处理请求的Controller中,Spring Security会自动调用CsrfFilter过滤器来校验该请求中的CsrfToken与之前存储在HttpSession中的CsrfToken是否一致。如果不一致,则会给用户返回一个403的Not Accessible错误。
示例
示例1:前后端分离的Csrf防御
假设前后端分离的SpringBoot项目结构如下:
my-spring-boot
├── src
| ├── main
| | ├── java
| | | ├── com.example
| | | | ├── web
| | | | | ├── ApiController.java
| | | | ├── MySpringBootApplication.java
| | ├── resources
| | | ├── static
| | | | ├── csrf-example.html
对于这个场景,需要在前端页面生成CsrfToken,然后在Ajax中发送请求时携带CsrfToken。具体代码如下:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="_csrf" th:content="${_csrf.token}" />
<meta name="_csrf_header" th:content="${_csrf.headerName}" />
<title>CSRF Example</title>
</head>
<body>
<h1>Spring Boot CSRF Example</h1>
<button id="ajax-get-btn">GET Request</button>
<button id="ajax-post-btn">POST Request</button>
<script src="/webjars/jquery/jquery.min.js"></script>
<script>
$(function () {
$('#ajax-get-btn').click(function() {
$.ajax({
url: '/api/hello?name=GET',
type: 'GET',
success: function(data) {
$('#data').show();
$('#data').text(data);
}
});
});
$('#ajax-post-btn').click(function() {
$.ajax({
url: '/api/hello?name=POST',
type: 'POST',
data: {},
success: function(data) {
$('#data').show();
$('#data').text(data);
}
});
});
});
</script>
<div id="data"></div>
</body>
</html>
在ApiController中,需要通过@PreAuthorize注解将请求限制为USER角色,防止不必要的请求。代码如下:
@RestController
@RequestMapping("/api")
public class ApiController {
@GetMapping("/hello")
@PreAuthorize("hasRole('USER')")
public String helloGet() {
return "hello GET";
}
@PostMapping("/hello")
@PreAuthorize("hasRole('USER')")
public String helloPost() {
return "hello POST";
}
}
示例2:Thymeleaf与SpringSecurity的Csrf防御
假设当前项目使用了Thymeleaf模板引擎,结构如下:
my-spring-boot
├── src
| ├── main
| | ├── java
| | | ├── com.example
| | | | ├── MySpringBootApplication.java
| | ├── resources
| | | ├── templates
| | | | ├── csrf-example.html
对于这个场景,可以使用input标签的hidden类型来生成CsrfToken。具体代码如下:
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8" />
<meta name="_csrf" th:content="${_csrf.token}" />
<meta name="_csrf_header" th:content="${_csrf.headerName}" />
<title>CSRF Example</title>
</head>
<body>
<h1>Spring Boot CSRF Example</h1>
<form action="#" th:action="@{/logout}" method="post">
<input type="submit" value="Logout" />
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}" />
</form>
</body>
</html>
需要注意的是,在模板引擎中必须使用SpringSecurity提供的ThymeleafDialect,此Dialect会帮助我们自动生成hidden类型的input标签。代码如下:
@Configuration
public class ThymeleafConfig {
@Bean
public SpringSecurityDialect securityDialect() {
return new SpringSecurityDialect();
}
}
通过在Thymeleaf模板中调用巨量其提供的<sec:csrfInput />
标签即可自动生成CsrfToken:
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org"
xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity4">
<head>
<meta charset="UTF-8" />
<title>CSRF Example</title>
</head>
<body>
<h1>Spring Boot CSRF Example</h1>
<form action="#" th:action="@{/logout}" method="post">
<input type="submit" value="Logout" />
<sec:csrfInput />
</form>
</body>
</html>
结论
通过上述对SpringSecurity中防止Csrf攻击的实现代码解析,我们了解到SpringSecurity具有非常优秀的Csrf防御能力,同时也描述了如何在前后端分离的和Thymeleaf模板引擎的场景下使用CsrfToken。
本站文章如无特殊说明,均为本站原创,如若转载,请注明出处:SpringSecurity的防Csrf攻击实现代码解析 - Python技术站