1-背景
1.1-Cookie
和Session
简介
HTTP
最开始是一个用来展示信息的无状态协议,但是由于太好用,很快就用来开发有状态的交互应用,因此有了Javascript
、Cookie
和 Session
。
Session
由后台实现,但其状态完全由客户端传递的Cookie
(即Session Id
)驱动。
Cookie
是由浏览器实现并管理,但浏览器和前端一般不直接使用Cookie,它只是:
- 按规则(主要是范围和有效期)保管
Cookie
; - 按规则在指定的请求中发送
Cookie
。
Cookie的创建和使用由服务器主导:
- 服务器通过
Set-Cookie
响应头设置Cookie,并通过具体属性控制Cookie的使用范围和有效期; - 通过
Cookie
请求头获取Session Id
,并关联Session
来管理用户状态。
早期Cookie
还被用来存储浏览器状态,但浏览器因安全问题对Cookie
的使用日趋严格,以及浏览器本地存储
技术的诞生,Cookie
越来越专注于成为 Session
的附属品,也只应用作Session
的附属品。
1.2-问题
由于HTTP
协议(现在叫体系更合适)本身的灵活性,尤其是iframe
和ajax
两大支持跨域的特性,让Cooke
始终笼罩在CSRF(跨站请求伪造)
的阴影之下。
由于以下三方因素的共同作用:
Cookie
跨前后端------由浏览器实现、却主要由服务器端使用;- 浏览器对
Cookie
的限制日趋严格,本身就是为了解决跨域场景下的CSRF
安全漏洞; - 前端技术的蓬勃发展带来的前后端分离架构。
带来的问题和冲突:
- 项目中
Cookie
跨域问题层出不穷,历史经验的不断失效; - 前后端之间存在
Cookie
跨域问题的知识盲区和权责空白; - 使用
Cookie
限制太多,不使用Cookie
安全风险和兼容代码太大。
2-潜在替代方案
坊间一度有废弃Cookie
的呼声,但都只是针对Cookie
限制过多的问题,并没有解决Cookie
限制背后的安全问题。
以下是一些替代性技术的介绍:
2.1-浏览器端存储Web Storage API
浏览器端存储Web Storage API
即刚出现的时候,很多文章一度把它视为 Cookie
的替代品;
它包括永久的Windows.localStorage
和会话级的Windows.sessionStorage
;
优点:
- 绕过了浏览器对
Cookie
的几乎所有限制,前端程序员(JS)控制对浏览器存储拥有完全的控制权; - 通过JS控制,按需发送给后端服务器,减少了
Cookie
泛滥的问题。
缺点:
浏览器端存储
是域名精确绑定的,根-子域名/跨域共享时需要借助其它技术手段,同样面临CSRF
威胁,不良地实现会增加用户的安全风险;浏览器端存储
并不是用来设计做前后端会话协商,浏览器并不会自动将localStorage
或sessionStorage
自动传递给后端,需要程序员自行实现;- 可靠性不如
Cookie
,浏览器会可能会清理掉浏览器端存储
,某些场景(如Safari无痕模式)下浏览器端存储
被禁用;
结论
浏览器端存储
只是浏览器端数据存储能力的补足,并不能替代Cookie
的前后端会话协商的作用。
2.2-自定义请求头
使用自定义请求头作为Session Id
的传递方案,也是很多团队共同的选择。
优点:
- 绕过了浏览器对
Cookie
的大多数限制,易于跨域传递; - 按需发送,减少
Cookie
泛滥问题。
缺点
- 自定义请求头缺少浏览器对
Cookie
级别的安全保障,使用自定义请求头管理用户状态,极大地增加了跨站请求伪造和信息泄露风险; - 自定义请求头没有持久化机制,需要借助
sessionStorage
等技术来实现刷新不丢失和跨页签共享; - 自定义请求头没有生命周期管理机制,不良的实现,反而增加状态丢失或状态不一致问题。
结论
自定义请求头只是绕过了浏览器的安全限制,只会让浏览器对Cookie
安全提升的努力付诸东流。
3-合理的解决方案
3.1-Cookie使用原则
Cookie
发展这么多年,其实其作用和限制已经非常清晰,只要遵循以下原则使用,就能在安全性、兼容性和成本上取得最佳的平衡:
Cookie
依然是浏览器端与服务端状态协商的最佳实现,成本最低、安全性最高;- 仅将
Cookie
用于浏览器与服务端状态协商 场景,避免Cookie
泛滥和请求过大; - 由后端程序员主导
Cookie
的规范使用 ,对前端程序员透明(设置HttpOnly
),权责清晰,且可避免浏览器升级事来的兼容性问题; - 前后端最好不要跨域 ,这样服务器默认配置(不允许跨域)即可打通前后端状态,且安全性最高,几乎没有
CSRF
风险; - 如果非要跨域,后端需要通过
Set-Cookie
和CORS(跨域资源共享)系列
响应头,明确设置允许跨域的范围; - 不要设置
Cookie
的Domain
,这样才兼具安全性、避免Cookie
泛滥和请求过大问题,跨域时使用CORS(跨域资源共享)系列
响应头; - 为App端、服务间调用等引入其它状态协商机制时,一定要与浏览器接口分开、且使用默认配置(不开放跨域)------防止接口被滥用在浏览器端,造成
CSRF
风险。
3.2-HTTP协议安全配置
3.2.1-场景一:前后端同域
前后端同域的场景配置最简单也最安全,只需要Set-Cookie
响应头满足以下规则:
yaml
Set-Cookie: <cookie-name>=<cookie-value>; HttpOnly;
关键点:
- 设置自定义
cookie名称
和cookie值
; - 设置
HttpOnly
,让Cookie
仅被浏览器托管,不能被前端代码访问; - 不设置
Domain
、SameSite
和Secure
头,兼具简单性、安全性和适应性; - 不设置
Max-Age
、Expire
等属性,则代表Cookie是会话级,浏览器关闭时会自动清理Cookie。
3.2.2-场景二:前后端跨域
通过Set-Cookie
和CORS(跨域资源共享)系列
响应头组合配置,可以保证Cookie
安全可控的同时,又支持前后端跨域。
0-前题条件
前后端跨域的场景,后端需要部署https域下,前端没有限制;
1-Set-Cookie
响应头
yaml
Set-Cookie: <cookie-name>=<cookie-value>; HttpOnly; SameSite=None; Secure
关键点:
- 设置自定义
cookie名称
和cookie值
; - 设置
HttpOnly
,让Cookie
仅被浏览器托管,不能被前端代码访问; - 设置
SameSite=None
,(协议强制)同时设置Secure
,让后台在https
的保护下,可以跨域共享Cookie
; - 不设置
Domain
,避免Cookie
泄漏、泛滥、冲突;
2-CORS(跨域资源共享)系列
响应头
yaml
Access-Control-Allow-Credentials: true
Access-Control-Allow-Origin: <与请求Origin一致:[协议]://[域名]:[端口]>
Access-Control-Allow-Methods: <GET>, <POST>, ...
Access-Control-Allow-Headers: <自定义请求头1>, <自定义请求头2>, ...
关键点:
- 通过
Access-Control-Allow-Credentials
允许在跨域请求中传递Cookie,固定值true
即可; - 通过
Access-Control-Allow-Origin
限制允许的前端域,注意一定要先检查Origin并动态设置,后文会介绍Spring框架通过配置允许多个域的方法; - 通过
Access-Control-Allow-Headers
限制允许的请求方法; - 可选:通过
Access-Control-Allow-Headers
允许跨域使用的自定义头,仅在需要跨域传递自定义请求头的情况下设置。
3.2.3-场景三:后端嵌入iframe中
当后端接口被嵌入在iframe
中时情况更复杂,需要在场景二设置的基本上,增加Content-Security-Policy
响应头。
后端接口通常是随前端静态资源被嵌入
iframe
中,通常设置前端代理(nginx)即可,后端无须设置。
yaml
Content-Security-Policy: frame-ancestors 'self' [协议1]://[域名1]:[端口1] [协议2]://[域名2]:[端口2] ...;
关键点:
- 后端响应时增加
Content-Security-Policy
响应头; - 响应头的值包含
frame-ancestors
指令,以指明当前接口允许被iframe
嵌入的域,使用空格分隔多个参数; 'self'
参数代指同域,即允许同域嵌入;[协议]://[域名]:[端口]
代指指定域,,
详情参考MDN文档:developer.mozilla.org/zh-CN/docs/...
3.3-Spring项目HTTP服务端安全配置
3.3.1-场景一:前后端同域
Spring MVC
项目
在Spring属性文件中设置:
yaml
server:
servlet:
session:
cookie:
name: [自定义SessionId的Cookie名]
http-only: true
Spring Webflux项目
在Spring属性文件中设置:
yaml
server:
reactive:
session:
cookie:
name: [自定义SessionId的Cookie名]
http-only: true
3.3.2-场景二:前后端跨域
Spring MVC
项目
在Spring属性文件中设置cookie:
yaml
server:
servlet:
session:
cookie:
name: [自定义SessionId的Cookie名]
http-only: true
same-site: none
secure: true
通过Java代码设置CORS
:
java
@Configuration
public class GlobalCorsConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowCredentials(true)
.allowedMethods("GET", "POST", "PUT", "DELETE")
.allowedOrigins("[协议1]://[域名1]:[端口1]", "[协议2]://[域名2]:[端口2]"/*, ...*/);
}
}
Spring Webflux
项目
在Spring属性文件中设置cookie:
yaml
server:
reactive:
session:
cookie:
name: [自定义SessionId的Cookie名]
http-only: true
same-site: none
secure: true
通过Java代码设置CORS
:
java
@Configuration
public class GlobalCorsConfig implements WebFluxConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowCredentials(true)
.allowedMethods("GET", "POST", "PUT", "DELETE")
.allowedOrigins("[协议1]://[域名1]:[端口1]", "[协议2]://[域名2]:[端口2]"/*, ...*/);
}
}
Spring Cloud Gateway
项目
所有配置都可在项目文件中设置:
yaml
server:
reactive:
session:
cookie:
name: [自定义SessionId的Cookie名]
http-only: true
same-site: none
secure: true
spring:
cloud:
gateway:
server:
webflux:
globalcors:
cors-configurations:
'[/**]':
allow-credentials: true
allowed-methods: GET,POST,PUT,DELETE
allowed-origin-patterns:
- "[协议1]://[域名1]:[端口1]"
- "[协议2]://[域名2]:[端口2]"
# ...
3.3.3-场景三:后端接口被嵌入在iframe
中
在场景二的基础上,增加以CSP配置:
Spring MVC
项目
使用全局过滤器为HttpServletResponse
实例添加CSP
响应头配置:
java
@WebFilter("/**")
public class CSPFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
HttpServletResponse httpServletResponse = (HttpServletResponse) response;
httpServletResponse.setHeader("Content-Security-Policy", "frame-ancestors 'self' [协议1]://[域名1]:[端口1] [协议2]://[域名2]:[端口2] ...;");
chain.doFilter(request, response);
}
}
Spring Webflux
项目
使用WebFilter添加CSP
响应头配置:
java
@Component
@Order(0)
public class CSPWebFilter implements WebFilter {
@Override
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
exchange.getResponse().getHeaders().set("Content-Security-Policy", "frame-ancestors 'self' [协议1]://[域名1]:[端口1] [协议2]://[域名2]:[端口2] ...;");
return chain.filter(exchange);
}
}
Spring Cloud Gateway
项目
Spring Cloud Gateway提供了配置来设置CSP
响应头:
yaml
spring:
cloud:
gateway:
server:
webflux:
filter:
#安全头配置,允许在iframe中使用
secure-headers:
frame-options: "SAMEORIGIN"
content-security-policy: "frame-ancestors 'self' [协议1]://[域名1]:[端口1] [协议2]://[域名2]:[端口2] ...;"