前言
继上篇文章 spring boot 2.x + shiro + redis实现前后端分离的项目 后有不少网友反应当用户无权限访问的时候,redis还是会多一条session存入的记录,后来证实发现确实如此,下面我们就来看看如何解决这个问题吧!
原因
首先我们来了解一下为什么会在无权限访问的时候会产生session?原因很简单,我们在ShiroConfig配置类中配置了未授权时跳转的页面地址,当我们携带无效的token访问后端系统时,shiro的authc默认拦截器会将请求重定向到未授权页面,而拦截器在重定向之前创建了一个session对象,我们来简单看一下FormAuthenticationFilter拦截器的onAccessDenied方法:
我们在进入saveRequestAndRedirectToLogin方法看下
经打断点查看后,发现在执行saveRequest方法后,redis里面就产生了一条session的记录,我们再进入saveRequest方法看一下
如果有兴趣的同学可以继续进入subject.getSession()方法中查看是如何创建的session的,这边我就不继续拓展下去了。
解决
解决的思路有几种,我这边先提供两种方式
方式一:继承FormAuthenticationFilter拦截器,重写onAccessDenied方法;
方式二:ShiroConfig放弃集成redisSessionDao,手动的去保存、读取和删除session;
方式一的优点主要是修改简单,很容易的解决上面的问题,但缺点是对于不知道源码逻辑的朋友就是一个黑盒,可能会产生其他的问题;
方式二的缺点主要是代码改造量比较大,但优点是可以自由控制,而且redis中存储的信息也可以是非序列化的内容,增加可读性和可修改性;
博主这边只贴出第一种方式的代码,有兴趣的朋友可以自己尝试第二种方法。
首先我们自己创建AuthcShiroFilter类继承FormAuthenticationFilter并重写onAccessDenied方法,具体思路就是:原本应该重定向的到未授权方法,这边不再实现重定向的逻辑,直接返回前端对应的信息即可,如下
public class AuthcShiroFilter extends FormAuthenticationFilter {
@Override
protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception{
if (isLoginRequest(request, response)) {
if (isLoginSubmission(request, response)) {
return executeLogin(request, response);
} else {
return true;
}
} else {
// option请求处理
HttpServletRequest req = (HttpServletRequest) request;
HttpServletResponse resp = (HttpServletResponse) response;
if (req.getMethod().equals(RequestMethod.OPTIONS.name())) {
resp.setStatus(HttpStatus.OK.value());
return true;
}
// 取消重定向,直接返回结果
returnTokenInvalid((HttpServletRequest)request, (HttpServletResponse)response);
return false;
}
}
/**
* 替代shiro重定向
*
* @param req
* @param resp
* @throws IOException
*/
private void returnTokenInvalid(HttpServletRequest req, HttpServletResponse resp) throws IOException {
resp.setHeader("Access-Control-Allow-Origin", req.getHeader("Origin"));
resp.setHeader("Access-Control-Allow-Credentials", "true");
resp.setContentType("application/json; charset=utf-8");
resp.setCharacterEncoding("UTF-8");
Writer out = new BufferedWriter(new OutputStreamWriter(resp.getOutputStream()));
out.write(JSONObject.toJSONString(new Result(ResultStatusCode.INVALID_TOKEN)));
out.flush();
out.close();
}
}
最后在ShiroConfig配置类中重新指定authc的拦截器即可
@Bean
public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager) {
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
shiroFilterFactoryBean.setSecurityManager(securityManager);
// 由于已经重写了authc的拦截器,此处设置的loginUrl和unauthorizedUrl已经没有用了
// 没有登陆的用户只能访问登陆页面,前后端分离中登录界面跳转应由前端路由控制,后台仅返回json数据
//shiroFilterFactoryBean.setLoginUrl("/common/unauth");
// 登录成功后要跳转的链接
//shiroFilterFactoryBean.setSuccessUrl("/auth/index");
// 未授权界面;
//shiroFilterFactoryBean.setUnauthorizedUrl("common/unauth");
//自定义拦截器
Map<String, Filter> filtersMap = new LinkedHashMap<String, Filter>();
//自定义authc访问拦截器
filtersMap.put("authc", new AuthcShiroFilter());
//限制同一帐号同时在线的个数。
filtersMap.put("kickout", kickoutSessionControlFilter());
shiroFilterFactoryBean.setFilters(filtersMap);
// 权限控制map.
Map<String, String> filterChainDefinitionMap = new LinkedHashMap<String, String>();
// 公共请求
filterChainDefinitionMap.put("/common/**", "anon");
// 静态资源
filterChainDefinitionMap.put("/static/**", "anon");
// 登录方法
filterChainDefinitionMap.put("/admin/*ogin*", "anon"); // 表示可以匿名访问
//此处需要添加一个kickout,上面添加的自定义拦截器才能生效
filterChainDefinitionMap.put("/admin/**", "authc,kickout");// 表示需要认证才可以访问
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
return shiroFilterFactoryBean;
}
至此已经全部ok,感谢各位!全部的源码请访问:源码地址