前后端交互
跨域问题
产生
浏览器的同源策略,同源策略是一个安全策略,用于限制一个origin的文档或者它加载的脚本如何能与另一个源的资源进行交互。它能帮助阻隔恶意文档,减少可能被攻击的媒介。
同源
scheme://host.domain:port/path/filename
- scheme - 定义因特网服务的类型(http、https、ftp、file)
- host - 定义域主机(http的默认主机是www)
- domain - 定义因特网域名(比如baidu.com)
- :port - 定义主机上的端口号(http默认端口号是80)
- path - 定义服务器上的路径
- filename - 定义文档/资源的名称
如果两个URL的scheme、host、domain和port都相同的话,就认为这两个URL是同源。
解决方案
跨域的解决方案有很多JSONP
、空iframe标签加form
、代理、CROS
、JavaScript的跨源脚本API
等方式。
CROS
,全称为Cross Origin Resource Sharing
,跨域资源共享。它是HTTP协议的一部分,允许服务端来指定哪些主机可以从这个服务端加载资源。工作量主要在后端。通过代码和配置文件等方式实现;Nginx代理
(开发环境下可以简单的使用webpack
的proxy
)进行反向代理,原理是搭建一个中转服务器来转发请求规避跨域的问题。JSONP
只能发送GET
请求(不推荐);空iframe
发送POST
请求可与上面结合使用(不推荐)。
实际解决办法
结合自己的项目实际,在SpringMVC的配置文件中配置cros
的支持。
大致如下,按照自己实际需求进行修改(地址已脱敏):
<mvc:cors>
<mvc:mapping path="/**"
allowed-origins="http://localhost:8080"
allowed-methods="POST, PUT, GET, OPTIONS, DELETE"
allowed-headers="Origin, No-Cache, X-Requested-With, If-Modified-Since, Pragma, Last-Modified, Cache-Control, Expires, Content-Type, X-E4M-With,Authorization,Token"
allow-credentials="true"
max-age="5000"
exposed-headers="Token"
/>
</mvc:cors>
Shiro的集成
本来以为解决了跨域的问题,就可以高枕无忧的调整业务代码了,哪成想,还有两个大问题需要解决。
登录成功会产生一个id
为jsessionid
的cookies
,每次请求带着这个,来验证用户是否登录和是否有权限。正因为如此,产生了如下问题。
复杂请求
- 复杂请求(比如请求携带某些请求头)会发送预请求,请求类型为
OPTIONS
,默认不会携带cookies、token等,也无法通过配置让其默认携带。
session超时
- session是有时限的,过期访问时,shiro会重定向到配置文件里配置的
loginUrl
,即默认去寻找Web工程根目录下的"/login.jsp"页面这与前后端分离的设想不符。需要使其返回的是Json格式的返回值,交由前端处理跳转登陆。
解决办法
- 通过继承
org.apache.shiro.web.filter.authc.FormAuthenticationFilter
类,并重写isAccessAllowed
方法,默认放行请求类型为OPTIONS
的请求,其他类型请求的方式仍交由父类的方法处理。 - 同上,重写
onAccessDenied
方法,使没有登录状态时返回json
类型的数据,在前台拦截并跳转到首页。
代码如下
@Override
public boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
// 如果是复杂请求的预请求,请求类型为OPTIONS,而且默认不携带cookies信息,也无法通过配置使之默认携带相关信息
// 所以会被Shiro拦截,导致真正的请求发送不出去。
// 解决的办法有很多,这里采用了放行所有OPTIONS类型请求的办法,并不会产生安全问题
if ("OPTIONS".equals(((HttpServletRequest) request).getMethod().toUpperCase())) {
return true;
}
return super.isAccessAllowed(request, response, mappedValue);
}
@Override
protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
// Shiro在拦截请求验证登录状态的时候,如果没有登录,则会跳转到配置的loginUrl路径,默认是login.jsp
// 这与前后端分离的精神不符。
// 重写这个方法,使登录验证未通过,返回json给前端。交由前端统一处理
HttpServletResponse res = (HttpServletResponse)response;
res.setHeader("Access-Control-Allow-Origin", "*");
res.setStatus(HttpServletResponse.SC_OK);
res.setCharacterEncoding("UTF-8");
PrintWriter writer = res.getWriter();
Map<String, Object> map= new HashMap<>();
map.put("code", 401);
map.put("msg", "Not logged in");
writer.write(JSON.toJSONString(map));
writer.close();
return false;
}
关于Token的问题这里先不讨论