Cookie安全使用指南

1-背景

1.1-CookieSession简介

HTTP最开始是一个用来展示信息的无状态协议,但是由于太好用,很快就用来开发有状态的交互应用,因此有了JavascriptCookieSession

Session由后台实现,但其状态完全由客户端传递的Cookie(即Session Id)驱动。

Cookie是由浏览器实现并管理,但浏览器和前端一般不直接使用Cookie,它只是:

  1. 按规则(主要是范围和有效期)保管Cookie
  2. 按规则在指定的请求中发送Cookie

Cookie的创建和使用由服务器主导:

  1. 服务器通过Set-Cookie响应头设置Cookie,并通过具体属性控制Cookie的使用范围和有效期;
  2. 通过Cookie请求头获取Session Id,并关联Session来管理用户状态。

早期Cookie还被用来存储浏览器状态,但浏览器因安全问题对Cookie的使用日趋严格,以及浏览器本地存储技术的诞生,Cookie 越来越专注于成为 Session的附属品,也只应用作Session的附属品。

1.2-问题

由于HTTP协议(现在叫体系更合适)本身的灵活性,尤其是iframeajax两大支持跨域的特性,让Cooke始终笼罩在CSRF(跨站请求伪造) 的阴影之下。

由于以下三方因素的共同作用:

  1. Cookie跨前后端------由浏览器实现、却主要由服务器端使用;
  2. 浏览器对Cookie的限制日趋严格,本身就是为了解决跨域场景下的CSRF安全漏洞;
  3. 前端技术的蓬勃发展带来的前后端分离架构。

带来的问题和冲突:

  1. 项目中Cookie跨域问题层出不穷,历史经验的不断失效;
  2. 前后端之间存在Cookie跨域问题的知识盲区和权责空白;
  3. 使用Cookie限制太多,不使用Cookie安全风险和兼容代码太大。

2-潜在替代方案

坊间一度有废弃Cookie的呼声,但都只是针对Cookie限制过多的问题,并没有解决Cookie限制背后的安全问题。

以下是一些替代性技术的介绍:

2.1-浏览器端存储Web Storage API

浏览器端存储Web Storage API即刚出现的时候,很多文章一度把它视为 Cookie的替代品;

它包括永久的Windows.localStorage和会话级的Windows.sessionStorage

优点:

  1. 绕过了浏览器对Cookie的几乎所有限制,前端程序员(JS)控制对浏览器存储拥有完全的控制权;
  2. 通过JS控制,按需发送给后端服务器,减少了Cookie泛滥的问题。

缺点:

  1. 浏览器端存储是域名精确绑定的,根-子域名/跨域共享时需要借助其它技术手段,同样面临CSRF威胁,不良地实现会增加用户的安全风险;
  2. 浏览器端存储并不是用来设计做前后端会话协商,浏览器并不会自动将localStoragesessionStorage自动传递给后端,需要程序员自行实现;
  3. 可靠性不如Cookie,浏览器会可能会清理掉浏览器端存储,某些场景(如Safari无痕模式)下浏览器端存储被禁用;

结论

浏览器端存储只是浏览器端数据存储能力的补足,并不能替代Cookie的前后端会话协商的作用。

2.2-自定义请求头

使用自定义请求头作为Session Id的传递方案,也是很多团队共同的选择。

优点:

  1. 绕过了浏览器对Cookie的大多数限制,易于跨域传递;
  2. 按需发送,减少Cookie泛滥问题。

缺点

  1. 自定义请求头缺少浏览器对Cookie级别的安全保障,使用自定义请求头管理用户状态,极大地增加了跨站请求伪造和信息泄露风险;
  2. 自定义请求头没有持久化机制,需要借助sessionStorage等技术来实现刷新不丢失和跨页签共享;
  3. 自定义请求头没有生命周期管理机制,不良的实现,反而增加状态丢失或状态不一致问题。

结论

自定义请求头只是绕过了浏览器的安全限制,只会让浏览器对Cookie安全提升的努力付诸东流。

3-合理的解决方案

3.1-Cookie使用原则

