前后端分离的优点:1:最大的好处就是前端JS可以做很大部分的数据处理工作,对服务器的压力减小到最小2:后台错误不会直接反映到前台,错误接秒较为友好3:由于后台是很难去探知前台页面的分布情况,而这又是JS的强项,而JS又是无法独立和服务器进行通讯的。所以单单用后台去控制整体页面,又或者只靠JS完成效果,都会难度加大,前后台各尽其职可以最大程度的减少开发难度。
思考:前后端分离我们会遇到哪些问题?
1、ajax跨域问题,因为浏览器的同源策略,导致无法在前端页面的域名下,无法访问后台域名下的接口。
2、最终要的就是会话,因为ajax请求每次过来都是无状态,导致用户前台登录后,访问敏感数据时,后台认为它没有登录。
一、跨域的解决方案
(1)jquery的jsop方式
这种方式是通过在文档中嵌入一个<script>标记来从另一个域中返回数据,这种方法拥有一个显著的缺点,那就是只支持GET操作,传输量小。
cors解决跨域,推荐阅读cors跨域介绍
对比两种方式,推荐使用cors方式解决跨域。
二、会话的维护
在shiro框架中,我们通过看源码知道,shiro在DefaultWebSessionManager这个类中获取sessionId是从cookie中获取的,这个时候我们有了两个解决思路:
1、第一个,重写获取sessionId的方法,从request请求头中获取sessionId.
2、第二个,让ajax请求在提交的时候带有cookie信息,不用重写获取sessionId的方法
三、实现cors跨域+request请求头中获取sessionId
(1)、新建一个类,继承DefaultWebSessionManager 重写 getSessionId这个方法
package com.mcu.system.filter;
import java.io.Serializable;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import org.apache.shiro.web.servlet.ShiroHttpServletRequest;
import org.apache.shiro.web.session.mgt.DefaultWebSessionManager;
import org.apache.shiro.web.util.WebUtils;
import com.mcu.common.utils.StringUtils;
/**
* 从请求头中获取sessionid
* @author 35168
*
*/
public class CustomDefaultWebSessionManager extends DefaultWebSessionManager{
private final String TOKEN = "token";
/**
* 获取session id
* 前后端分离将从请求头中获取jsesssionid
*/
@Override
protected Serializable getSessionId(ServletRequest request, ServletResponse response) {
// 从请求头中获取token
String token = WebUtils.toHttp(request).getHeader(TOKEN);
// 判断是否有值
if (StringUtils.isNoneBlank(token)) {
// 设置当前session状态
request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_SOURCE, "url");
request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID, token);
request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_IS_VALID, Boolean.TRUE);
return token;
}
// 若header获取不到token则尝试从cookie中获取
return super.getSessionId(request, response);
}
}
sessionId在用户登录的时候,返回,例如:
@Log("登录")
@PostMapping("/login")
@ResponseBody
R ajaxLogin(String username, String password,HttpServletRequest request) {
//R 是一个HashMap
UsernamePasswordToken token = new UsernamePasswordToken(username, password);
Subject subject = SecurityUtils.getSubject();
try {
subject.login(token);
R r = R.ok();
r.put("token", subject.getSession().getId());
return r;
} catch (AuthenticationException e) {
return R.error(e.getMessage());
}
}
在用户登录成功后,就可以从返回信息中得到token,并在后面的请求中,都在请求头中添加上
(2)cors解决跨域问题
在推荐文章中,我们可以了解到,要想跨域重要的是后台,返回信息,并且还有简单请求和非简单请求,我们应该在后台添加一个过滤器,例如
package com.mcu.system.filter;
import java.io.IOException;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.filter.OncePerRequestFilter;
/**
* 跨域资源共享 解决前后端分离
* @author 35168
*
*/
public class CorsFilter extends OncePerRequestFilter{
private static Logger logger = LoggerFactory.getLogger(CorsFilter.class);
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
String origin = request.getHeader("Origin");
if(null != origin && !"".equals(origin)){
logger.debug("跨域资源共享 解决前后端分离Access-Control-Allow-Origin"+origin);
response.addHeader("Access-Control-Allow-Origin",origin);//允许这个域名可以跨域
response.setHeader("Access-Control-Allow-Headers", "Content-Type,x-requested-with,token");//允许携带的请求头
response.setHeader("Access-Control-Allow-Methods", "OPTIONS, GET, PUT, POST, DELETE");//支持的请求方法
if(request.getMethod().equals(RequestMethod.OPTIONS.name())){
response.setStatus(200);
response.setHeader("Access-Control-Max-Age", "86400");//如果是预检请求,设置一个有效期,在有效期内就不会再有预检请求了
return;
}
}
filterChain.doFilter(request,response);
}
}
三、实现cors跨域+ajax请求在提交的时候带有cookie
这样我们不用重写shiro虎丘sessionId的方法,只要ajax请求带有cookie就可以了,ajax要想带有cookie信息,需要两个方面的工作
(1)、后台允许前台携带cookie
package com.mcu.system.filter;
import java.io.IOException;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.filter.OncePerRequestFilter;
/**
* 跨域资源共享 解决前后端分离
* @author 35168
*
*/
public class CorsFilter extends OncePerRequestFilter{
private static Logger logger = LoggerFactory.getLogger(CorsFilter.class);
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
String origin = request.getHeader("Origin");
if(null != origin && !"".equals(origin)){
logger.debug("跨域资源共享 解决前后端分离Access-Control-Allow-Origin"+origin);
response.addHeader("Access-Control-Allow-Origin",origin);
response.addHeader("Access-Control-Allow-Credentials","true");//允许携带cookie
response.setHeader("Access-Control-Allow-Headers", "Content-Type,x-requested-with");
response.setHeader("Access-Control-Allow-Methods", "OPTIONS, GET, PUT, POST, DELETE");
if(request.getMethod().equals(RequestMethod.OPTIONS.name())){
response.setStatus(200);
response.setHeader("Access-Control-Max-Age", "86400");
return;
}
}
filterChain.doFilter(request,response);
}
}
(2)、是前台提交的时候,要指定ajax可以携带cookie
$.post({
url: '',
dataType: "json",
timeout:300000,
xhrFields:{withCredentials: true},
success: function (result) {
},
error: function () {
}
});
这样,前台指定携带cookie,后台也允许携带cookie,那ajax在提交的时候就可以携带cookie,shiro框架也就可以从cookie中获取session
四、一些细节的问题
shiro框架,封装了很多的东西,在前后端分离的时候,我们需要考虑的,比如shiro框架实现了退出方法,然后跳转到登录页面,这个在我们前后端分离中就不太合适了,需要修改
package com.mcu.system.filter;
import java.io.PrintWriter;
import javax.servlet.ServletContext;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import org.apache.shiro.session.SessionException;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.web.filter.authc.LogoutFilter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.alibaba.fastjson.JSONObject;
/**
* 自定义退出的拦截器,退出返回json
* @author 35168
*
*/
public class SystemLogoutFilter extends LogoutFilter{
private static final Logger log = LoggerFactory.getLogger(SystemLogoutFilter.class);
@Override
protected boolean preHandle(ServletRequest request, ServletResponse response) throws Exception {
Subject subject = getSubject(request, response);
try {
subject.logout();
} catch (SessionException ise) {
log.debug("Encountered session exception during logout. This can generally safely be ignored.", ise);
}
response.setCharacterEncoding("UTF-8");
PrintWriter out = response.getWriter();
JSONObject result = new JSONObject();
result.put("code", 0);
result.put("msg", "成功");
out.println(result);
out.flush();
out.close();
return false;
}
}
重写shiro的退出过滤器,在注册的时候注册我们自己写的这个过滤器,其他的有返回页面的也都是类似的处理,保证给前台统一返回的都是json数据