Spring Boot + Spring Security 应用开启接口跨域

一、为何要提供跨域资源服务

最近在写的一个项目后端使用了Spring Boot和Spring Security,前端使用Vue和axios(发送ajax)。为了实现前后端分离,前端和后端能部署在不同的域名下,后端要开启 CORS(跨站资源共享) ,保证后端接口能被不同域名下的前端正常请求。

由于浏览器的同源策略,跨域请求一般会被拒绝,导致ajax无法访问到后端资源。
跨域的几种场景,可以判断自己的网站是否跨域:

  1. 域名不同
  2. 域名相同,端口不同
  3. 域名相同,协议不同,如http和https

了解了为什么要开启CORS后可以开始编写开启CORS的代码了。

二、Spring Boot配置里开启CORS

Spring Boot开启CORS只需在配置里添加CORS的支持,直接编写配置类即可:

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;

@Configuration
public class CorsConfig {
    private CorsConfiguration buildConfig() {
        CorsConfiguration corsConfiguration = new CorsConfiguration();
        corsConfiguration.addAllowedOrigin("*"); // 允许的域名
        corsConfiguration.addAllowedHeader("*"); // 允许的请求头
        corsConfiguration.addAllowedMethod("*"); // 允许的方法(post、get等)
        return corsConfiguration;
    }

    @Bean
    public CorsFilter corsFilter() {
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", buildConfig()); // 对接口配置跨域设置
        return new CorsFilter(source);
    }
}

三、Spring Security配置

由于使用Spring Security后请求都会被它拦截,需要对security也进行配置:

@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    ...

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                // 如果有允许匿名的url,填在下面
                .anyRequest()
                .permitAll()
                .and()
                .formLogin()
                .loginProcessingUrl("/user/login")
                // 自定义处理器,对ajax请求响应json数据
                .successHandler(new AuthenticationSuccessHandler() {
                    @Override
                    public void onAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {
                        httpServletResponse.setContentType("application/json;charset=utf-8");
                        PrintWriter out = httpServletResponse.getWriter();
                        // 将结果转换成Json数据,Result是自己编写的结果类
                        JSONObject result = JSONObject.fromObject(new Result(Result.SUCCESS, "登录成功!"));
                        out.write(result.toString());
                        out.flush();
                        out.close();
                    }
                })
                // 自定义处理器,对ajax请求响应json数据
                .failureHandler(new AuthenticationFailureHandler() {
                    @Override
                    public void onAuthenticationFailure(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
                        httpServletResponse.setContentType("application/json;charset=utf-8");
                        PrintWriter out = httpServletResponse.getWriter();
                        // 将结果转换成Json数据,Result是自己编写的结果类
                        JSONObject result = JSONObject.fromObject(new Result(Result.ERROR, "登录失败!"));
                        out.write(result.toString());
                        out.flush();
                        out.close();
                    }
                })
                .usernameParameter("username")
                .passwordParameter("password")
                .permitAll()
                .and()
                .logout()
                .logoutUrl("/logout")
                .permitAll();
        // 开启CORS,关闭CSRF跨域
        http.cors().and().csrf().disable();
    }

    @Override
    public void configure(WebSecurity web) {
        // 设置拦截忽略文件夹,可以对静态资源放行
        web.ignoring().antMatchers("/css/**", "/js/**", "/js/**", "/lib/**", "/upload/**");
    }
}

这里有两个坑:

设置CORS和CSRF

一是,下面这一句必须要设置,否则请求会直接403 Forbidden

http.cors().and().csrf().disable();

自定义登录处理器

二是,SpringSecurity通常配置有 http.formLogin().successForwardUrl().failureForwardUrl() 这几个配置,但是这些配置都是基于服务器渲染页面的模式,在前后端分离项目中登录只用发送携带登录信息的ajax,服务器响应结果即可,开启这些跳转配置会使ajax无法正常获取响应。

所以要自定义处理器,登录成功响应对应的Json结果,写到 HttpServletResponse 中:

new AuthenticationSuccessHandler() {
    @Override
    public void onAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {
        httpServletResponse.setContentType("application/json;charset=utf-8");
        PrintWriter out = httpServletResponse.getWriter();
        // 将结果转换成Json数据,Result是自己编写的结果类
        JSONObject result = JSONObject.fromObject(new Result(Result.SUCCESS, "登录成功!"));
        out.write(result.toString());
        out.flush();
        out.close();
    }
}

四、前端设置

在跨域请求的时候,通常会自动发送两个请求。第一个请求方法为OPTION,目的是探测后端是否支持跨域,如果支持跨域再发送真实请求。这里是自动发送不用自己编写代码。

然后在发送登录请求的时候,会遇到前端ajax发送了username和password参数,但是后端收不到参数而验证失败。这里的原因是,前端ajax比如axios通常以json格式发送数据,请求头中 content-typeapplication/json ,但是Spring Security的登录不支持这种格式所以找不到参数。

解决方法有两种,一是重写Spring Security的登录Filter,使其支持json类型数据,操作比较复杂网上有很多相关博客这里就不提了。二是在登录时前端设置数据格式为SpringSecurity支持的 application/x-www-form-urlencoded

五、总结

至此CORS就已经开启,前端就可以愉快地使用ajax访问后端了。


原文:https://marwin.cn/2019/04/07/SpringBoot开启接口跨域/