写这篇博客,主要是让自己忘记的时候可以翻起来看看,毕竟人老多忘事。
对于SpringBoot集成Shiro框架,也算是蛮了解的,但是之前搭的项目,前后端都是部署在一起的,所以就不存在跨域的问题,最近接到一个项目,需要前后端分开部署。刚开始想着用SpringBoot集成shiro安全验证,直接做个后端服务系统,应该是蛮简单的,所以就开搞了。
Shiro工作原理是服务端将SessionID写入客户端浏览器的cookie中,客户端发起请求时携带cookie信息,服务端从cookie中读取sessionId,以此来维持会话。但是在前后端分离模式下,我们的后端服务不能将sessionId写到请求浏览器cookie中,而且存在跨域问题,基于安全方面原因,ajax请求也都不带cookie信息,后端程序没办法取得sessionId,也就无法验证登录。
既然这样,那我们可不可以模拟这种工作方式,将SessionID返回前端做保存(写入cookie或localStage中),然后前端发起请求的时候,将Sessionid传给服务器,服务器获取这个SessionID,再取对应的session呢?事实证明,这是可以行的通的,网上也有好多大牛这么做过,只是自己在动手过程中,还是碰到了好些问题,现在就一一记录下来。
后台登录接口,需要将ShiroSession的SessionId返回给前端,前端保存到cookie中,如下:
} catch (LockedAccountException e) {
e.printStackTrace();
retMap.put("msg", "登录失败,该用户已被冻结");
return retMap;
} catch (AuthenticationException e) {
e.printStackTrace();
retMap.put("msg", "用户名或密码错误");
return retMap;
} catch (Exception e) {
e.printStackTrace();
retMap.put("msg", "登录异常:"+e);
return retMap;
}
saveLog(ConstantsUtil.OPT_TYPE_LOGIN,"用户登录-User Login","login()");
retMap.put("code", 200);
retMap.put("token", req.getSession().getId()); //返回SessionID
retMap.put("user", userLogin);
return retMap;
登录成功后,前端JS中接收sessionID(token),并保存到cookie中,
$.ajax({
url: config.apiRoot()+'/test/testlogin.do',
type: 'POST',
beforeSend: function(xhr) {
xhr.withCredentials = true;
},
crossDomain: true,
data: JSON.stringify(params),
contentType: "application/json;charset=UTF-8",
cache: 'false',
success: function(ret) {
if(ret!=null){
if(ret.code==200){
//登录成功
// 登录成功 返回登录前url
localStorage.setItem("admin",JSON.stringify(ret.user));
localStorage.setItem("token",ret.token); //保存SessionID
$.cookie("token",ret.token); //保存SessionID
window.location.href = "index.html";
}else{//未定义业务接收 方法
_log(ret.msg);
}
}
},
error: function(jqXHR, textStatus, errorThrown) {
//TODO:提示消息
_log("登录失败!");
//_log("请求错误:"+JSON.stringify(jqXHR) + "," + textStatus + "," + errorThrown);
}
});
第一,修改Shiro安全配置,重写SessionManager类,创建一个MySessionManager类继承DefaultWebSessionManager类,重写getSessionId方法,先从cookie获取SessionID,取不到再从requestHeader中获取,如取不到再从请求参数中获取,具体如下:
/**
* 重写获取sessionId的方法调用当前Manager的获取方法
*
* @param request
* @param response
* @return
*/
@Override
protected Serializable getSessionId(ServletRequest request, ServletResponse response) {
return this.getReferencedSessionId(request, response);
}
/**
* 获取sessionId从请求中
*
* @param request
* @param response
* @return
*/
private Serializable getReferencedSessionId(ServletRequest request, ServletResponse response) {
String id = this.getSessionIdCookieValue(request, response); //此方法copy自父类
if (id == null) {
// 获取请求头中的sessionId
HttpServletRequest req = (HttpServletRequest) request;
// id = WebUtils.toHttp(request).getHeader(this.authorization);
id = req.getHeader(this.authorization);
if (id == null) {
id = req.getParameter(this.authorization);
if (id == null) {
id = this.getUriPathSegmentParamValue(request, "SID"); //此方法copy自父类
if (id == null) {
String name = this.getSessionIdName(); //此方法copy自父类
id = request.getParameter(name);
if (id == null) {
id = request.getParameter(name.toLowerCase());
}
}
}
}
}
if (id != null) {
request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_SOURCE, "cookie");
request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID, id);
request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_IS_VALID, Boolean.TRUE);
return id;
} else {
return super.getSessionId(request, response);
}
}
第二,修改Shiro配置中的session管理类,配置为我们的MySessionManager类,这里用到了redis作为缓存管理器,
//配置核心安全事务管理器
@Bean(name="securityManager")
public SecurityManager securityManager(@Qualifier("authRealm") AuthRealm authRealm) {
System.err.println("--------------shiro已经加载----------------");
DefaultWebSecurityManager manager=new DefaultWebSecurityManager();
//注入缓存管理器;
manager.setCacheManager(redisCacheManager());//这个如果执行多次,也是同样的一个对象;
manager.setSessionManager(sessionManager());
manager.setRealm(authRealm);
return manager;
}
/**
*
* @描述:sessionManager添加session缓存操作DAO
* @创建人:
* @创建时间:2018年4月24日 下午8:13:52
* @return
*/
@Bean
public DefaultWebSessionManager sessionManager() {
MySessionManager sessionManager = new MySessionManager();//使用我们的MySessionManager管理类
sessionManager.setSessionDAO(redisSessionDAO());
return sessionManager;
}
/**
* 配置shiro redisManager
*
* @return
*/
@ConfigurationProperties(prefix = "spring.redis")
public RedisManager redisManager() {
RedisManager redisManager = new RedisManager();
return redisManager;
}
/**
* cacheManager 缓存 redis实现
*
* @return
*/
public RedisCacheManager redisCacheManager() {
RedisCacheManager redisCacheManager = new RedisCacheManager();
redisCacheManager.setRedisManager(redisManager());
return redisCacheManager;
}
/**
* RedisSessionDAO shiro sessionDao层的实现 通过redis
* <p>
* 使用的是shiro-redis开源插件
*/
@Bean
public RedisSessionDAO redisSessionDAO() {
RedisSessionDAO redisSessionDAO = new RedisSessionDAO();
redisSessionDAO.setRedisManager(redisManager());
return redisSessionDAO;
}
第三,在项目中添加一个过滤器类CrosFilter,处理跨域问题,其中,OPTIONS预处理请求,要返回200,表示成功,否则ajax发起post,get等请求,服务器不能响应。另外,Access-Control-Allow-Headers配置项中,要设置Authorization,而且,Access-Control-Allow-Credentials 要设置成true.
/**
* 〈一句话功能简述〉跨域过滤器<br>
* 〈〉
*
* @author lenovo
* @create 2018/7/4
* @since 1.0.0
*/
@Component
public class CorsFilter implements Filter {
final static org.slf4j.Logger logger = org.slf4j.LoggerFactory.getLogger(CorsFilter.class);
@Override
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
HttpServletResponse response = (HttpServletResponse) res;
HttpServletRequest request = (HttpServletRequest)req;
response.setHeader("Access-Control-Allow-Origin", request.getHeader("Origin"));
response.setHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS, DELETE");
response.setHeader("Access-Control-Max-Age", "3600");
response.setHeader("Access-Control-Allow-Headers", "X-Requested-With,Content-Type,Origin,Authorization, X-File-Name,Cookie,Accept,Connection,User-agent");//
response.setHeader("Access-Control-Allow-Credentials", "true"); //设置允许携带cookie信息
System.out.println("*********************************过滤器被使用**************************");
if (request.getMethod().equals( "OPTIONS" )) { //这里很重要,options请求,直接返回200,表示成功
response.setStatus(200);
return;
}
chain.doFilter(req, res);
}
@Override
public void init(FilterConfig filterConfig) {}
@Override
public void destroy() {}
}
最后,前端ajax发起请求时,需要添加请求头信息,requestHeader添加Authorization项,值为登录时返回的sessionID($.cookie("token")),设置crossDomain为true。具体如下:
var param={};
param.tocken= localStorage.getItem("token");
param.radom = Math.random();
param.currentTime= new Date();
$.ajax({
// 地址格式
url: getApiRoot()+"/checkActiveTT.do",
type: 'post',
beforeSend: function(xhr) {
xhr.withCredentials = true;
xhr.setRequestHeader("Authorization", $.cookie("token"));
},
crossDomain: true,
contentType: 'application/json;charset=UTF-8',
data: JSON.stringify(param),
cache: 'false',
async: false,
success: function (data) {
if (data.code=="200") {
}
else {
toLogin();
}
},
error: function (e) {
toLogin();
}
});
经验证,前后端可以正常交互。这个问题,搞了两天,头壳疼。。
【转载请注明出处——大道迷途】