问题复盘:飞书OAuth登录跨域Cookie方案探索与实践

飞书OAuth登录跨域Cookie方案探索与实践

一次前后端分离架构下,从困惑到清晰的完整技术探索历程

一、背景

业务需求

我们的智能平台需要集成飞书OAuth2.0登录,实现统一身份认证。系统采用前后端分离架构:

  • 前端:React单页应用,部署在独立域名
  • 后端:Spring Boot REST API,部署在另一个域名/IP
  • 认证方式:OAuth2.0 + Cookie传递token
  • 核心挑战:前后端跨域,如何让Cookie正常工作?
初始架构
环境 前端地址 后端地址
SIT(本地开发) localhost:3000 localhost:9080
UAT(测试环境) mvpfont-uat.example.com UAT后端IP:9080
PROD(生产环境) mvpfont.example.com PROD后端IP:9080

二、第一个困惑:URL重定向异常

问题现象

飞书授权成功后,用户应该被重定向到前端首页,结果却出现了异常路径:

  • 期望:http://UAT后端IP:3000/dashboard
  • 实际:http://localhost:9080/feishu/UAT后端IP:3000/dashboard URL被当作相对路径拼接到了当前请求路径上!
最初的配置文件
yaml 复制代码
# application.yml  
frontURL:  
  sitURL: localhost:3000/dashboard          # 缺少协议  
  uatURL: UAT后端IP:3000/dashboard       # 缺少协议  
  prodURL: PROD后端IP:3000/dashboard        # 缺少协议
问题根源

