记vue nginx springcloud oauth2.0三方系统登录中问题处理过程

        今天是个好日子,杭州雨过天晴,而且,折腾我近半个月的问题迎来解决胜利的曙光。

        之前的一段时间主要花在前端vue方面了。在面对vue的痛苦、焦躁中,一次次想要放弃,又一次次捡起来,硬着头皮去查找方案解决问题,这种滋味真的酸爽难以忘却。经过一段时间的吭哧吭哧,好在终于搞定了首页、登录、注册页的样式和更改后的功能。期间处理了element-ui 改为普通样式布局,开发自定义的样式、路由不受控制自动跳转刷新网页、自定义js事件不触发、button按钮二次点击才生效 等等一系列的问题,此时心中还是有一丝欣慰的。于是在国庆节前一天的晚上,我决定把登录页最后的功能oauth2.0三方系统登录给搞定,因为后端代码半年前就已经搞定,无奈前端是自己的弱项,一直没有开工。所以前端的请求代码写一写,应该很快。谁知一折腾就是近半个月了。。。

        在这期间,伴随着焦躁、希望、失落、淡定的复杂情绪,本着死磕到底的心志,把网络搜索都要查个稀烂,在各种方案中反复尝试,终于一点一点的发现问题,寻找灵感,构思方案,搞定了vue springcloud nginx 条件下的 oauth2.0 三方系统账号登录问题。下面的文章中,我将分享出期间经历的各种问题,以及处理方式和思路分析,避免以后走弯路。如果有不对或者不合理的地方,或者您有更好的方式,热烈欢迎批评指正,留言交流。解决问题就是这样,你永远不知道在问题的解决过程中又会触发多少新的问题。^_^

项目概况简述:

        项目是前后端分离的,前端采用的vue,发送请求到后端nginx服务器,然后经过配置好的nginx反向代理,请求到服务器上的静态资源或者部署的springcloud 网关zuul,然后经过zuul转发分发到具体的服务模块。大致的流程如下:

记vue nginx springcloud oauth2.0三方系统登录中问题处理过程_第1张图片

三方登录有多个系统,比如QQ,百度,微博等等,代码逻辑都是一样的,我就以QQ为例。【文中的截图可能部分是百度的例子,不过没影响,都能说明问题。】

具体oauth2.0、跨域、三方系统应用的创建等基础知识,网上搜索一大堆,傻瓜式的教程简单易学,我就在此不再赘述。

vue的 oauth请求代码如下:就是一个非常普通的axios请求,

nginx的配置代码如下:可以看到,是有跨域配置的。

记vue nginx springcloud oauth2.0三方系统登录中问题处理过程_第2张图片

后端有两个接口,一个是QQ登录接口qqLogin,一个是回调callback接口,目前的逻辑是vue请求登录接口qqLogin,在这个接口中系统结合配置好的QQ给的id和密钥,生成qq的确认授权访问url,通过重定向返回浏览器,浏览器跳转到授权页,用户授权后,再次重定向访问系统的callback回调接口。回调接口中,我处理数据,生成用户的本系统token,通过json返回前端。我画了一张图就是大致的流程:

记vue nginx springcloud oauth2.0三方系统登录中问题处理过程_第3张图片

后端代码:

记vue nginx springcloud oauth2.0三方系统登录中问题处理过程_第4张图片

终于可以尝试了,用axios成功发送了请求,结果意料之中,出现了跨域问题。之所以说是意料之中,结合上面画的流程不难看出,先是访问本系统qqLogin,重定向却到了QQ的网站grep.qq.com。看问题,是出现在第四步,访问qq授权页时出现的跨域。

redirected from 'https://www.yuanhuiying.com/api/dxxdswAccount/oauth2/uncheck/qq/qqLogin') from origin 'https://www.yuanhuiying.com' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.

一开始这一块知识我也不是很清楚,所以就有点没头苍蝇乱撞的感觉。既然是跨域,没有Access-Control-Allow-Origin这个的header配置?那就配上呗。参考文章https://www.jiansu.com/p/98d4bc7565b2 中也提到了,的确是要进行这个header的配置,

