解决CAS 单点登录、iframe在最新版谷哥(99)Edge(99)等浏览器Cookie丢失的过程实记(二)

一、实记前言

各位看官你们好,经过上一篇我的文章:解决CAS 单点登录中iframe在最新版谷哥(99)Edge(99)等浏览器中Cookie丢失的方案(一)通篇的介绍以及铺垫,我相信您已经大概的知道了我们的应用场景了。下面继续介绍发现问题的过程以及如何解决我们遇到的问题。 先上一张图,让大家了解一下各个系统的情况。

cas_http.png

二、开发以及内测环境问题赘述

(一)开发环境

  1. 集成cas单点登录 遇到casClient相关的jar包不加载
  2. jar包加载了但是casClient客户端拦截不到用户的请求
  3. 拦截到请求了,终于进入到登录环节了,但是输入对应的用户名以及密码以后后台报票根错误。
  4. 票根错误解决完毕了,应该能正常的与casServer交互,并继续往下走,然后casServer返回对应的跳转地址url,但是此时B系统前端js无法获取casServer返回的url地址。
  5. 不报票根错误了,但是登录成功以后到后应该跳转到我们系统的对应页面,但是浏览器直接显示了一段被它自己识别为普通字符串的html代码

(二)内测环境

  1. 用户登录成功以后,点击A系统页面对应的按钮,正常的话应该是在他们的当前的页面中弹出一个弹窗,这个弹窗通过iframe 展示我们的页面,但是实际的现象确是浏览器直接打开了一个新的窗口,来展示我们的页面,这样导致了A系统的一个自动处置流程被迫中断
  2. 点击A系统页面对应的按钮,在当前页面通过弹窗显示我们的页面以后,我们页面会调用后台接口,但是当时的现象是刨去没有以正确的方式打开我们的页面以外,页面调用我们后台接口时也报错。
  3. 当解决完了让我们的页面在iframe里可以正确显示以后,新的问题又出现了,本应该嵌套显示的是我们对应的业务的页面,但是此时是显示的登录页面。也就是,用户已经登陆了一次了,应该直接跳转到我们业务页面,但是不知什么原因又被casClient拦截了,直接认定当前用户没有登录,跳转到了登录页。
  4. 解决好了跳转登录页面以后以后本以为问题得到了解决,但是现实情况很打击人,本应该显示我们页面的地方死活不显示我们的页面,后来终于发现是浏览器的安全限制问题,如果用iframe 嵌套第三方页面,通过http请求方式访问第三方系统的时候,浏览器会禁止携带第三方的cookie去请求对应的第三方系统,也就是所谓的cookie丢失
  5. 为了解决iframe cookie丢失问题,我们采取了https请求方式。结果问题依旧没有得到解决。此时陷入无解的境地。

三、 开发以及内测环境遇到的问题解决方案(从此处开始上干货!!!!!!!)

因为起初iframe采用的是http协议请求我们的系统。也就是在决定采取https协议之前。 从此处开始着重挨个说明开发环境遇到的问题以及表现出的现象,以及对应的解决办法。还有内测环境遇到的问题以及表现出的还有对应的解决办法。 开发环境如下: A系统页面代码(此处只是为了演示,真正的页面比下面展示的要复杂,而且采用的是动态的赋值给iframe 的src属性)

<!DOCTYPE html>
<html lang="zh" xmlns:th="http://www.thymeleaf.org" >
<head>
</head>
<body class="" style="height: 700px;width: 1150px" >
   <div align="center" style="height: 600px;width: 1000px" >
      <iframe id="testPage" align="center" style="height: 500px;width: 850px"
         src="http://192.168.0.166:18080/accident/transmission/handle?devId=11111&bayId=22222&stId=33333&from=robot"   about="about:blank"   >
      </iframe>
   </div>
   <th:block th:include="include :: footer" />
   <script type="text/javascript">
   </script>
</body>
</html>
复制代码

nginx配置如下:


events {
    worker_connections  1024;
}