Cookie发展这么多年,其实其作用和限制已经非常清晰,只要遵循以下原则使用,就能在安全性、兼容性和成本上取得最佳的平衡:

  1. Cookie依然是浏览器端与服务端状态协商的最佳实现,成本最低、安全性最高;
  2. 仅将Cookie用于浏览器与服务端状态协商 场景,避免Cookie泛滥和请求过大;
  3. 由后端程序员主导Cookie的规范使用 ,对前端程序员透明(设置HttpOnly),权责清晰,且可避免浏览器升级事来的兼容性问题;
  4. 前后端最好不要跨域 ,这样服务器默认配置(不允许跨域)即可打通前后端状态,且安全性最高,几乎没有CSRF风险;
  5. 如果非要跨域,后端需要通过Set-CookieCORS(跨域资源共享)系列响应头,明确设置允许跨域的范围;
  6. 不要设置CookieDomain,这样才兼具安全性、避免Cookie泛滥和请求过大问题,跨域时使用CORS(跨域资源共享)系列响应头;
  7. 为App端、服务间调用等引入其它状态协商机制时,一定要与浏览器接口分开、且使用默认配置(不开放跨域)------防止接口被滥用在浏览器端,造成 CSRF风险。

3.2-HTTP协议安全配置

3.2.1-场景一:前后端同域

前后端同域的场景配置最简单也最安全,只需要Set-Cookie响应头满足以下规则:

yaml 复制代码
Set-Cookie: <cookie-name>=<cookie-value>; HttpOnly;

关键点:

  1. 设置自定义cookie名称cookie值
  2. 设置HttpOnly,让Cookie仅被浏览器托管,不能被前端代码访问;
  3. 不设置DomainSameSiteSecure头,兼具简单性、安全性和适应性;
  4. 不设置Max-AgeExpire等属性,则代表Cookie是会话级,浏览器关闭时会自动清理Cookie。

3.2.2-场景二:前后端跨域

通过Set-CookieCORS(跨域资源共享)系列响应头组合配置,可以保证Cookie安全可控的同时,又支持前后端跨域。

0-前题条件

前后端跨域的场景,后端需要部署https域下,前端没有限制;

1-Set-Cookie响应头
yaml 复制代码
Set-Cookie: <cookie-name>=<cookie-value>; HttpOnly; SameSite=None; Secure

关键点:

  1. 设置自定义cookie名称cookie值
  2. 设置HttpOnly,让Cookie仅被浏览器托管,不能被前端代码访问;
  3. 设置SameSite=None,(协议强制)同时设置Secure,让后台在https的保护下,可以跨域共享Cookie
  4. 不设置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>, ...

关键点:

  1. 通过Access-Control-Allow-Credentials允许在跨域请求中传递Cookie,固定值true即可;
  2. 通过Access-Control-Allow-Origin限制允许的前端域,注意一定要先检查Origin并动态设置,后文会介绍Spring框架通过配置允许多个域的方法;
  3. 通过Access-Control-Allow-Headers限制允许的请求方法;
  4. 可选:通过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] ...;

关键点:

  1. 后端响应时增加Content-Security-Policy响应头;
  2. 响应头的值包含frame-ancestors指令,以指明当前接口允许被iframe嵌入的域,使用空格分隔多个参数;
  3. 'self'参数代指同域,即允许同域嵌入;
  4. [协议]://[域名]:[端口]代指指定域,,

详情参考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] ...;"

4-参考资源

相关推荐
fakerth17 小时前
【OpenHarmony】文件访问接口模块架构
架构·操作系统·openharmony
尘世中一位迷途小书童18 小时前
Monorepo 工具大比拼:为什么我最终选择了 pnpm + Turborepo?
前端·架构
数据智能老司机18 小时前
LLM 提示工程——理解 LLM
gpt·架构·llm
尘世中一位迷途小书童18 小时前
2025年了,你还在用传统的多仓库管理吗?Monorepo 架构深度解析
前端·架构
数据智能老司机19 小时前
LLM 提示工程——提示工程入门
gpt·架构·llm
自由的疯19 小时前
Java Jenkins+Docker部署jar包
java·后端·架构
自由的疯19 小时前
Java Jenkins、Dockers和Kubernetes有什么区别
java·后端·架构
数据智能老司机20 小时前
构建 Medallion 架构——构建 Silver 层
大数据·架构·数据分析
文火冰糖的硅基工坊20 小时前
[嵌入式系统-123]:中高端图形处理器RM Mali-G610 MP4 GPU 是 ARM 公司推出的基于 Valhall 架构 的移动 GPU
arm开发·ai·架构·嵌入式·gpu