【ps:关于跨域具体知识也可以参考这篇文章,写的很清晰】

于是在系统的qqLogin 接口中 添加如下代码,进行header的配置:

response.setHeader("Access-Control-Allow-Origin", "*");
response.setHeader("Access-Control-Allow-Credentials", "true");
response.setContentType("text/html; charset=utf-8");

然而并没有任何效果,报错依旧,这时候在想,是不是和springcloud的网关zuul有关系?难道要进行全局设置?百度下,还真有相关文章:根据:微服务架构问题之跨域(基于springcloud的两种CORS解决方式:网关ZUUL、服务提供方本身)_coolcoffee168的博客-CSDN博客 这篇文章的描述,建议 如果项目用到网关的话,还是建议在网关中统一进行跨域处理,这样处理可以使服务提供方只专注业务处理就可以了,不用每个微服务都要自己进行跨域处理。不要网关、服务都同时进行跨域处理,否则有问题。

那就试试看,撤销刚才的改动恢复原样,然后进行zuul的网关改造。文中两种方式,一种添加全局网关过滤器,另一种注入配置bean。先尝试方式一,添加全局zuul的cors自定义过滤器,进行处理。详细的处理器代码如下:

package com.xxxxxxx.postFilter;

import javax.servlet.http.HttpServletResponse;

import org.springframework.stereotype.Component;

import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;

import lombok.extern.slf4j.Slf4j;

/**
 * @author	    :
 * @createTime  :2020-09-30 1:48:02 PM
 * @description : 跨域处理全局过滤器
 * @param		:
 */
@Slf4j
@Component
public class CorsResponseFilter extends ZuulFilter {
	
	/**
	 * 过滤器类型:
	 * pre: 在请求被路由之前调用
	 * route: 在路由请求时被调用
	 * post: 表示在route和error过滤器之后被调用
	 * error: 处理请求发生错误是被调用
	 */
	@Override
	public String filterType() {
		return "post";
	}
	
	/**
	 * 过滤器执行顺序,数值越小优先级越高,不同类型的过滤器,执行顺序的值可以相同
	 */
	@Override
	public int filterOrder() {
		return 1000;
	}
 
	/**
	 *返回布尔值来判断该过滤器是否要执行。可以通过此方法来执行过滤器的有效范围
	 */
	@Override
	public boolean shouldFilter() {
		return true;
	}
 
	/**
	 * 具体逻辑
	 */
	@Override
	public Object run() {
		log.info("======CorsResponseFilter: run()方法在执行");
		RequestContext ctx = RequestContext.getCurrentContext();
		HttpServletResponse response = ctx.getResponse();
		// 设置哪个源可以访问我
		response.setHeader("Access-Control-Allow-Origin", "*");
		// 允许哪个方法(也就是哪种类型的请求)访问我
		response.setHeader("Access-Control-Allow-Methods", "PUT,GET,POST,OPTIONS");
		// 允许携带哪个头访问我
		response.setHeader("Access-Control-Allow-Headers", "X-Requested-With, Content-Type, X-File-Name,token");
		//文本大小,可以不设置(下载文件的时候注意下,发现没加的话,文件大小变了)
		if (ctx.getOriginContentLength() != null && ctx.getOriginContentLength() > 0) {
			response.setHeader("Content-Length", ctx.getOriginContentLength().toString());
		}
		//允许携带cookie
		response.setHeader("Access-Control-Allow-Credentials", "true");
		ctx.setResponse(response);
		return null;
	}
 
}

再试,发现不行,报错依旧,现在回想起来,当时就有点没过脑子,方式一和方式二其实原理一样,都是添加配置,无非方式不同而已,可我当时一看方式一不行,就去用方式二,想在看看,就是不动脑子,想抓救命稻草一点希望似的做无用功。

那就暂时删除刚刚添加的cros过滤器,新建CorsFilter配置类,并实现WebMvcConfigurer接口,如下:此时完全是按照上面参考文章的第二种方法,

​
package com.xxxxx.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

/**
 * @author 
 * @createTime 2020年9月30日下午3:09:09
 * @description
 */