response.sendRedirect()的行为:

  • 绝对URL(含http://https://):直接重定向到该地址
  • 相对路径(不含协议):拼接到当前请求的完整路径上

我们的URL缺少协议前缀,被识别为相对路径:

  • 当前请求: http://localhost:9080/feishu/callback
  • 重定向参数: UAT后端IP:3000/dashboard
  • 实际结果: http://localhost:9080/feishu/UAT后端IP:3000/dashboard
解决方案
  1. ​完善配置文件​
bash 复制代码
frontURL:  
  sitURL: http://localhost:3000/dashboard  
  uatURL: https://mvpfont-uat.example.com/dashboard  
  prodURL: https://mvpfont.example.com/dashboard
  1. ​添加防御性代码​
ini 复制代码
String frontendUrl = feishuLoginService.getFrontEndURL();  
if (frontendUrl != null && !frontendUrl.startsWith("http://") && !frontendUrl.startsWith("https://")) {  
    frontendUrl = "http://" + frontendUrl;  
    logger.warn("前端URL缺少协议前缀,已自动添加: {}", frontendUrl);  
}  
response.sendRedirect(frontendUrl);
经验总结

重定向URL务必包含完整的协议前缀(http://https://),否则会被当作相对路径处理。


三、第二个困惑:前端读取不到Cookie

问题现象

后端成功设置了Cookie,但前端JavaScript读取不到:

javascript 复制代码
const token = document.cookie.split('; ').find(row => row.startsWith('access_token='));  
console.log(token);  // undefined

浏览器开发者工具显示Cookie根本没有被保存!

跨域Cookie的三座大山
  1. ​SameSite属性​​:

    • 浏览器默认SameSite=Lax,跨站点请求被拒绝
    • 控制台警告:Cookie "access_token" has been rejected because it is in a cross-site context
  2. ​Secure标志​​:

    • 要设置SameSite=None,必须同时设置Secure标志
    • HTTP环境下SameSite=None会被浏览器忽略
  3. ​Domain属性​​:

    • 跨域场景下,Domain必须正确设置
    • 后端域名UAT后端IP:9080与前端域名mvpfont-uat.example.com完全不同
解决方案:多环境配置 + 智能Domain提取
  1. ​配置文件支持多环境​
yaml 复制代码
spring:  
  profiles:  
    active: uat  # sit / uat / prod  

frontURL:  
  sitURL: http://localhost:3000/dashboard  
  uatURL: https://mvpfont-uat.example.com/dashboard  
  prodURL: https://mvpfont.example.com/dashboard  

backendURL:  
  sitURL: http://localhost:9080  
  uatURL: http://UAT后端IP:9080  
  prodURL: http://PROD后端IP:9080
  1. ​智能提取顶级域名​
typescript 复制代码
private String extractTopLevelDomain(String url) {  
    if (url == null || url.isEmpty()) return null;  
    try {  
        String host = url;  
        if (host.contains("://")) host = host.substring(host.indexOf("://") + 3);  
        if (host.contains(":")) host = host.substring(0, host.indexOf(":"));  
        if (host.contains("/")) host = host.substring(0, host.indexOf("/"));  
        
        if ("localhost".equals(host) || host.matches("^\d+\.\d+\.\d+\.\d+$")) {  
            return null;  // localhost或IP不设置Domain  
        }  
        
        String[] parts = host.split("\.");  
        if (parts.length >= 2) {  
            return "." + parts[parts.length - 2] + "." + parts[parts.length - 1];  
        }  
        return null;  
    } catch (Exception e) {  
        logger.error("提取顶级域名失败: {}", url, e);  
        return null;  
    }  
}
  1. ​手动构建跨域Cookie​
typescript 复制代码
private void addCrossDomainCookie(HttpServletResponse response, String name, String value,  
                                 String path, int maxAge, boolean secure, String domain) {  
    StringBuilder cookieHeader = new StringBuilder();  
    cookieHeader.append(name).append("=").append(value);  
    cookieHeader.append("; Path=").append(path);  
    cookieHeader.append("; Max-Age=").append(maxAge);  
    
    if (domain != null && !domain.isEmpty()) {  
        cookieHeader.append("; Domain=").append(domain);  
    }  
    
    if (secure) {  
        cookieHeader.append("; Secure");  
        cookieHeader.append("; SameSite=None");  
    } else {  
        cookieHeader.append("; SameSite=Lax");  
    }  
    response.addHeader("Set-Cookie", cookieHeader.toString());  
}
经验总结

跨域Cookie三要素:

  1. Domain:设置为共同的顶级域名(如.example.com
  2. SameSite=None:允许跨站点传递
  3. Secure:必须HTTPS(SameSite=None的前提)

四、第三个困惑:HTTPS连HTTP报错

问题现象

修改配置使用HTTPS后,启动服务访问时出现错误:

sql 复制代码
java.lang.IllegalArgumentException: Invalid character found in method name [0x160x030x010x07...]

日志显示一堆十六进制数据!

问题分析
  • 配置声称使用HTTPS(https://localhost:9080
  • 服务实际运行HTTP(port 9080 (http))
  • 浏览器尝试建立TLS连接,发送TLS握手数据
  • HTTP服务器无法解析二进制TLS数据,报错!
核心经验:HTTPS不能用纯IP
  1. IP地址无法申请SSL证书
  2. 自签名证书浏览器不信任
解决方案:环境差异化配置
yaml 复制代码
backendURL:  
  sitURL: http://localhost:9080        # 本地开发,HTTP  
  uatURL: http://UAT后端IP:9080     # 内网IP,HTTP(内网不需要HTTPS)  
  prodURL: http://PROD后端IP:9080      # 内网IP,HTTP(由网关处理HTTPS)
架构说明

​SIT环境(本地开发)​​:

  • 浏览器 → HTTP → localhost:3000 (前端) → HTTP → localhost:9080 (后端)
  • 同域名(localhost),Cookie使用SameSite=Lax,无需HTTPS

​UAT/PROD环境(生产环境)​​:

经验总结

HTTPS使用原则:

  1. 本地开发:HTTP足够,简单快速
  2. 跨域场景:前端必须HTTPS + 域名
  3. 内网服务:可以用HTTP + IP,由网关处理SSL
  4. IP地址无法使用HTTPS,必须用域名

五、最终方案总结

配置文件(application.yml)
yaml 复制代码
spring:  
  profiles:  
    active: uat  

frontURL:  
  sitURL: http://localhost:3000/dashboard  
  uatURL: https://mvpfont-uat.example.com/dashboard  
  prodURL: https://mvpfont.example.com/dashboard  

backendURL:  
  sitURL: http://localhost:9080  
  uatURL: http://UAT后端IP:9080  
  prodURL: http://PROD后端IP:9080
环境对比表
配置项 SIT UAT PROD
前端协议 HTTP HTTPS HTTPS
前端地址 localhost:3000 mvpfont-uat.example.com mvpfont.example.com
后端协议 HTTP HTTP HTTP
后端地址 localhost:9080 UAT后端IP:9080 PROD后端IP:9080
Cookie Domain 不设置 .example.com .example.com
Cookie SameSite Lax None None
Cookie Secure
是否跨域

六、核心经验总结

  1. ​URL重定向​​:

    • 务必包含完整协议(http://https://
    • 添加防御性检查,自动补全协议
  2. ​多环境配置​​:

    • YAML中配置sit/uat/prod三套URL
    • 通过spring.profiles.active切换环境
    • Service层根据环境动态获取URL
  3. ​跨域Cookie​​:

    • Domain:从前端URL提取顶级域名(如.example.com
    • SameSite=None:允许跨站点传递(需要HTTPS)
    • Secure:必须HTTPS(SameSite=None的前提)
    • 清除旧Cookie:避免出现多个同名但Domain不同的Cookie
  4. ​HTTPS与IP​​:

    • localhost可用HTTP
    • 跨域必须HTTPS + 域名
    • IP地址不能用HTTPS
    • 内网服务可用HTTP,由网关处理SSL

七、总结

这次技术探索,我们从最初的困惑:

  • URL重定向去哪儿了?
  • 为什么Cookie前端读不到?
  • HTTPS连HTTP为什么报错?

到最终形成完整的解决方案:

  • 多环境配置 + 动态URL获取
  • 智能Domain提取 + 跨域Cookie
  • 环境差异化的协议选择

最大的收获:

  1. 重定向URL必须包含协议
  2. 跨域Cookie的Domain是关键
  3. IP地址不能用HTTPS
  4. 环境差异化配置很重要
  5. 清除旧Cookie避免冲突
相关推荐
踏浪无痕4 小时前
AI 时代架构师如何有效成长?
人工智能·后端·架构
程序员小假4 小时前
我们来说一下无锁队列 Disruptor 的原理
java·后端
武子康6 小时前
大数据-209 深度理解逻辑回归(Logistic Regression)与梯度下降优化算法
大数据·后端·机器学习
maozexijr6 小时前
Rabbit MQ中@Exchange(durable = “true“) 和 @Queue(durable = “true“) 有什么区别
开发语言·后端·ruby
源码获取_wx:Fegn08956 小时前
基于 vue智慧养老院系统
开发语言·前端·javascript·vue.js·spring boot·后端·课程设计
独断万古他化6 小时前
【Spring 核心: IoC&DI】从原理到注解使用、注入方式全攻略
java·后端·spring·java-ee
毕设源码_郑学姐6 小时前
计算机毕业设计springboot基于HTML5的酒店预订管理系统 基于Spring Boot框架的HTML5酒店预订管理平台设计与实现 HTML5与Spring Boot技术驱动的酒店预订管理系统开
spring boot·后端·课程设计
不吃香菜学java6 小时前
spring-依赖注入
java·spring boot·后端·spring·ssm
ja哇6 小时前
Spring AOP 详细讲解
java·后端·spring
南部余额6 小时前
Spring Boot 整合 MinIO:封装常用工具类简化文件上传、启动项目初始化桶
java·spring boot·后端·文件上传·工具类·minio·minioutils