前端登录成功后将后端放在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); });