问题背景
公司项目是前后端分离的,最近要求在请求时都要在请求头加入自定义的 token
,在做接口调试时,前端总是请求不通,然而自己用 POSTMAN
等工具时都可以,这就出现了问题,也就是 复杂请求 的跨域问题。
问题分析
部分文段摘自 跨域资源共享 CORS 详解
复杂请求
浏览器将CORS
请求分成两类:简单请求(simple request
)和非简单请求(not-so-simple request
)。
只要同时满足以下两大条件,就属于简单请求。
- 请求方法是以下三种方法之一:
HTTP
的头信息不超出以下几种字段:1 2 3 4 5
| Accept Accept-Language Content-Language Last-Event-ID Content-Type: 只限于三个值 application/x-www-form-urlencoded multipart/form-data text/plain
|
这是为了兼容表单(form
),因为历史上表单一直可以发出跨域请求。AJAX
的跨域设计就是,只要表单可以发,AJAX
就可以直接发。
凡是不同时满足上面两个条件,就属于非简单请求。
预检请求
非简单请求的CORS
请求,会在正式通信之前,增加一次HTTP
查询请求,称为”预检”请求(preflight
)。
浏览器先询问服务器,当前网页所在的域名是否在服务器的许可名单之中,以及可以使用哪些HTTP
动词和头信息字段。只有得到肯定答复,浏览器才会发出正式的XMLHttpRequest
请求,否则就报错。
过滤器
由于项目中的 shiro
使用了 UserFilter
, 下面是其代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| public class UserFilter extends AccessControlFilter { public UserFilter() { }
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) { if (this.isLoginRequest(request, response)) { return true; } else { Subject subject = this.getSubject(request, response); return subject.getPrincipal() != null; } }
protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception { this.saveRequestAndRedirectToLogin(request, response); return false; } }
|
可以看出过滤器在过滤时上面的判断是用来判断是否为登录请求的,否则就去寻找登录凭证。而在 OPTIONS
请求中,是没有携带上 token
信息的,下面是当时情况下请求的 header
:
1 2 3 4 5 6 7 8 9 10 11 12
| === MimeHeaders === host = 192.168.7.139:4000 connection = keep-alive accept = */* access-control-request-method = POST access-control-request-headers = content-type,x-admin-token origin = http://192.168.7.117:8080 sec-fetch-mode = cors referer = http://192.168.7.117:8080/ user-agent = Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.88 Safari/537.36 accept-encoding = gzip, deflate accept-language = zh-CN,zh;q=0.9
|
可以看到 token
是被带在了 access-control-request-headers
中,这样 shiro
是找不到登录凭证的,请求自然就被拒绝了。
问题解决
解决办法就是重写 UserFilter
(具体看项目用的是哪个过滤器) 的 isAccessAllowed
方法,代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| public class StatelessAuthcFilter extends UserFilter {
@Override protected boolean preHandle(ServletRequest request, ServletResponse response) throws Exception { HttpServletRequest httpRequest = WebUtils.toHttp(request); HttpServletResponse httpResponse = WebUtils.toHttp(response); if (httpRequest.getMethod().equals(RequestMethod.OPTIONS.name())) { httpResponse.setHeader("Access-control-Allow-Origin", httpRequest.getHeader("Origin")); httpResponse.setHeader("Access-Control-Allow-Methods", httpRequest.getMethod()); httpResponse.setHeader("Access-Control-Allow-Headers", httpRequest.getHeader("Access-Control-Request-Headers")); httpResponse.setStatus(HttpStatus.OK.value()); return false; } return super.preHandle(request, response); } }
|
然后再去 shiro 配置中将 user 过滤器修改为自定义的过滤器:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
|
@Bean public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager) { ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean(); shiroFilterFactoryBean.setSecurityManager(securityManager); shiroFilterFactoryBean.setLoginUrl(loginUrl); shiroFilterFactoryBean.setUnauthorizedUrl(unauthorizedUrl);
Map<String, Filter> filters = new LinkedHashMap<>(); filters.put("user", new StatelessAuthcFilter()); shiroFilterFactoryBean.setFilters(filters);
Map<String, String> filterChainDefinitionMap = new LinkedHashMap<>(); filterChainDefinitionMap.put("/favicon.ico**", "anon"); filterChainDefinitionMap.put("/css/**", "anon"); filterChainDefinitionMap.put("/docs/**", "anon"); filterChainDefinitionMap.put("/fonts/**", "anon"); filterChainDefinitionMap.put("/img/**", "anon"); filterChainDefinitionMap.put("/ajax/**", "anon"); filterChainDefinitionMap.put("/js/**", "anon"); filterChainDefinitionMap.put("/auth/login", "anon"); filterChainDefinitionMap.put("/**", "user"); shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap); return shiroFilterFactoryBean; }
|
本以为这样就好了,但实际上在页面重定向后再请求接口还是有问题,这是因为重定向会会默认把请求头清空,所以还需要将 onAccessDenied
方法重写,完整的代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| public class StatelessAuthcFilter extends UserFilter {
@Override protected boolean preHandle(ServletRequest request, ServletResponse response) throws Exception { HttpServletRequest httpRequest = WebUtils.toHttp(request); HttpServletResponse httpResponse = WebUtils.toHttp(response); if (httpRequest.getMethod().equals(RequestMethod.OPTIONS.name())) { httpResponse.setHeader("Access-control-Allow-Origin", httpRequest.getHeader("Origin")); httpResponse.setHeader("Access-Control-Allow-Methods", httpRequest.getMethod()); httpResponse.setHeader("Access-Control-Allow-Headers", httpRequest.getHeader("Access-Control-Request-Headers")); httpResponse.setStatus(HttpStatus.OK.value()); return false; } return super.preHandle(request, response); }
@Override protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception { HttpServletResponse httpResp = WebUtils.toHttp(response); HttpServletRequest httpReq = WebUtils.toHttp(request);
httpResp.addHeader("Access-Control-Allow-Origin", httpReq.getHeader("Origin")); httpResp.addHeader("Access-Control-Allow-Headers", "*"); httpResp.addHeader("Access-Control-Allow-Methods", "*"); httpResp.addHeader("Access-Control-Allow-Credentials", "true");
this.saveRequestAndRedirectToLogin(request, response); return false; }
}
|
这样就解决了 shiro
导致的跨域问题,如果内容对你有所帮助,可以分享给你的好友共同学习。