http {
    include       mime.types;
    default_type  application/octet-stream;
    sendfile        on;
    keepalive_timeout  65;
    underscores_in_headers on;
    server {
        listen 18080;
        server_name cyber.com;
          location / {
                root E:/WORKSPACE-DY/CyberMonitor-ui/dist;
		try_files $uri $uri/ /index.html;
		index  index.html index.htm;
          }
        
          location /prod-api/ {
          proxy_set_header NGINX-REQEST-URL $host:$server_port/prod-api;
	     	proxy_set_header Host $host:$server_port;
	     	add_header Access-Control-Allow-Origin * always;
	        add_header Access-Control-Allow-Credentials true;
		proxy_pass http://192.168.0.166:21080/;
        }
        
    }
}
复制代码

Vue前端打包配置如下:

ENV = 'production'
VUE_APP_BASE_API = '/prod-api/'
VUE_APP_BASE_LOGIN_API = '/prod-api/'
复制代码

(一)、开发环境遇到问题(用的是谷哥99版本浏览器,谷哥79版本浏览器,Edge99版本浏览器)

  • 1:集成cas单点登录 遇到casClient相关的jar包不加载

问题原因:SpringBoot启动类未添加对应的casClient相关jar包的扫描路径配置: 如下代码配置则导致容器扫描不到casClient的jar包

@SpringBootApplication(
        exclude = { DataSourceAutoConfiguration.class })
public class CyberApplication {
    public static void main(String[] args) {
        // System.setProperty("spring.devtools.restart.enabled", "false");
        SpringApplication.run(CyberApplication.class, args);
    }
}
复制代码

对应的应该修改为下面的配置,com 是casClient jar包最外层的包名

@SpringBootApplication(
        exclude = { DataSourceAutoConfiguration.class },
        scanBasePackages = {"com"})
public class CyberApplication {
    public static void main(String[] args) {
        // System.setProperty("spring.devtools.restart.enabled", "false");
        SpringApplication.run(CyberApplication.class, args);
    }
}
//因为我们引入的cas是p平台团队修改过的,所以cas客户端的源码跟网络上开源的有稍微的区别
复制代码
  • 2. cas客户端的jar包加载了但是casClient客户端拦截不到用户的请求

该问题的原因如下,是因为我们采用的开源框架本身是为了前后端分离的方式量身定制的,有一定的局限性。但是p平台团队之前研发的单点登录系统虽然有微服务版本的,也支持SpringBoot,但是因为我们的开源框架请求头没有包含 X-Requested-With 这个请求头,但是casClient过滤器需要这个请求头进行逻辑判断,所以导致casClient过滤器无法拦截我们的请求。casClient部分源码如下:因为他是首先通过获取请求头中的X-Requested-With这个变量,然后继续后面的过滤操作。

public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
    HttpServletRequest request = (HttpServletRequest)servletRequest;
    HttpServletResponse response = (HttpServletResponse)servletResponse;
    String requestX = request.getHeader("X-Requested-With");
    if (!"XMLHttpRequest".equalsIgnoreCase(requestX)) {
        String queryStr = request.getQueryString();
        String url = request.getRequestURL().toString();
        if (url.contains("?")) {
            url = url.substring(0, url.indexOf("?"));
        }
        int tmep = url.indexOf(";jsessionid=");
        if (tmep > 0) {
            url = url.substring(0, tmep);
            if (!Util.isEmpty(queryStr)) {
                response.sendRedirect(url + "?" + queryStr);
            } else {
                response.sendRedirect(url);
            }
            return;
        }
        if (Util.isExcludePage(url)) {
            filterChain.doFilter(servletRequest, servletResponse);
            return;
        }
        HttpSession session = request.getSession(false);
        if (session != null && session.getAttribute("ssoWhiteURL") != null) {
            List<String> list = (List)session.getAttribute("ssoWhiteURL");
            if (list.contains(request.getRequestURI())) {
                filterChain.doFilter(servletRequest, servletResponse);
                return;
            }
        }
        if (!Util.isEmpty(queryStr)) {
            int index = queryStr.lastIndexOf("WT=");
            if (index >= 0) {
                session = request.getSession();
                if (session.getAttribute("ssoWhiteURL") == null) {
                    List<String> whiteURL = new ArrayList();
                    session.setAttribute("ssoWhiteURL", whiteURL);
                }
                ((List)session.getAttribute("ssoWhiteURL")).add(request.getRequestURI());
                if (index == 0) {
                    response.sendRedirect(url);
                } else {
                    response.sendRedirect(url + "?" + queryStr.substring(0, index - 1));
                }
                return;
            }
        }
        super.setCasServerLoginUrl(Util.findMatchingCasServerUrlPrefix(request) + "login");
        super.doFilter(servletRequest, servletResponse, filterChain);
    } else {
        super.setCasServerLoginUrl(Util.findMatchingCasServerUrlPrefix(request) + "login");
        super.doFilter(servletRequest, servletResponse, filterChain);
    }
}
复制代码

