SpringSecurity前后端分离Header中添加Authorization的设置以及跨域问题踩坑记录
作者:mmseoamin日期:2023-12-11

SpringSecurity前后端分离Header中添加Authorization的设置以及跨域问题踩坑记录

前端登录成功后将后端放在Response header里面Authorization字段提取出来,存入到store(这里我后端Authorization大写,浏览器响应也是大写,我前端原来也是大写但是为undefined,打印header后发现是小写authorization很奇怪)

 store.commit('changeLogin',{ Authorization: res.headers['authorization']});
 this.$router.push('/admin/resource') 

main.js文件里面设置每次请求添加Authorization:token

axios.interceptors.request.use(
  config => {
      if (window.localStorage.getItem('Authorization')) {
          config.headers['Authorization'] = window.localStorage.getItem('Authorization');
      }
      return config;
  },
  error => {
      return Promise.reject(error);
  });

但是这时请求会出现跨域问题,(不加时正常访问)

Access to XMLHttpRequest at ‘http://xxxxxxxx’ from origin ‘http://xxxxxxxx’ has been blocked by CORS policy: Response to preflight request doesn’t pass access control check: No ‘Access-Control-Allow-Origin’ header is present on the requested resource.

​ 这是因为浏览器将CORS请求分成两类:简单请求(simple request)和非简单请求(not-so-simple request)。

1、简单请求

  只要同时满足以下两大条件,就属于简单请求。

(1) 请求方法是以下三种方法之一:HEAD、GET、POST
(2)HTTP的头信息不超出以下几种字段:
Accept
Accept-Language
Content-Language
Last-Event-ID
Content-Type:只限于三个值 application/x-www-form-urlencoded、multipart/form-data、text/plain

凡是不同时满足上面两个条件,就属于非简单请求。

由于我向请求头中添加了自定义的属性,所以发送请求时就属于非简单请求。

2、预检请求

  非简单请求的CORS请求,会在正式通信之前,增加一次HTTP查询请求,称为 “预检” 请求(preflight)。

浏览器先询问服务器,当前网页所在的域名是否在服务器的许可名单之中,以及可以使用哪些HTTP动词和头信息字段。只有得到肯定答复,浏览器才会发出正式的XMLHttpRequest请求,否则就报错。

从上图中可以看出浏览器发送了两次getAllImages请求,一次操作方法为OPTIONS,一次为GET。OPTIONS请求即为预检请求,用于判断后端服务是否能接受OPTIONS请求,注意这个请求是没有Auh字段的。如果没在Header里面添加Auth字段则为一次普通的GET请求。由于后端拦截器逻辑获取Auth字段导致为null,抛出异常,不能执行完整的 filterChain.doFilter导致浏览器认为OPTIONS操作不允许,所以出现上述跨域问题。

   try {
                    String token = httpServletRequest.getHeader("Authorization");
                    System.out.println("token" + token);
                    stringRedisTemplate.opsForSet().isMember("Token", token);
                    filterChain.doFilter(httpServletRequest, httpServletResponse);
                } catch (Exception e) {
                    Map map = new HashMap<>();
                    map.put("code", "401");
                    map.put("message", "无权限");
                    PrintWriter out = httpServletResponse.getWriter();
                    out.write(new ObjectMapper().writeValueAsString(map));
                    out.flush();
                    out.close();
                    System.out.println(e);
                }
            }

OPTIONS请求失败,我全局配置的CORS应该是晚于自定义的handler,所以出现了即使CORS配置了OPTIONS操作的许可,还是出现跨域问题了。

我的解决办法加一次判断如果是OPTIONS直接放行dofilter操作,这样的话浏览器OPTIONS预请求成功,就能发送带Auth的GET请求

if (!httpServletRequest.getRequestURI().equals("/login") && !httpServletRequest.getRequestURI().equals("/getAllTag") && !httpServletRequest.getRequestURI().equals("/getAllImage")) {
            if ("OPTIONS".equals(httpServletRequest.getMethod())) {
                filterChain.doFilter(httpServletRequest, httpServletResponse);
            } else {
                httpServletResponse.setContentType("application/json;charset=utf-8");
                try {
                    this.stringRedisTemplate.opsForSet().isMember("Token", token);
                    filterChain.doFilter(httpServletRequest, httpServletResponse);
                } catch (Exception var7) {
                    Map map = new HashMap();
                    map.put("code", "401");
                    map.put("message", "无权限");
                    PrintWriter out = httpServletResponse.getWriter();
                    out.write((new ObjectMapper()).writeValueAsString(map));
                    out.flush();
                    out.close();
                }
            }
        } else {
            filterChain.doFilter(httpServletRequest, httpServletResponse);
        }

或者简单Security配置一下

@Override
protected void configure(HttpSecurity http) throws Exception {
    http
        // by default uses a Bean by the name of corsConfigurationSource
        .cors().and()
        //跨域请求会先进行一次options请求
        .antMatchers(HttpMethod.OPTIONS).permitAll()
        ...
}

