顾乔芝士网

持续更新的前后端开发技术栈

问题复盘:飞书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被当作相对路径拼接到了当前请求路径上!

最初的配置文件

# 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. 完善配置文件
frontURL:  
  sitURL: http://localhost:3000/dashboard  
  uatURL: https://mvpfont-uat.example.com/dashboard  
  prodURL: https://mvpfont.example.com/dashboard
  1. 添加防御性代码
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读取不到:

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

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

跨域Cookie的三座大山

  1. SameSite属性
  2. 浏览器默认SameSite=Lax,跨站点请求被拒绝
  3. 控制台警告:Cookie "access_token" has been rejected because it is in a cross-site context
  4. Secure标志
  5. 要设置SameSite=None,必须同时设置Secure标志
  6. HTTP环境下SameSite=None会被浏览器忽略
  7. Domain属性
  8. 跨域场景下,Domain必须正确设置
  9. 后端域名UAT后端IP:9080与前端域名mvpfont-uat.example.com完全不同

解决方案:多环境配置 + 智能Domain提取

  1. 配置文件支持多环境
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. 智能提取顶级域名
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+#34;)) {  
            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
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后,启动服务访问时出现错误:

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. 自签名证书浏览器不信任

解决方案:环境差异化配置

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 → Nginx (mvpfont-uat.example.com) → HTTP → UAT后端IP:9080 (后端)
  • 前端使用HTTPS域名,后端使用HTTP内网IP
  • Cookie使用Domain=.example.com + SameSite=None + Secure

经验总结

HTTPS使用原则:

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

五、最终方案总结

配置文件(application.yml)

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重定向
  2. 务必包含完整协议(http://或 https://)
  3. 添加防御性检查,自动补全协议
  4. 多环境配置
  5. YAML中配置sit/uat/prod三套URL
  6. 通过spring.profiles.active切换环境
  7. Service层根据环境动态获取URL
  8. 跨域Cookie
  9. Domain:从前端URL提取顶级域名(如.example.com)
  10. SameSite=None:允许跨站点传递(需要HTTPS)
  11. Secure:必须HTTPS(SameSite=None的前提)
  12. 清除旧Cookie:避免出现多个同名但Domain不同的Cookie
  13. HTTPS与IP
  14. localhost可用HTTP
  15. 跨域必须HTTPS + 域名
  16. IP地址不能用HTTPS
  17. 内网服务可用HTTP,由网关处理SSL

七、总结

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

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

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

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

最大的收获:

  1. 重定向URL必须包含协议
  2. 跨域Cookie的Domain是关键
  3. IP地址不能用HTTPS
  4. 环境差异化配置很重要
  5. 清除旧Cookie避免冲突
控制面板
您好,欢迎到访网站!
  查看权限
网站分类
最新留言