1.解决跨域访问的问题
一、CORS简述
要说明CORS(Cross Origin Resourse-Sharing) 跨站资源共享,就必须先说同源策略。长话短说,同源策略就是向服务端发起请求的时候,以下三项必须与当前浏览器应用一致:域名、端口、协议。用白话说:就是你的应用发送请求不能访问别人的资源,否则浏览器就会限制你。
当然也有例外,如:img、srcipt、iframe等
资源引用的HTML标签不受同源策略的限制。
但是我们实际开发中又经常会跨站访问,比如前后端分离的应用是分开部署的,在浏览器看来是两个域。所以同源策略是用来禁止跨域访问的,CORS正好相反是根据自己的需求与规则,有限的开放部分资源的共享。
二、Spring-CORS规则基础配置
想在Spring或Spring Boot的web环境下实现跨域资源共享,主要有三种实现方式:
-
@CrossOrigin注解
,这个注解是作用于Controller类或者请求方法上的,实现局部接口的跨域资源共享。 -
实现WebMvcConfigurer接口addCorsMappings方法
,实现全局配置的跨域资源共享。 -
注入CorsFilter过滤器
,实现全局配置的跨域资源共享。推荐使用。
这三种实现方式在我的另外一篇文章《SpringBoot解决跨域访问的问题》中已经介绍过,这里就不多做说明了。
三、Spring Security 中的配置CORS
当我们的应用使用了Spring Security之后,我们会发现上面的配置方法全部失效。此时需要在spring security的WebSecurityConfigurerAdapter中的configure(HttpSecurity http)配置方法,加上http.cors()
配置,第二小节中的配置才会生效。
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.cors().and()
...
}
}
另外Spring Security为我们提供了一种新的CORS规则的配置方法:CorsConfigurationSource 。使用这种方法实现的效果等同于注入一个CorsFilter过滤器。
@Bean
CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowedOrigins(Arrays.asList("http://localhost:8888"));
configuration.setAllowedMethods(Arrays.asList("GET","POST"));
configuration.applyPermitDefaultValues();
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration);
return source;
}
四、深入了解
Spring 里那么多种 CORS 的配置方式,到底有什么区别
五、前端测试代码
请将如下代码,放入与jwt-server不同域的应用下面,进行测试。
- jwt-server的端口这里是8889
- 使用jwt令牌进行访问
window.onload = function () {
var headers = {};
headers['JWTHeaderName'] = "<这里替换为jwt令牌>";
$.ajax({
url: 'http://localhost:8889/hello',
type: "POST",
headers: headers,
success: function (data) {
alert("跨域请求配置成功")
},
error: function (data) {
alert("跨域请求配置失败")
}
});
}
2.CSRF跨站攻击防护
一、什么是CSRF
他只防御POST/DELETE/PUT
的请求方式,针对GET请求不防御
很多朋友在学习Spring Security的时候,会将CORS(跨站资源共享)和CSRF(跨站请求伪造)弄混,以为二者是一回事。其实不是,先解释一下:
-
CORS(跨站资源共享)
是局部打破同源策略的限制,使在一定规则下HTTP请求可以突破浏览器限制,实现跨站访问。 -
CSRF是一种网络攻击方式
,也可以说是一种安全漏洞,这种安全漏洞在web开发中广泛存在。我们要需要堵上这个漏洞。
当我们使用Spring Security的时候,这种CSRF漏洞默认的被防御掉了。但是你会发现在跨域请求的情况下,我们的POST、DELETE、PUT等HTTP请求方式失效了。所以在笔者之前的文章中,我们使用http.csrf.disable()
暂时关闭掉了CSRF的防御功能,但是这样是不安全的,那么怎么样才是正确的做法呢?就是本文需要向大家介绍的内容。
二、CSRF的攻击方式
通常的CSRF攻击方式如下:
- 你登录了网站A,攻击者向你的网站A账户发送留言、或者伪造嵌入页面,带有危险操作链接。
- 当你在登录状态下点击了攻击者的连接,因此该链接对你网站A的账户进行了操作。
- 这个操作是你在网站A中主动发出的,并且也是针对网站A的HTTP链接请求,同源策略无法限制该请求。
如果你不小心点击的连接,是针对网站的数据操作,如:转出货币,你的钱就被转走了。因为点击"链接"的请求是HTTP的GET请求,所以正规的开发人员的做法是不要使用GET方法进行数据操作,只使用GET方法进行数据查询。
三、如何防御CSRF攻击
- 为系统中的每一个连接请求加上一个token,这个token是随机的,服务端对该token进行验证。破坏者在留言或者伪造嵌入页面的时候,无法预先判断CSRF token的值是什么,所以当服务端校验CSRF token的时候也就无法通过。所以这种方法在一定程度上是靠谱的。
- 但是如果你的电脑中毒,网络信息被劫持使用token的方法仍然不安全。所以没有绝对的安全,道高一次魔高一丈。作为开发者,我们就做到我们应该做到的。
- 跳转提示:当用户不小心点击了第三方连接,合格的应用应该提示用户相关的风险!由用户自己确认是否真的要跳转或者执行第三方连接,或者就干脆不让非可信连接在留言区等地方存在。
四、Spring Security的CSRF token攻击防护
首先,我们要先开启防护功能,在用户登陆操作之后,生成的CSRF Token就保存在cookies中。
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
//开启csrf防御
http.csrf()
//使用cookie去存csrf令牌,允许浏览器当前cookie可以被js脚本读取
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
//忽略登入认证的请求,/authentication 登录认证的请求
.ignoringAntMatchers("/authentication");
.and()
...
}
}
- 使用CookieCsrfTokenRepository生成CSRF Token放入cookie,并设置cookie的HttpOnly=false,允许读取该cookie。这样非浏览器等无法自动维护cookie的客户端可以读取cookie中的CSRF Token,以供后续资源请求中使用。
- 使用ignoringAntMatchers开放一些不需要进行CSRF防护的访问路径,比如:登录授权。
至此,我们生成了CSRF token保存在了cookies中,浏览器向服务端发送的HTTP请求,都要将CSRF token带上,服务端校验通过才能正确的响应。这个校验的过程并不需要我们自己写代码实现,Spring Security会自动处理。但是我们需要关注前端代码,如何正确的携带CSRF token。
五、前端请求携带CSRF Token的方式
- 默认情况下,
CookieCsrfTokenRepository
会向cookies中写入一个key为XSRF-TOKEN
的cookie。 -
CookieCsrfTokenRepository
在跨站防御验证的过程中,可以从HTTP Header中读取X-XSRF-TOKEN
或者从HTTP参数中读取_csrf
,作为跨站防御验证的令牌.
注意:这里是
XSRF-TOKEN
和X-XSRF-TOKEN
,没有写错。而不是CSRF-TOKEN
和X-CSRF-TOKEN
在thymeleaf模板中可以使用如下方式,在发送HTTP请求的时候携带CSRF Token。如果是前后端分离的应用,或者其他模板引擎,酌情从cookies中获取CSRF Toekn。
5.1.在Header中携带CSRF token
var headers = {};
headers['X-XSRF-TOKEN'] = "${_csrf.token}";
$.ajax({
headers: headers,
});
5.2.直接作为参数提交。
$.ajax({
data: {
"_csrf": "${_csrf.token}"
}
});
5.3.form表单的隐藏字段
<input type="hidden" name="${_csrf.parameterName}" value="${_csrf.token}">
- 请求授权认证返回
XSRF-TOKEN
- 再次请求的时候携带时,
X-XSRF-TOKEN
等于上面的值
3.JWT集群应用方案
一、回顾JWT授权与验证流程
在我们之前实现的JWT应用中,登录认证的Controller和令牌验证的Filter是在同一个应用中的。
要想使用JWT访问资源需要
- 先使用用户名和密码,去Controller换取JWT令牌
- 然后才能进行资源的访问,资源接口的前端由一个"JWT验证Filter"负责校验令牌和授权访问。
二、集群应用
那我们可以思考一个问题,如果上面的应用部署两份形成集群应用,也就是“应用A”和“应用B”,代码是同一套代码。如果认证过程是在“应用A”获取的JWT令牌,可以访问“应用B”的接口资源么?(如下图)
答案是:可以。因为两个应用中没有在内存(session)中保存中保存任何的状态信息,所有的信息都是去数据库里面现加载的。所以只要这两个应用,使用同一个数据库、同一套授权数据、同一个用于签名和解签的secret。就可以实现“应用A”的认证、在“应用B”中被承认。
那么另外一个问题来了,对于上面的集群应用,“应用A”和“应用B”实际上是一份代码部署两份。如果“应用A”和“应用B”是真正的两套代码的部署结果呢?答案仍然是可以。前提是你的认证Controller代码和鉴权Filter代码的实现逻辑是一样的、校验规则是一样的。使用同一个数据库、同一套授权数据、同一个用于签名和解签的secret。所以JWT服务端应用可以很容易的扩展。
条件要求
满足以上就可以在A服务器上鉴权授权返回jwt令牌,然后去携带jwt令牌,去B服务器请求访问,通过授权
三、独立的授权服务
基于JWT的这种无状态的灵活性,它很容易实现应用横向扩展。只要具备以下条件任何JWT的应用都可以整合为一个应用集群。
- 认证Controller代码统一
- 鉴权Filter代码统一、校验规则是一样的。
- 使用同一套授权数据
- 同一个用于签名和解签的secret。
基于这个条件前提,我们完全可以把认证Controller代码单独抽取出来,形成“认证服务器”。如下图所示:
或者我们还可以进一步把所有的Jwt验证鉴权Filter代码单独抽取出来,形成“服务网关”,放在接口资源的前端。当然“服务网关”的功能不只是鉴权、还要有请求转发的功能。
最后剩下的一系列的“接口资源”,实际上就是我们常说的“资源服务器”。