java&angularjs,恼人的No 'Access-Control-Allow-Origin' 解决方案

由于公司的新项目决定使用java 与angularjs进行开发,前后端分离,因此我需要对前端的请求框架进行搭建。接触angularjs是在上一个ionic的项目,感觉angularjs的mvc架构非常出色,尤其对大型项目很有好处。
angularjs的请求是通过ajax的,在请求过程中,有一个很麻烦的问题,那就是跨域。在这次的项目中,打算在请求的http header中加入自token进行身份验证,结果遇到了麻烦。于是现在把解决方案写下来,希望能给自己留一个记忆,并希望能够帮助遇到问题的小伙伴。
首先,任何请求都需要在http header中加入token,我了解到了angularjs里的一个很重要的机制:拦截。
可在config中加入:

config(['$httpProvider', function($httpProvider) {  
$httpProvider.interceptors.push('sessionInjector');
$httpProvider.defaults.headers.mbs-common['X-Requested-With'];  }]);

factory中加入sessionInjector如下:

testService.factory('sessionInjector', ['$localStorage','HOST', function($localStorage,HOST) 
{  var sessionInjector = {    
request: 
function(config) {      
config.headers = config.headers || {};     
 if(config.url.indexOf(HOST.URL)>=0) {   
if ($localStorage.u && $localStorage.u.token) {  
            config.headers.token = $localStorage.u.token;       
 }      
}      
return config;   
}  
};  
return sessionInjector;}]);

代码写好,跑起来一看,token已经成功加入到http header中,很好!但是请求出错了!报:No 'Access-Control-Allow-Origin' 错误。看到这个报错,我的第一反映是跨域问题。但是我前一个ionic+angularjs项目在java端已经解决了跨域问题了,而这个新的项目用的框架与上个项目几乎一样的,为何还会跨域呢?我感觉自己对跨域的理解不够彻底,于是又开始漫长的搜索,各种google,stackoverflow。终于找到了跨域的原因,wiki上说得很清楚:https://en.wikipedia.org/wiki/Cross-origin_resource_sharing
核心问题在于跨域时会先进行一个OPTIONS请求,如果成功了才会进行GET或POST请求。于是马上通过chrome查看OPTIONS请求的结果,果然OPTIONS报403错误。

Paste_Image.png

看到OPTIONS请求的403错误,我就蒙了!怎么回事呢,我明明在java代码中写了一个CorsFilter,并且我也加入了跨域的允许代码:


HttpServletResponse httpResponse = (HttpServletResponse) response;
  HttpServletRequest httpRequest = (HttpServletRequest) request;
  httpResponse.setContentType("text/html;charset=UTF-8");
  httpResponse.setHeader("Access-Control-Allow-Origin",httpRequest.getHeader("Origin"));
  httpResponse.setHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS");
  httpResponse.setHeader("Access-Control-Max-Age", "0");
  httpResponse.setHeader("Access-Control-Allow-Headers", "Origin, No-Cache, X-Requested-With, If-Modified-Since, Pragma, Last-Modified, Cache-Control, Expires, Content-Type, X-E4M-With,authorization,mbs_token");
  httpResponse.setHeader("Access-Control-Allow-Credentials", "true");
  httpResponse.setHeader("XDomainRequestAllowed","1");
         
  chain.doFilter(request, response);

网上说:Access-Control-Allow-Origin不能用"*",而是需要指定请求的域名:httpRequest.getHeader("Origin")

然后我再跑了一次,查看后台日志,发现CorsFilter根本就没有跑!
于是又经过了各种google与stackoverflow,转眼两天下来了,还是无果!心里真是有说不出的郁闷!!!

然后,我怀疑是不是tomcat层面拦截了OPTIONS请求,于是看了一下TOMCAT的请求日志,发现果然有拦截记录,可是不应该TOMCAT层拦截的呀!于是我做了一个HELLO WORD 发现跨域是没问题的,于是放弃这个想法,再继续找原因!

最后,我怀疑是不是Web.xml有问题,这个web.xml有部分是直接复制人家配置好的,没有好好去分析各个节点,于是我一句一句核对,终于发现一个关键地方:

<security-constraint>
        <web-resource-collection>
                <web-resource-name>SSL</web-resource-name>
                <url-pattern>/*</url-pattern>
                <http-method>PUT</http-method>
                <http-method>DELETE</http-method>
                <http-method>HEAD</http-method>
                <http-method>OPTIONS</http-method>
                <http-method>TRACE</http-method>
        </web-resource-collection>  
        <auth-constraint></auth-constraint>
 </security-constraint>

没错,就是这里!:
<http-method>OPTIONS</http-method>

我立即把这段文字删除了,然后跑了一下,奇迹出现了,跨域解决了!!!当时心里真是无比兴奋,从开始解决到完工,我整整花了三天,而且后两天是双休!

于是我花时间了解一下这个关键的<security-constraint>,网上对于这个节点的说明比较少,经过google的查阅发现:
web.xml中<security-constraint> 的子元素 <http-method> 是可选的,如果没有 <http-method> 元素,这表示将禁止所有 HTTP 方法访问相应的资源。
子元素 <auth-constraint> 需要和 <login-config> 相配合使用,但可以被单独使用。如果没有 <auth-constraint> 子元素,这表明任何身份的用户都可以访问相应的资源。也就是说,如果 <security-constraint> 中没有 <auth-constraint> 子元素的话,配置实际上是不起中用的。

<security-constraint> 是java servlet的安全配置,
可参考:http://openhome.cc/Gossip/ServletJSP/DeclarativeSecurityBasic.html

关键的一句:如果加入了 <auth-constraint> 子元素,但是其内容为空,这表示所有身份的用户都被禁止访问相应的资源。
其实在解决的过程中我也注意到过:<http-method>OPTIONS</http-method>,当时我的理解是,允许这个求请方法!
总结:任何技术疑问都不能马虎,要彻底理解才行啊!

推荐阅读更多精彩内容