@Configuration
public class CorsConfig implements WebMvcConfigurer {

	public CorsFilter corsFilter() {
		UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
		CorsConfiguration config = new CorsConfiguration();
		config.setAllowCredentials(true);
		config.addAllowedOrigin("*");
		config.addAllowedHeader("*");
		config.addAllowedMethod("OPTIONS");
		config.addAllowedMethod("HEAD");
		config.addAllowedMethod("GET");
		config.addAllowedMethod("PUT");
		config.addAllowedMethod("POST");
		config.addAllowedMethod("DELETE");
		config.addAllowedMethod("PATCH");
		source.registerCorsConfiguration("/**", config);

		return new CorsFilter(source);
	}

	@Override
	public void addCorsMappings(CorsRegistry registry) {
		registry.addMapping("/**")
				// 是否发送Cookie
				.allowCredentials(true)
				// 放行哪些原始域
				.allowedOrigins("*").allowedMethods(new String[] { "GET", "POST", "PUT", "DELETE" }).allowedHeaders("*")
				.exposedHeaders("*");
	}

}

​

启动,发现竟然报错,.IllegalArgumentException: '*' is not a valid exposed header value 

尝试       删除上面代码倒数第三行的  .exposedHeaders("*");再次启动,正常启动。

继续登录访问尝试:还是不行,老样子。这时才反应过来,其实上面的尝试是在做重复性的操作,无论配置bean也好,使用过滤器也好,原理都是一样的。有点病急乱投医,不知所措的味道了。

接下来,十月一日,八天长假第一天,我却无心出去走走,一大早带着电脑跑公司去了,继续研究这个问题。在网上不断搜索查询。发现文章 比如 此文章正在二次审核中,即将跳转到首页 - 程序员大本营 ,和我的情况基本一致,但是没有处理意见。。。文章作者在csdn论坛 https://bbs.csdn.net/topics/395570343  也有问,但貌似没合适答案,我就先留个言,看样子这老哥基本不会再出现csdn了。正如此文中说的现象,我也发现了 用a标签 的href 直接请求qqLogin接口QQ帐号安全登录 ,或者在浏览器地址栏输入访问,一切跳转正常没问题,最后返回的json的token数据成功显示在网页上。基于这点,我当时就认为后端应该没有问题,应该是前端发送请求的问题,否则怎么用浏览器地址栏访问,window,location,href都可行,用asiox不可行呢,而且前端我是外行,很有可能是前端出问题,哪里没配置好。现在回头看,这里应该是我被现象误导了,后面下文会提及。

此时,可以说我基本认定是前端问题,那就往这方面搜。就查vue 跨域。网上查vue axios 跨域,很多文章都说在配置代理服务器,可是项目中已经有了,就是本地代理到目标服务器。这个配置实际上处理的是本地开发localhost:8080 连接 目标服务器的跨域问题,和我的跨域问题不是同一个方向。就这样忙活大半天,各种搜尝试vue的配置,没有进展。vue.config.js 中代理代码如下贴一下:

