目录

项目场景:

问题描述:

解决方案:

通用

终极方案

方案一:

方案二:

骚操作:


 


项目场景:

目前在重构一个导购后端系统,我负责用户的模块和登录鉴权的整个业务的架构设计和代码编写

利用SpringBoot + SpringSecurity + Oauth2完成了简单的登录和鉴权

登录功能:首先需要调用/oauth/token接口,根据用户名密码获取toke,拿到token后将token放入请求头Header中再去请求其它接口。

接口开发完成并使用Postman测试通过,再由前端去画页面联调接口。


问题描述:

问题就出现在了前端接口联调这里,前端是用Vue完成的,安装好Nodejs环境使用 npm run dev 本地运行

此时前端的服务端口是8080,后端的用户鉴权服务使用的端口为8081,这里端口不一致就出现了跨域问题(端口、协议、域名不一致都会出现跨域问题)

前端和后端当然不可能同时使用8080端口

这里Vue提供了一个代理服务器,只需要在index.js中配置proxyTable相关的信息即可

    proxyTable: {
      // 这里配置 '/api' 就等价于 target , 你在链接里访问 /api === http://localhost:8081
      '/api': {
        target: 'http://localhost:8081/', // 真实服务器的接口地址
        secure: true, // 如果是 https ,需要开启这个选项
        changeOrigin: true, // 是否是跨域请求
        pathRewirte: {
          // 这里是追加链接,比如真是接口里包含了 /api,就需要这样配置.
          '/^api': 'api/', 
          // 等价于 
          // step 1  /api = http://localhost:8081/
          // step 2 /^api = /api + api == http://localhost:8081/api
        }
      }
     }

 这当然是一种解决方案,还有一种是前端工程打包放入Nginx,由Nginx做请求转发,这个只适用于生产环境。

但是我们遇到的问题是配置的代理不生效,使用了所有的办法proxyTable配置居然不生效,所以跨域的这个问题交给了后端。


解决方案:

这里不再对proxyTable不生效的问题进行分析,而是结合目前使用的框架进行后端跨域的配置。

通用

因为使用的是SpringBoot+SpringSecurity框架,所以需要编写Cors跨域的配置,并在WebSecurityConfigurerAdapter安全适配器中添加

Cors配置类:

@Configuration
public class CorsConfig implements WebMvcConfigurer {

    @Override
    public void addCorsMappings(CorsRegistry registry) {
        // 允许所有请求跨域
        registry
                .addMapping("/**")
                .allowedMethods("*")
                .allowedOrigins("*")
                .allowCredentials(true)
                .allowedHeaders("*")
                .maxAge(3600);
    }

    private CorsConfiguration buildConfig() {
        CorsConfiguration corsConfiguration = new CorsConfiguration();
        corsConfiguration.addAllowedOrigin("*"); // 1
        corsConfiguration.addAllowedHeader("*"); // 2
        corsConfiguration.addAllowedMethod("*"); // 3
        return corsConfiguration;
    }

    @Bean
    public CorsFilter corsFilter() {
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", buildConfig()); // 4
        return new CorsFilter(source);
    }
}