解决办法如下:就是前端添加http请求拦截器,对请求统一添加一个X-Requested-With请求头。 代码如下:

axios.defaults.headers['X-Requested-With'] = 'XMLHttpRequest';
复制代码
  • 3. 拦截到请求了,终于进入到登录环节了,但是输入对应的用户名以及密码以后后台报票根错误。

出现这个问题的原因是我们application.yml 配置文件里面,路径配置错误,导致casServer 对比票据时失败,例如: 我们后台接口是21080,但是通过nginx对外发布的接口是18080 所以SSO_serverName应该配置成 SSO_serverName: https://192.168.0.166:18080/prod-api 起初因为配置成了 SSO_serverName: https://192.168.0.166:21080/prod-api 导致了报错。 如图:

image.png

报错原因是因为cas的工作机制是根据请求url 动态的截取并最后拼接成一个service路径,因为浏览器请求nginx,由nginx转发到后台,由后台的casClient拦截进行处理,nginx的端口是18080,但是因为给casClient 配置的端口信息是 21080 ,所以导致最后的两个作对比用的service 路径端口号不同,导致票根验证失败。

项目后台配置
server:
  port: 21080
  shutdown: graceful 
  servlet:
    context-path: /
  tomcat:
    uri-encoding: UTF-8
    max-threads: 800
    min-spare-threads: 30