后面我像设置一下别处登录后上一个登录的终端请求失效

 @Override
    protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {
        if (httpServletRequest.getRequestURI().equals("/login") || httpServletRequest.getRequestURI().equals("/getAllTag")
                || httpServletRequest.getRequestURI().equals("/getAllImage")
                || httpServletRequest.getRequestURI().equals("/vercode")) {
            filterChain.doFilter(httpServletRequest, httpServletResponse);
        } else {
            if ("OPTIONS".equals(httpServletRequest.getMethod())) {
                filterChain.doFilter(httpServletRequest, httpServletResponse);
            } else {
                try{
            
                httpServletResponse.setContentType("application/json;charset=utf-8");
              
                String token = httpServletRequest.getHeader("Authorization");
              
                
                    Claims claims = jwtUtil.getClaimsByToken(token);
                 
                    Boolean redisToken = redisTemplate.opsForHash().hasKey("Token", claims.get("sub", String.class));
                    if (redisToken) {
                        if (token.equals(redisTemplate.opsForHash().get("Token", claims.get("sub", String.class)))) {
                            filterChain.doFilter(httpServletRequest, httpServletResponse);
                        } else {
                            httpServletResponse.setHeader("Access-Control-Allow-Origin", "*");
                            Map map = new HashMap<>();
                            map.put("code", "403");
                            map.put("message", "账户已在别处登录");
                         
                            PrintWriter out = httpServletResponse.getWriter();
                            out.write((new ObjectMapper()).writeValueAsString(map));
                            out.flush();
                            out.close();
                        }
                    }
                } catch (Exception e){
                    System.out.println(e);
                    Map map = new HashMap<>();
                    map.put("code", "401");
                    map.put("message", "无权限");
                    PrintWriter out = httpServletResponse.getWriter();
                    out.write((new ObjectMapper()).writeValueAsString(map));
                    out.flush();
                    out.close();
                }
            }
        }

又出现需要认证接口的跨域问题了

No ‘Access-Control-Allow-Origin‘ header is present on the requested resource.

后面查阅资料发现可以自定义一个requestfilter,header添加上Access-Control-Allow-Origin

@WebFilter(filterName = "requestFilter", urlPatterns = {"/*"})
public class RequestFilter implements Filter {
 
    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
    }
 
    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
 
        HttpServletResponse response = (HttpServletResponse) servletResponse;
        HttpServletRequest request = (HttpServletRequest) servletRequest;
        
        // 此处 setHeader、addHeader 方法都可用。但 addHeader时写多个会报错:“...,but only one is allowed”
        response.setHeader("Access-Control-Allow-Origin", "*"); 
//        response.addHeader("Access-Control-Allow-Origin", request.getHeader("origin"));
        // 解决预请求(发送2次请求),此问题也可在 nginx 中作相似设置解决。
        response.setHeader("Access-Control-Allow-Headers", "x-requested-with,Cache-Control,Pragma,Content-Type,Token, Content-Type");
        response.setHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS, DELETE");
        response.setHeader("Access-Control-Max-Age", "3600");
        response.setHeader("Access-Control-Allow-Credentials", "true");
        String method = request.getMethod();
        if (method.equalsIgnoreCase("OPTIONS")) {
            servletResponse.getOutputStream().write("Success".getBytes("utf-8"));
        } else {
            filterChain.doFilter(servletRequest, servletResponse);
        }
    }
 
    @Override
    public void destroy() {
 
    }

我就想着直接在自定义tokenfilter中需要验证访问接口逻辑返回段添加 httpServletResponse.setHeader(“Access-Control-Allow-Origin”, “*”);

   if (redisToken) {
                        if (token.equals(redisTemplate.opsForHash().get("Token", claims.get("sub", String.class)))) {
                            filterChain.doFilter(httpServletRequest, httpServletResponse);
                        } else {
                            httpServletResponse.setHeader("Access-Control-Allow-Origin", "*");
                            Map map = new HashMap<>();
                            map.put("code", "403");
                            map.put("message", "账户已在别处登录");
                            System.out.println("hahahahahahah");
                            PrintWriter out = httpServletResponse.getWriter();
                            out.write((new ObjectMapper()).writeValueAsString(map));
                            out.flush();
                            out.close();
                        }
}

发现前端访问不跨域了。

查找原因:

界面不需要认证的接口请求:

需要认证的接口

对比两种GET请求 发现getAllImages请求下返回响应头没有 Access-Control-Allow-Origin: http://localhost:8080

字段所以造出浏览器跨域问题。

检查后端代码发现成功的接口都完整执行了filterChain.doFilter(httpServletRequest, httpServletResponse);

所以猜测自定义的tokenfilter在securityfilter中添加这个响应头的过滤器之前,这里自定义TokenFilter验证接口验证失败直接执行了httpservletresponse.getWriter()方法返回响应,导致后面的过滤器链给响应添加 Access-Control-Allow-Origin

的方法为执行,所以造成了跨域问题,即使自己已经全局配置了跨域。

main.js文件配置请求和响应拦截器:

axios.interceptors.response.use(
  res=>{
    console.log(res)
      if( res.data.code == 403){
        alert("!!已在其他终端登录,请重新登录!!")
        window.localStorage.clear
        //记住是router 和上面router.beforeEach一样,此时app还未喧染。this不能用,$不能用
        router.push("/login")
      }else if(res.data.code == 401){
        alert("!!无权限!!")
        return res;
      }else{
       return res;
      }
  },
  err=>{
    alert("!!无权限!!")
  }
)
//请求拦截
axios.interceptors.request.use(
  config => {
      if (window.localStorage.getItem('Authorization')) {
        console.log(window.localStorage.getItem('Authorization')+"679t7867876")
          config.headers['Authorization'] = window.localStorage.getItem('Authorization');
      }
      return config;
  },
  error => {
      return Promise.reject(error);
  });