WebSecurityConfigurerAdapter配置:

    @Override
    protected void configure(HttpSecurity http) throws Exception {
            http
                // 跨域
                .cors()
                .and()
                // 禁用csrf
                .csrf().disable();

在这里加上cors()之后,会从Spring容器中拿到一个名字为CorsFilter的过滤器Bean,也就是刚开始配置的过滤器(CorsConfig),从对应的源码也能看到相应的注释

	/**
	 * Adds a {@link CorsFilter} to be used. If a bean by the name of corsFilter is
	 * provided, that {@link CorsFilter} is used. Else if corsConfigurationSource is
	 * defined, then that {@link CorsConfiguration} is used. Otherwise, if Spring MVC is
	 * on the classpath a {@link HandlerMappingIntrospector} is used.
	 *
	 * @return the {@link CorsConfigurer} for customizations
	 * @throws Exception
	 */
	public CorsConfigurer<HttpSecurity> cors() throws Exception {
		return getOrApply(new CorsConfigurer<>());
	}

一般情况下,配置完这个跨域过滤器之后就可以解决跨域的问题了

再次尝试请求/oauth/token接口,这下可以成功获取token,前端工程端口8080,后端服务端口8081

之后拿着token去请求其它接口,在这一步再次出现问题。

前端js没有问题:

  methods: {
    // 获取省份列表
    getProvinceList() {
      let that = this
      that.axios.get('/provinceList',{
        headers:{
          'Authorization': 'Bearer ' + localStorage.getItem('access_token')
        }
      }
    ).then(data => {
.....

页面对/provinceList发起了两次请求,其中有一次请求为OPTIONS

Request URL:http://127.0.0.1:8081/provinceList

Request Method:OPTIONS

Status Code:401

Request Headers:

Accept:*/*

Accept-Encoding:gzip, deflate, br

Accept-Language:zh-CN,zh;q=0.9

Access-Control-Request-Headers:authorization

Access-Control-Request-Method:GET

之后的Get请求也是失败

原来在请求接口之前会有一次预检请求,以检测实际请求是否可以被服务器所接受。预检请求报文中的Access-Control-Request-Method 首部字段告知服务器实际请求所使用的 HTTP 方法;Access-Control-Request-Headers 首部字段告知服务器实际请求所携带的自定义首部字段。服务器基于从预检请求获得的信息来判断,是否接受接下来的实际请求。

从上面的请求信息也知道了请求失败的原因:

由于SpringSecurity对该OPTIONS请求进行了拦截,发现头部没有携带Token信息,直接返回了401的状态,浏览器认为预检请求不通过,之后的真实请求当然也认为是不通过的。

所以通用方案只能起部分作用。

终极方案

既然知道了原因,那解决起来就容易多了。

方案一:

直接放行OPTIONS请求。

这种方案简单粗暴,既然被SpringSecurity拦截了,那就直接在配置中放行OPTIONS请求。

    @Override
    protected void configure(HttpSecurity http) throws Exception {
            http
                // 放行所有OPTIONS请求
                .antMatchers(HttpMethod.OPTIONS).permitAll()
                // 其他的需要登陆后才能访问
                .anyRequest().authenticated()
                .and()
                // 跨域
                .cors()
                .and()
                // 禁用csrf
                .csrf().disable();

但是这种方案存在一定缺陷,因为我们在编写接口时,有些接口对请求方式没有做具体要求(例如:@RequestMapping("/xxx")),这会导致某些资源没有做权限校验就拿到了,安全性不高。

方案二:

在SpringSecurity进行权限校验之前拦截OPTIONS请求,直接返回200。这种方案不会造成服务器资源的安全问题。

操作步骤也很简单,在SpringSecurity的FilterChain拦截链条中添加自定义的拦截器,设置这个拦截器的顺序在权限校验拦截器之前,在拦截器里直接过滤掉OPTIONS请求。

自定义拦截器:

public class OptionsFilter implements Filter {
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        HttpServletRequest req = (HttpServletRequest) request;
        HttpServletResponse resp = (HttpServletResponse) response;
        // 判断请求是否为OPTIONS
        if (req.getMethod().equals(HttpMethod.OPTIONS.name())) {
            // 响应状态设置为200
            resp.setStatus(HttpStatus.SC_OK);
            // 响应头
            resp.setHeader("Access-Control-Allow-Origin", "*");
            resp.setHeader("Access-Control-Allow-Methods", "*");
            resp.setHeader("Access-Control-Max-Age", "3600");
            resp.setHeader("Access-Control-Allow-Headers", "*");
            return;
        }
        chain.doFilter(request, response);
    }
}

加入SpringSecurity的拦截链条:

    @Override
    protected void configure(HttpSecurity http) throws Exception {
            http
                // 放行所有OPTIONS请求
                .antMatchers(HttpMethod.OPTIONS).permitAll()
                // 其他的需要登陆后才能访问
                .anyRequest().authenticated()
                .and()
                // 过滤OPTIONS请求
                .addFilterBefore(new OptionsFilter(), WebAsyncManagerIntegrationFilter.class)
                .and()
                // 跨域
                .cors()
                .and()
                // 禁用csrf
                .csrf().disable();

代码中把OptionsFilter过滤器加到了WebAsyncManagerIntegrationFilter这个过滤器之前,当然换成其他也可以,但必须是在读取头部Token之前

保存重启工程,前端请求没有问题,至此就彻底解决了跨域的问题。

骚操作:

除了以上两种解决方案,还有一种

直接上代码

// web安全适配器
@Configuration
@EnableGlobalMethodSecurity(securedEnabled = true,prePostEnabled = true)
@Order(-1)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    .....

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.requestMatchers().antMatchers(HttpMethod.OPTIONS, "/oauth/token", "/rest/**", "/api/**")
                .and()
                // 跨域
                .cors()
                .and()
                // 禁用csrf
                .csrf().disable();
    .....

骚操作最简单,至于原理就不细说了