复制代码
cas配置
cas:
  #是否开启单点登录功能, true启用单点,false关闭单点登录
  CAS_ENABLE: true
  # SSO_casServerUrlPrefix 单点登录服务器地址,当有内外网时,内网地址与外网地址用一个空格分开(如示意)
  casServerUrlPrefix: https://192.168.0.166:49174/cas/
  # SSO_casServerUrlPrefix_validate 单点登录服务器地址,当同时使用内外网发布时(非内外网同时发布时,就可以注释该项配置),
  #该配置为SSO_casServerUrlPrefix中的内网部分
  SSO_casServerUrlPrefix_validate: http://192.168.0.166:8280/cas/
  # 当前应用的服务名或域名(前端访问根路径),配置对外的发布地址,当有内外网时,内网地址与外网地址用一个空格分开(如示意)
  SSO_serverName: https://192.168.0.166:18080/prod-api
  # 单点白名单配置,配置哪些类型的资源不受单点过滤器拦截,通常情况下业务系统的静态资源不做拦截
  EXCLUDE_RESOURCE_TYPE: ico,js,css,png,gif,jpg,ttf,woff,woff2,text/html
  # 单点白名单配置,配置哪些URL不受单点过滤器拦截。
  #如果某一个url太长,或者配置额url个数太多,那么查看就不太方便,但是又不能直接的换行,否则读取属性的值的时候其换行部分就被忽略了.其实我们可以通过增加一个\符号来达到换行的效果。
  #url-pattern配置规范:*(匹配0或者任意数量的字符),**(匹配0或者更多的目录)
  # 例子:EXCLUDEPAGES=/sguap-client/iscintegratetest/mxwebtest/jsp/mxFrameWorkTestPage.jsp,\
  #                 /sguap-client/iscintegrate/mxweb/jsp/mxFrameWorkTestPage.jsp
  # SSO_EXCLUDEPAGES=/osp/Security/**
  SSO_EXCLUDEPAGES: /system/**

  #单点黑名单配置,配置哪些URL必须进行拦截。
  #黑名单优先级高于白名单。拦截器优先从黑名单中检查,如果存在就进行拦截。
  #如果某一个url太长,或者配置额url个数太多,那么查看就不太方便,但是又不能直接的换行,否则读取属性的值的时候其换行部分就被忽略了.其实我们可以通过增加一个\符号来达到换行的效果
  #url-pattern配置规范:*(匹配0或者任意数量的字符),**(匹配0或者更多的目录)
  # 例子:INCLUDEPAGES=/sguap-client/iscintegratetest/mxwebtest/jsp/mxFrameWorkTestPage.jsp,\
  #                 /sguap-client/iscintegrate/mxweb/jsp/mxFrameWorkTestPage.jsp
  # SSO_INCLUDEPAGES=/osp/Security/user/index.jsp
  SSO_INCLUDEPAGES:
  # 当前应用单点登录注销后的跳转附加路径(全路径,一般是网站首页,暂时还没有考虑内网同时发布的情况)
  SSO_SUCCESS_URL:
  # 如果前端是直接通过ZUUL访问微服务(其他场景不需要,如前端直接访问微服务,或者通过NGINX代理访问),则该参数配置为微服务的spring.application.name;
  # ZUUL_SERVICE_NAME=graph-server
  #系统敏感字过滤正则表达式
  #注释掉cas.SENSITIVE_WORD_REG则不做任何过滤
  SENSITIVE_WORD_REG: .*('|(<[\w]+)|([\w]+>)|(<[\w]+>)|(</)|(/>)|(--)|(/[*])|([*]/)|((\+|\b)(select|update|and|or|delete|union|insert|trancate|drop|execute)(\+|\b))).*
  #URL关键字过滤,注释掉则不做过滤
  URL_WORD_REG: ^.*([~`!$^*()+|'<>]|(--)|(%20or%20)|(%20and%20)+).*$
  #敏感字符检测过滤器,默认不开启,true开启
  PARAMETERFILTER_ENABLE: false
  #越权检查过滤器,默认不开启,true开启
  SECURITYFILTER_ENABLE: false
  #HOST地址配置
  HOST: 127.0.0.1:9000,192.168.207.226:9000
复制代码
  • 4. 票根错误解决完毕了,应该能正常的与casServer交互,并继续往下走,然后casServer返回对应的跳转地址url casredirect 这个变量,,但是此时B系统前端js无法获取casredirect的值。如果前端能获取到值,则按照以下方式进行页面的重定向 top.location.href = res.headers['casredirecturl'];(。。。。后来发现。此处不能加top,因为当时没有注意,直接导致了在内测环境中,iframe加载它嵌套的页面时,直接打开了一个浏览器窗口,并且浏览器地址栏的地址发生了改变,而不是在当前页面的iframe中显示我们的页面,这是自己埋下的一个坑啊。。。。。。)

前端代码如下:

// 响应拦截器
service.interceptors.response.use(res => {
    // 未设置状态码则默认成功状态
    const code = res.data.code || 200;
    // 获取错误信息
    const msg = errorCode[code] || res.data.msg || errorCode['default']
    if (code === 401) {
      location.href = '/404';
    } else if (code === 500) {
      Message({
        message: msg,
        type: 'error'
      })
      return Promise.reject(new Error(msg))
    } else if (code !== 200) {
      Notification.error({
        title: msg
      })
      return Promise.reject('error')
    } else {
     if (res.headers['casredirect'] && res.headers['casredirect'] === 'true') {
        top.location.href = res.headers['casredirecturl'];
        console.log("casredirecturl:" + res.headers['casredirecturl']);
        return;
      }
      return res.data
    }
  }
复制代码
  • 5. 不报票根错误了,但是登录成功以后到后应该跳转到我们系统的对应页面,但是浏览器直接显示了一段被它自己识别为普通字符串的html代码

出现此问题的原因,起初通过抓包工具Fiddle 进行分析,登陆成功以后,A系统的页面可以正常显示,但是我们B系统的页面就无法显示,通过分析数据包,发现我们的后台返回的响应头里面有Security 节点,立马怀疑是框架SpringSecurity的问题。后来进行修改,验证结果依旧没能解决问题。然后继续查找问题的原因,最后发现是因为单单服务返回的响应Response 应该添加一个响应头,告知浏览器返回数据的contentType, 源码如下:代码的最后一行告诉浏览器,返回的是html。

修改前的代码:
@RestController
@RequestMapping({"/casController"})
public class CasController {
    public CasController() {
    }

    @RequestMapping({"/transfer"})
    public void test(HttpServletRequest request, HttpServletResponse response) throws IOException {
        PrintWriter writer = response.getWriter();
        StringBuffer buffer = new StringBuffer();
        if (request.getParameter("transferRefer") == null) {
            System.out.println("transferRefer is null!");
            writer.println("transferRefer is null!");
        } else {
            String transferRefer = request.getParameter("transferRefer").toString();
            transferRefer = transferRefer.replaceAll("SQB", "&");
            buffer.append("<html><head><script>location.href ="").append(transferRefer).append(""</script></head></html>");
            System.out.println("transferRefer is: " + buffer.toString());
            writer.println(buffer.toString());
           
        }

    }
}




修改后的代码:
@RestController
@RequestMapping({"/casController"})
public class CasController {
    public CasController() {
    }

    @RequestMapping({"/transfer"})
    public void test(HttpServletRequest request, HttpServletResponse response) throws IOException {
        PrintWriter writer = response.getWriter();
        StringBuffer buffer = new StringBuffer();
        if (request.getParameter("transferRefer") == null) {
            System.out.println("transferRefer is null!");
            writer.println("transferRefer is null!");
        } else {
            String transferRefer = request.getParameter("transferRefer").toString();
            transferRefer = transferRefer.replaceAll("SQB", "&");
            buffer.append("<html><head><script>location.href ="").append(transferRefer).append(""</script></head></html>");
            System.out.println("transferRefer is: " + buffer.toString());
            writer.println(buffer.toString());
            response.setHeader("Content-Type", "text/html;charset=utf-8");
        }

    }
}
复制代码

(二)、内测环境遇到的问题(用的是谷哥79版本,谷哥99版本,还有Edge95版本,Edge96版本,Edge99版本)

  • 1. 用户登录成功以后,点击A系统页面对应的按钮,正常的话应该是在他们的当前的页面中弹出一个弹窗,这个弹窗通过iframe 展示我们的页面,但是实际的现象确是浏览器直接打开了一个新的窗口,并且浏览器地址发生了改变,这个使用户无法继续流程的处置,导致后续业务无法继续操作,这样导致了A系统的一个自动处置流程被迫中断。

出现此问题的原因在开发环境问题描述中的第四个已经进行了说明。解决办法就是把top去掉就可以。

  • 2. 点击A系统页面对应的按钮,在当前页面通过弹窗显示我们的页面以后,我们页面会调用后台接口,但是当时的现象是刨去没有以正确的方式打开我们的页面以外,页面调用我们后台接口时也报错。

出现此问题的原因就是跨域问题。浏览器直接拒绝执行请求因为在110机器上,打开页面,然后页面访问120机器,ip和端口都不同,导致浏览器拒绝执行请求。

  • 3. 当解决完了让我们的页面在iframe里可以正确显示以后,新的问题又出现了,本应该嵌套显示的是我们对应的业务的页面,但是此时是显示的登录页面。也就是,用户已经登陆了一次了,应该直接跳转到我们业务页面,但是不知什么原因又被casClient拦截了,直接认定当前用户没有登录,跳转到了登录页,而且会出现循环跳转登录页,最后浏览器报错,重定向次数过多。

大概很多人都遇到过这个题。网上也有很多解决办法。但是在我们这个应用场景下,试了很多方案都没有解决我们这个问题。然后就想出了一个解决办法,通过Fiddle抓包工具分析数据包,然后一点一点剖析问题到底是出在什么地方。皇天不负有心人,最后被我们发现:通过iframe加载第三方的页面的时候,谷哥浏览器的安全限制策略导致我们 在浏览器正式访问第三方系统的时候本应该携带上已经生成的会话状态标识:cookie,但是浏览器确实禁止携带cookie。然而因为用户已经成功登陆过一次了,只不过是通过p系统登录的。这样导致的一个奇怪现象是,因为用户已经登录过了,当去的访问我们系统的时候,虽然携带了cas第一次登录认证通过后签发的票据(导致我们B系统casClient 认为这个用户已经登陆过了,直接放行,但是因为没有携带我们系统生成的cookie,所以我们后台容器直接认为该用户没有登录,此处需要详细了解http协议以及浏览器如何与容器建立连接的详细过程),但是因为缺少浏览器与我们系统建立会话的状态标识:cookie,导致最后访问失败。该问题出现的原因就在于浏览器认为当前的访问不安全,所以解决此问题的办法就是更换https协议。 如果要修改成https协议的话我们需要nginx支持https,另外安装openssl,需要重新编译, 另外证书是 tls3版本。(因为这个tls版本问题,导致了我们进入了无解的境地。。。。。。) 直接上代码:

http协议的nginx配置

user root;
worker_processes  1;
events {
    worker_connections  1024;
}
http {
    include       mime.types;
    default_type  application/octet-stream;
    sendfile        on;
    keepalive_timeout  65;

    server {
        listen      18080;
        server_name  localhost;
	location / {
            root  /home/d5000/shandongjx/cyber/cyber_ui/dist;
            add_header X-Requested-With XMLHttpRequest;
	    try_files $uri $uri/ /index.html;
            index  index.html index.htm;
        }
        location /prod-api/ {
             set $URL  $scheme://$http_host$request_uri/prod-api/;
             proxy_set_header NGINX-REQEST-URL $host:$server_port/prod-api;
             error_page   500 502 503 504  /50x.html;
        }
        location = /50x.html {
            root   html;
        }
    }
}

https协议的配置,如果不了解如何配置nginx如何才能支持https协议,请参如下文章:
Nginx 配置 SSL 支持 HTTPS(自签证书)
<https://blog.csdn.net/cxy35/article/details/106277053>
安装openssl <https://www.cnblogs.com/chendongbky/p/12887747.html>

worker_processes  1;

events {
    worker_connections  1024;
}

http {
    include       mime.types;
    default_type  application/octet-stream;
    sendfile        on;
    keepalive_timeout  65;
    underscores_in_headers on;
    
  server {
        listen 443 default ssl;#默认443端口
        ssl on;
        server_name 192.168.0.166:443;
      
        ssl_certificate      D:/develop/NGINX/nginx-1.16.1/conf/ssl/cas.crt;#ssl证书 
        ssl_certificate_key  D:/develop/NGINX/nginx-1.16.1/conf/ssl/cas.key;#ssl证书key
    
        ssl_session_cache    shared:SSL:50m;
        ssl_session_timeout  50m;
    
        ssl_ciphers  HIGH:!aNULL:!MD5;
        ssl_prefer_server_ciphers  on;
        
        location / {
            root E:/WORKSPACE-DY/CyberMonitor-ui/dist;
		try_files $uri $uri/ /index.html;
		index  index.html index.htm;
        }
        
        location /prod-api/ {
          proxy_set_header NGINX-REQEST-URL $host:$server_port/prod-api;
	     	proxy_set_header Host $host:$server_port;
	     	proxy_set_header Scheme https;
	     	add_header Access-Control-Allow-Origin * always;
	        add_header Access-Control-Allow-Credentials true;
        
		proxy_pass http://192.168.0.166:21080/;
        }
      
    
    }
  
    
}
复制代码

生成了ssl证书,配置好证书路径已经key的路径,重启nginx。本以为问题得到解决了。我们满怀期待,再次进行测试。请继续往下看 4.

  • 4. 采用了https协议,解决了跳转登录页面问题,本以为此时已经能拿到我们系统与浏览器客户端的会话标识cookie,整个功能就能正常了,但是现实情况很打脸。请看5.

  • 5. 为了解决iframe cookie丢失问题,我们采取了https请求方式。结果问题依旧没有得到解决。此时陷入无解的境地。

谷哥99版本浏览器报错提示: image.png

Edge99版本浏览器报错提示 image.png

这个到底是什问题导致的呢。会不会是nginx配置不正确。但是奇怪的是我们在开发环境测试是ok的,但是到了内侧环境就报错。 接下来请看我的 解决单点登录、iframe在最新版谷哥(99)Edge(99)等浏览器Cookie丢失的过程实记(三)

猜你喜欢

转载自juejin.im/post/7083304472453054495
今日推荐