什么是CSRF
CSRF:跨站请求伪造。也可称为一站式攻击。也可写作XSRF。
按照字面意思来理解,跨站请求伪造,意思就是说用户登录了A网站之后,会话没有过期,然后登录了B网站,这个时候B网站中的请求访问了A网站,这个时候A网站就会认为是合法的用户的请求,这个时候用户是无感知的,从而导致用户在A网站的账户出现安全问题。特别是在一些银行之类的网站上如果受到这种攻击,造成的损失是致命的。
CSRF防御
CSRF攻击的原因,就是用户的浏览器会携带cookie,然而服务器不会判断这个cookie是不是这个网站内部发送的,这样我们就没有办法拒绝这样的请求。如果能够在请求中携带一个攻击者无法携带的参数就可以避免CSRF攻击 ,SS中就提供了这样一种机制,称为令牌同步机制
。
就是在服务端会生成一个令牌,这个令牌在httpsession中也保存一份,任何一个请求都需要携带这个令牌,这样就可以验证用户的请求是不是站内的请求。
前后端不分离项目中的csrf
在没有开启csrf之前登录界面的代码,这和我们
开启了csrf之后的登录界面源代码
可以看到这里有一个_csrf
的变量,值是一个后端生成特定的值。这个值在其他的网站中是无法获取的,所以就可以避免scrf攻击。
前后端分离系统的csrf防御
针对于前后端分离的系统,我们的value值不再保存在服务器端而是直接返回给前端,前端每次请求获取这个值进行提交。这个时候我们可能会像cookie也会被第三方网站获取之后发起请求,但是实际上我们的前端系统并不是直接传递cookie的值,而是对cookie进行一定的封装格式化之后传递到后端,具体格式化的方式我们项目的开发者是知道的,但是第三方网站是不知道的,这就实现了前后端分离系统的csrf防御。
后端配置
.csrf()
//将令牌保存到cookie中,允许cookie前端获取,这样前端就可以通过js获取cookie的值
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
这里面涉及到一个类是CsrfFilter
,它里面有一个方法
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
request.setAttribute(HttpServletResponse.class.getName(), response);
//这里后端自己计算csrfToken
CsrfToken csrfToken = this.tokenRepository.loadToken(request);
final boolean missingToken = csrfToken == null;
if (missingToken) {
csrfToken = this.tokenRepository.generateToken(request);
this.tokenRepository.saveToken(csrfToken, request, response);
}
request.setAttribute(CsrfToken.class.getName(), csrfToken);
request.setAttribute(csrfToken.getParameterName(), csrfToken);
if (!this.requireCsrfProtectionMatcher.matches(request)) {
filterChain.doFilter(request, response);
return;
}
//这里从前端获取csrfToken
String actualToken = request.getHeader(csrfToken.getHeaderName());
if (actualToken == null) {
actualToken = request.getParameter(csrfToken.getParameterName());
}
//最后比较是不是相同,如果相同就可以通过,如果不相同还会判断是否是幂等的请求,如果是幂等的请求也会直接通过。
if (!csrfToken.getToken().equals(actualToken)) {
if (this.logger.isDebugEnabled()) {
this.logger.debug("Invalid CSRF token found for "
+ UrlUtils.buildFullRequestUrl(request));
}
if (missingToken) {
this.accessDeniedHandler.handle(request, response,
new MissingCsrfTokenException(actualToken));
}
else {
this.accessDeniedHandler.handle(request, response,
new InvalidCsrfTokenException(csrfToken, actualToken));
}
return;
}
filterChain.doFilter(request, response);
}
我们可以在CookieCsrfTokenRepository
源码中看到如果是从请求头里面获取的话key就是
static final String DEFAULT_CSRF_HEADER_NAME = "X-XSRF-TOKEN";
如果是在请求参数中获取的话就是
static final String DEFAULT_CSRF_PARAMETER_NAME = "_csrf";
跨域
如果源地址与目标地址中,协议、主机、端口有任何一个不同都是跨域的。
对于简单的get请求来说,如果要发起一个跨域请求,则请求头会加入Host、Origin、Reference字段,如果服务器支持跨域则返回的响应头中会包含一个Access-Control-Allow-Origin的字段。这时候浏览器发现了这个字段就不会对跨域请求进行限制,这种是不需要进行预检请求的跨域。
对于不是get的请求,也就是非简单请求,会受限发起一个options的预检请求,请求头Origin告诉服务器当前页面所在的域,请求头Access-Control-Request-Methods告诉服务器即将发起的跨域请求所使用的方法,服务端对此进行判断,如果允许跨域则会给出特定的响应,浏览器进而发起真正的请求。
Spring解决跨域的方法
- 使用
@CrossOrigin
注解,可以用在类和方法上。
编写前端页面
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
<script>
axios.get("http://localhost:8080/public/cors");
</script>
<body>
</body>
</html>
后端接口
@GetMapping("/public/cors")
@ResponseBody
public String cors(){
System.out.println("cors - test");
return "cors test";
}
此时打开前端页面
此时我在后端方法上加上@CrossOrigin
注解重新访问前端页面,此时就不会报错了
- 第二种方式,实现
WebMvcConfigurer
接口,重写addCorsMappings
方法。这是基于SpringMvc的解决方案。这种方式相对于前面更加方便,不用每个接口都加注解。
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedMethods("*")
.allowedOrigins("*")
.allowCredentials(false)
.allowedHeaders("*")
.maxAge(3600)
.exposedHeaders("")
;
}
注释掉原来方法上的注解然后访问
- 第三种方式,springweb给我们提供了一个处理跨域问题的过滤器
CorsFilter
注入一个bean
@Bean
public FilterRegistrationBean<CorsFilter> corsFilter() {
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
CorsConfiguration config = new CorsConfiguration();
config.setAllowCredentials(true);
config.addAllowedOrigin("*");
config.addAllowedHeader("*");
config.addAllowedMethod("*");
source.registerCorsConfiguration("/**", config);
FilterRegistrationBean<CorsFilter> bean = new FilterRegistrationBean<>(new CorsFilter(source));
bean.setOrder(0);
return bean;
}
SS中的CORS跨域处理
如果项目中引入了SS的依赖,就会导致我们前面三种解决跨域的方式中前两种失效,后面一种还需要看它过滤器的优先级是不是比ss中的过滤器的优先级高,是的话就可以生效,否则跨域处理失败。前两种都是基于拦截器处理的,拦截器是处理的很晚的,请求已经经过了过滤器、dispatcherservlet到达controller之前被拦截器处理,在这之前会经过ss的过滤器,所以前两种一定是会失效的。当然,是针对于非简单方式的请求,简单方式的请求依然会生效。
为什么呢?因为非简单请求的预检不会携带认证信息,所以预检请求会被ss的过滤器过滤出去,那么跨域就无法处理了。
var s = axios.get("http://localhost:8080/public/cors");
let post = axios.post('http://localhost:8080/public/post/cors');
在ss中我们可以直接配置
.cors()
.configurationSource(configurationSource())
.and()
CorsConfigurationSource configurationSource(){
CorsConfiguration corsConfiguration = new CorsConfiguration();
corsConfiguration.setAllowedHeaders(Arrays.asList("*"));
corsConfiguration.setAllowedMethods(Arrays.asList("*"));
corsConfiguration.setAllowedOrigins(Arrays.asList("*"));
corsConfiguration.setMaxAge(3600l);
UrlBasedCorsConfigurationSource configurationSource = new UrlBasedCorsConfigurationSource();
configurationSource.registerCorsConfiguration("/**",corsConfiguration);
return configurationSource;
}
文章评论