module.exports = {
  publicPath: "/",
  productionSourceMap: false,
  devServer: {
    proxy: {
      "/api": {
        target: "https://www.yuanhuiying.com", //服务器地址
        secure: false,
        changeOrigin: true,
        pathRewrite: {
          "^/api": "xxxxxxx",
        },
      },
    },
    https: true,
  },

20201003,假期第三天,不行,还继续搞,不然心里放不下。在vue那边没搞出名堂,我就开始想仔细看看请求的具体信息了。

今天又再次检查了请求的接口,响应结果如下:一共两个请求:

第一个就是请求系统的相关三方登录接口:

记vue nginx springcloud oauth2.0三方系统登录中问题处理过程_第5张图片

第二个,就是重定向的地址,

记vue nginx springcloud oauth2.0三方系统登录中问题处理过程_第6张图片

重定向的地址请求直接飘红,没有任何响应:console直接报跨域的错,这些就是问题现象。

记vue nginx springcloud oauth2.0三方系统登录中问题处理过程_第7张图片

在我尝试把网关中的cors过滤器,无论是否删掉,发现访问登录接口qqLogin,返回的响应头照样都有跨域相关的设置,如下图:

这时候才想起,请求是先到nginx的, 通过nginx的转发才到zuul网关的,这不就是nginx的那个跨域设置嘛【很久以前的设置,自己都不记得了。。。】:

记vue nginx springcloud oauth2.0三方系统登录中问题处理过程_第8张图片

当我把nginx中的这两行add_header的配置删除掉,再次从网站访问登录接口【第一个请求】,响应头如下

记vue nginx springcloud oauth2.0三方系统登录中问题处理过程_第9张图片

没有出现Access-Control-Allow-Origin 和 Access-Control-Allow-Credentials,也就意味着无论zuul网关还是微服务模块,无论是否设置了跨域,浏览器中是否有响应跨域的设置,只和是否在nginx中进行跨域设置有关。如果nginx没有这些跨域设置,在本地启动访问接口,会报错如下:

Access to XMLHttpRequest at 'https://www.yuanhuiying.com/api/dxxdswAccount/oauth2/uncheck/qq/qqLogin' from origin 'https://localhost:8080' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.

好吧,现在看来是不是问题 出在nginx的设置而不是vue?于是接下来的突破重点又从vue转移到nginx的身上来了。网关大多文章无非都是在nginx中添加类似如下的配置

add_header 'Access-Control-Allow-Credentials' 'true';
add_header 'Access-Control-Allow-Origin' '*';
add_header 'Access-Control-Allow-Methods' 'PUT,GET,POST,OPTIONS';
add_header 'Access-Control-Allow-Headers' 'X-Requested-With, Content-Type, X-File-Name,token';

或者添加上对预检请求的处理,这些配置很容易搜到,我进行各种尝试添加配置后都没有效果。此时感觉自己一直在乱试,一定得先确定处理方向。

既然登录接口用a标签,还有在浏览器地址栏都能正常工作,问题就出现在改用vue axios发送get请求的方式之后,那么问题一定还是在vue上。难道转半天又回到原点了?那就对比下两种情况下的请求、响应数据对比。同时比对了例如腾讯网站的人家的请求和响应数据,发现正常的响应的 content-type 是 text/html, 而请求系统qqLogin接口返回的content-type 是 application/json .这里有差异,是不是这的原因导致的呢?

把qqLogin等第三方平台接口的类上的@RestController改为@Controller,同时在相关callback的回调接口上添加@Responsebody;@RequestMapping加上produces

@RequestMapping(value = "/baidu/baiduLogin", method = RequestMethod.GET , produces="text/html;charset=UTF-8")

尝试发现返回体还是Content-Type: application/json;charset=utf-8,貌似没用,

再检查是不是有全局配置给改变了?检查发现网关zuul的过滤器中有以前的代码:

        HttpServletRequest request = ctx.getRequest();
        HttpServletResponse response = ctx.getResponse();
        response.setCharacterEncoding("UTF-8");  
        response.setContentType("application/json; charset=utf-8");

把这些过滤器中的      response.setContentType("application/json; charset=utf-8"); 注释掉,再试:倒好,直接没有content-type 显示了。。。更改没用,看来走不通,恢复代码原样,还得另寻出路。

这时发现:在请求头的参数里面,有这么一个地方:

Sec-Fetch-Dest: empty

Sec-Fetch-Mode: cors

而正常的的都是:

Sec-Fetch-Dest: document 

Sec-Fetch-Mode: navigate

好吧,问题是不是又在这里的导致的呢?于是突破方向又变为vue,看看请求发送的设置能否进行这些设置,折腾到最后都能没能成功。

        也许你已经注意到了,一路走来,问题始终无法定位到具体的方向,一会vue,一会后端,一会nginx,导致晕头转向的乱查乱试,其实我也注意到这个问题了,但是没办法定位我也很无奈,现在看来,根本原因就是对http的相关知识、前端相关设置、跨域等的原理 不熟悉不了解,心里没有一个整体的轮廓。 当然,这么一趟下来,我也了解的差不多了,就是时间代价有点大。当然,这是后话了。

        此时,已经接近一周了,天天心里像猫挠似的。于是开始反思是不是设计的有问题,导致这种方式一开始就无法实现。在继续查找相关跨域的资料渐渐意识到,可能还真的是这样,但为什么人家互联网网站都可以实现呢?我也没找到具体的解释。在搜索的过程中,发现文章:javascript - Facebook xhr登录:跨域请求被阻止 【javascript - Facebook xhr登录:跨域请求被阻止 - IT工具网】 ,文中这样说:

最佳答案

  为什么XHR被阻止在浏览器可以访问的相同URL上?
  因为这是一个跨域请求,因此,远程方必须首先允许该请求,这就是所谓的CORS。
  Facebook不允许通过来自不同域的脚本加载其登录对话框-显而易见的原因是,用户需要能够通过浏览器地址栏确认将登录凭据    发送到的站点,以免遭受网络钓鱼。
  您不能通过XHR / AJAX在后台加载FB登录对话框。您需要在顶部窗口实例中调用/重定向到它。

看样子好像真的是实现不了,需要重新设计。我的问题虽然也是跨域问题,但是我在服务器上再怎么设置跨域,也只是针对别人访问我的服务器内容才有效的跨域设置,比如他用两个域名最终都要访问我的服务器上的东西,这种情况我可以用nginx反向代理等方式来实现,例如上文nginx的跨域设置删除的时候,localhost:8080的本地访问就失败了。

现在的问题是我需要重定向访问qq网站,按道理应该在qq的网站服务器设置跨域,允许来自我的服务器yuanhuiying.com的访问,可这是不可能的,腾讯的服务器你怎么改,所以得另辟蹊径。我开始考虑放弃使用axios的方式,使用window.location.href或者window.open 打开新页面来访问qqLogin接口url。 任选其一即可。

// window.location.href = oauthTarUrl;
var newWin = window.open(oauthTarUrl,"_self");

这片文章也解开了我之前心中的困惑:ajax发送http请求会有跨域问题,为啥在浏览器地址栏上输入同样的url没有跨域问题? 【ajax发送http请求会有跨域问题,为啥在浏览器地址栏上输入同样的url没有跨域问题?_慕课猿问】

这时,又发现新的问题了,最后callback接口返回的json数据直接显示在页面了,而我最终目的是要把json里的数据存到localstorage里面的啊,如何才能获取新打开的页面的响应数据呢?又是一次次这方面的资料寻找,包括询问部分前端人员,貌似行不通,要想js存数据到localstorage,必须异步ajax请求,这可咋办,两难。。。就这么拖了两天,我想修改下流程,让callback回调先访问一个新加的vue页面,然后在这个vue页面发送axios去访问后端的callback接口,返回token,这样就规避了问题。于是新问题就来啦,๑乛◡乛๑

我把callback重定向地址配置为 https://xxxxx/#/dd 的 时候,发现在重定向的时候,浏览器之请求了https://xxxxx ,#以及后面的内容被吃了。。。第一次遇见这种情况,一查才有所了解,参考 URL中的#、?、&解释 URL中的#、?、&解释_zlingyun的博客-CSDN博客 才有所了解。

这个#是由于 vue 的h hash 默认模式产生的,要想去掉,可以设置 vue 的路由:

const router = new Router({
  mode: 'hash',
  base: process.env.BASE_URL,
  routes: routes,
})

把 mode 的 hash 改为 history ,但是这样会对现有代码的其它路由跳转有影响【比如子路由切换后刷新页面出现空白】,得不偿失,所以放弃了。有兴趣了解相关知识可以参考:不同的历史模式 | Vue Router

那就还是得在callback接口里面做文章,查阅资料受到cookei的启发,于是构思可以手动在callback接口里面把token添加到cookie里面,然后不要直接返回json,而是重定向到一个html页面【vue页面url带#,重定向不过去,所以自己就用了一个html页面】,在页面 里获取cookie,不就得到了token嘛。

修改后端callback代码如下:

@RequestMapping("/qq/callback")
	public void qqCallback(HttpServletRequest request,HttpServletResponse response,Model model) throws Exception {
		ResponseWrapper res = new ResponseWrapper();
		AccessToken accessTokenObj = (new Oauth()).getAccessTokenByRequest(request);
		try {
			if (accessTokenObj.getAccessToken().equals("")) {
				log.error("我们的网站被CSRF攻击了或者用户取消了授权,没有获取到响应参数");
				response.sendRedirect(ConstantUtil.URL_400);
				return;
			} 
			String jwtToken = oAuth2Service.qqCallback(accessTokenObj);
			Cookie cookie = new Cookie(ConstantUtil.cookieToken,jwtToken);
			//加上这两句设置可以处理 新添加的cookie重定向后丢失的问题
			cookie.setDomain("xxxxxx");//域名
			cookie.setPath("/");
			response.addCookie(cookie);
			response.sendRedirect(ConstantUtil.URL_MIDDLE_JUMP);
		} catch (Exception e) {
			e.printStackTrace();
			response.sendRedirect(ConstantUtil.URL_400);
		}
	}

一个小插曲,自己添加的cookie丢失获取不到,代码加上这两句配置就可以了。

//加上这两句设置可以处理 新添加的cookie重定向后丢失的问题
cookie.setDomain("xxxxxx");//域名
cookie.setPath("/");

到这里,已经越来越看见胜利 的曙光了。

接下来,html页面中获取cookie,解析出token,然后该如何存到locastorage呢?还得是靠vue页面,那就再新建一个middle vue页面,在html中跳转过去,顺便把token拼在url后面带到middle vue 页面。然后在middle vue页面,存储token到localstorage,然后跳转到首页,完成登录。

        至此,终于告一段落。在这期间还有很多别的尝试和小问题,有些没什么分量就没有记录,而有些也分量不大,比如vue的路由跳转等等,这些问题属于之前没有接触不知道的内容,现在通过实践有了接触,了解,执行,以后也更得心应手吧。总得来说,一方面对一些知识比较陌生,不了解导致处理问题找不到方向,另外呢,就是在处理问题过程中,能否做到思路清晰,沉稳不慌。毕竟问题一生十,十生百 是常态,有时搞着搞着就抓着乱试,偏离了方向,浪费时间做无用功。

        现在系统页面还有一些小问题,比如vue打包生成的js过大导致页面加载很慢,需要近四十几秒,后端的一些bug等等,我将抓紧时间修复,但主要的问题已经基本搞定。说实话,这套方案我心里也没底会有哪些不合理的地方,比如安全性、性能上等等,后续有时间我将继续思索方案,进行改进性优化。如果大佬觉得不认可,欢迎批评指出,感激不尽!

        那就暂告一段落吧,下次再见!

由于过程中查的文章过多,已经记不全了,这里粘贴几个作为参考:

相关资料文章:

axios 跨域请求详解 - 掘金  axios 跨域请求详解

重定向URL_weixin_30265103的博客-CSDN博客  重定向URL 

javascript - Facebook xhr登录:跨域请求被阻止 - IT工具网  javascript - Facebook xhr登录:跨域请求被阻止

ajax发送http请求会有跨域问题,为啥在浏览器地址栏上输入同样的url没有跨域问题?_慕课猿问 ajax发送http请求会有跨域问题,为啥在浏览器地址栏上输入同样的url没有跨域问题?

URL中的#、?、&解释_zlingyun的博客-CSDN博客  URL中的#、?、&解释

Referrer-Policy - HTTP | MDN

不同的历史模式 | Vue Router

码字不易,感谢支持! 点关注,下次相遇不迷路!

欢迎关注微信公众号:独行侠的守望

记vue nginx springcloud oauth2.0三方系统登录中问题处理过程_第10张图片

    - - 无论如何都要坚定的前行,去拼搏,去奋斗。爱生活,爱自己!码农一枚,主要分享生活日常、兴趣爱好,记录成长。欢迎一起交流!

                                         ​

本文同步发布于个人网站:https://www.yuanhuiying.com    进入主站点击“愚默博客“标签页进行访问。

你可能感兴趣的:(20,问题,处理,vue.js,nginx,spring,cloud)