shiro 拦截未登录的ajax_Shiro是如何拦截未登录请求的(一)

问题描述

之前在公司搭项目平台的时候权限框架采用的是shiro,由于系统主要面向的是APP端的用户,PC端仅仅是公司内部人员在使用,而且考虑到系统的可用性和扩展性,服务端首先基于shiro做了一些改造以支持多数据源认证和分布式会话(关于分布式session可查看{% post_link SpringBoot集成Shiro实现多数据源认证授权与分布式会话(一)%}).我们知道在web环境下http是一种无状态的通讯协议,要想记录和校验用户的登录状态必须通过session的机制来实现,浏览器是通过cookie中存储的sessionid来确定用户的session数据的,shiro默认也是采用这种机制.而对于移动端用户来讲,则可以使用token的方式来进行身份鉴权,原理跟浏览器使用cookie传输是一样的.

token身份鉴权的流程

1.服务端在用户正常登录之后,通过特定算法生成一个全局唯一的字符串token并返回给客户端.

2.客户端在接下来的请求都会在请求头中携带token,服务端拦截token并对用户做身份鉴权.

3.token带有自动失效的机制,当用户主动退出或者失效时间一到则服务端删除会话信息.

遇到的问题

网上查了一下我们知道shiro也是通过携带cookie中的sessionid来做鉴权的,既然移动端使用的是token的机制,那么要想使shiro能够支持这套机制就必须改造shiro的鉴权方式.之前在搭框架的时候为了解决这个问题曾经草草的翻了一下shiro的源码(这货的代码量真心大啊,看的人一头雾水),找了很久也没找到它是在何处处理的,当时因为时间关系只好放弃,用了一种很笨的方法在请求头header中存储以键为Cookie,值为token=web_session_key-xxx的键值对的方式来确保shiro能通过解析校验,这样app端是能够正常交互的,但是对于后面增加的h5应用或者小程序则不行,首先是跨域问题(关于跨域可查看{% post_link 前后端分离之CORS跨域请求踩坑总结%}),由于是前后端分离的应用,浏览器的同源策略不允许js访问跨域的cookie,这样每次请求shiro获取的cookie都为空,过滤器会拦截下这个请求并作出如下响应:

image.png

为了h5应用能够与服务端正常交互只好想办法绕过shiro的拦截校验,既然无法传输cookie,只好在header中传一个token,并在自定义的过滤器(继承自shiro的FormAuthenticationFilter)中覆写它的isAccessAllowed方法,此方法返回值若为true则说明shiro鉴权通过,否则执行redirectToLogin方法跳转到登录页面.

@Override

protected boolean isAccessAllowed(ServletRequest request,

ServletResponse response, Object mappedValue) {

HttpServletRequest httpRequest = (HttpServletRequest) request;

boolean isLogin;

String device = httpRequest.getHeader("device");

// 如果是客户端是H5

if (StringHelpUtils.isNotBlank(device) && device.equals("H5")) {

String h5Token = httpRequest.getHeader("token");

Cookie[] cookies = httpRequest.getCookies();

if (null != cookies) {

for (Cookie cookie : cookies) {

if (cookie.getName().equals("token")) {

cookie.setValue(h5Token);

}

}

}

isLogin = isH5Login(h5Token);//绕过shiro,直接到redis中校验token

} else {

// 如果是APP或者PC端

Subject subject = getSubject(request, response);

isLogin = subject.isAuthenticated();

}

return isLogin;

}

到这里基本上shiro的登录校验是绕过去了,其实这里并不是真的绕过,因为shiro该做的事情还是会照做,只不过是我们再到redis中去匹配一次而已,但是却带来了一个新的问题,那就是服务端通过SecurityUtils.getSubject().getSession();取到的用户session对象与之前登录时产生的session对象并不是同一个,原因是shiro本身在执行校验时由于无法获取到cookie中的token,所以它把这个请求当成是一个新的请求,每次调用都会创建一个新的session,但这个新session里面并不存在我们需要的用户相关登录信息,而由于app与小程序是同一套接口,这样就影响到了原先已写好的业务代码了...虽然解决方法还是有的,但是总觉得整个过程下来,代码和功能的实现都让人觉得很别扭,因此本文想从源码的角度去逐步剖析shiro是如何拦截未登录请求的,从根源上来寻求解决方案,同时又不会对已对接好的业务接口造成影响.

源码跟踪

在开始跟源码之前,我们先来看看下面的异常堆栈图

image.png

之所以要帖这个图是因为shiro的代码实在太多,全部去看不太现实,因此在程序里面造了个异常,从异常的底部开始一步步跟下去总可以发现根源的,我们知道shiro的入口是个过滤器shiroFilter,因此不用怀疑先从过滤器找起,首先是ApplicationFilterChain,看名字和包路径就知道这个不是shiro的实现类,大概查了一下知道它是tomcat实现的过滤器链.其采用了责任链的设计模式,我们在idea中打开这个类,在它的internalDoFilter方法上加断点.

image.png

由这行代码ApplicationFilterConfig filterConfig = this.filters[this.pos++];可知this.filters是一个ApplicationFilterConfig集合,这个集合存储了ApplicationFilterChain里面的所有过滤器,如下图.

QQ截图20180711145037.png

其中ApplicationFilterConfig是个filter容器,我们来看看它的定义:

org.apache.catalina.core.ApplicationFilterConfig

Implementation of a javax.servlet.FilterConfig useful in managing the filter instances instantiated when a web application is first started.

大致意思是说当web应用一开始启动时,会将工程中的所有的实例化的filter实例加载到此容器中.下面来看看容器启动后加载了哪些filter实例.

name=characterEncodingFilter

name=hiddenHttpMethodFilter

name=httpPutFormContentFilter

name=requestContextFilter

name=corsFilter

name=shiroFilter

name=Tomcat WebSocket(JSR356) Filter

其中characterEncodingFilter(用于处理编码问题)、hiddenHttpMethodFilter(隐藏HTTP函数)、httpPutFormContentFilter(form表单处理)、requestContextFilter(请求上下文)等是springboot自动添加的一些常用过滤器(注意这几个filter的完整限定类名为Ordered开头的,如OrderedCharacterEncodingFilter继承自CharacterEncodingFilter,里面多了个order属性,用于确定该filter的执行顺序).

corsFilter是我们后端统一使用cors来解决跨域问题的.

wsFilter这个filter应该是用来处理WebSocket的.

最后一个shiroFilter是我们要关注的重点,它是整个shiro框架的入口,注意它的filterClass是ShiroFilterFacotoryBean$SpringShiroFilter,看名字应该是spring生成的一个代理类了,先不管它是怎么生成的,继续往下看会发现上述列出来的过滤器中除了wsFilter和shiroFilter之外其他的filter都继承自org.springframework.web.filter.OncePerRequestFilter.而spring的OncePerRequestFilter是一个抽象过滤器类,其中定义的抽象方法doFilterInternal是由其子类来实现的,doFilter方法则是final的子类只能继承不能覆写.比如CharacterEncodingFilter继承了OncePerRequestFilter的doFilter方法且实现了doFilterInternal方法.

所以从上面的异常堆栈图中我们可以看出每次ApplicationFilterChain执行链中filter的doFilter方法时都会先执行它的父类OncePerRequestFilter的doFilter方法然后再执行这个filter实现的doFilterInternal方法,一直到shiro自己的OncePerRequestFilter(注意shiro自己实现的这个filter跟spring的不是同一个)为止.你一定会很奇怪为什么这里会执行shiro的OncePerRequestFilter,按道理springboot默认的filter和跨域的filter都执行过去了,那么接下来要执行的应该是shiro的入口shiroFilter才对.所以到目前为止我们一共有两个疑惑:

1.ShiroFilterFacotoryBean$SpringShiroFilter是怎么来的.

2.shiro的执行入口为什么是其内部实现的OncePerRequestFilter.

带着这些问题我们继续往下看,首先是项目中shiroFilter的配置.

ShiroFilterFactoryBean

@Bean

public ShiroFilterFactoryBean shiroFilter() {

ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();

// 必须设置 SecurityManager

shiroFilterFactoryBean.setSecurityManager(getDefaultWebSecurityManager());

........

return shiroFilterFactoryBean;

}

从上述配置可知使用了ShiroFilterFactoryBean来创建shiroFilter,所以重点在于ShiroFilterFactoryBean这个类.它的主要源代码如下:

public class ShiroFilterFactoryBean implements FactoryBean, BeanPostProcessor {

private static final transient Logger log = LoggerFactory.getLogger(ShiroFilterFactoryBean.class);

private SecurityManager securityManager;

private Map filters = new LinkedHashMap();

private Map filterChainDefinitionMap = new LinkedHashMap();

private String loginUrl;

private String successUrl;

private String unauthorizedUrl;

private AbstractShiroFilter instance;

......

}

从类的定义中可知ShiroFilterFactoryBean实现了接口FactoryBean和BeanPostProcessor.

BeanPostProcessor接口的作用是在Spring容器启动时,容器中所有的bean在初始化的前后都会调用这个接口的方法postProcessBeforeInitialization并在这个方法中判断当前的bean是否为Filter,若是则装载进Map集合filters中.

public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {

if (bean instanceof Filter) {

log.debug("Found filter chain candidate filter '{}'", beanName);

Filter filter = (Filter)bean;

this.applyGlobalPropertiesIfNecessary(filter);

this.getFilters().put(beanName, filter);

} else {

log.trace("Ignoring non-Filter bean '{}'", beanName);

}

return bean;

}

public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {

retu

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
在Spring Boot中使用Shiro进行拦截Ajax请求,可以使用Shiro提供的Filter来实现。 首先,需要创建一个自定义的ShiroFilter,并在Spring Boot的配置文件中进行配置: ```java @Configuration public class ShiroConfig { @Bean public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager) { ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean(); shiroFilterFactoryBean.setSecurityManager(securityManager); Map<String, Filter> filters = new HashMap<>(); filters.put("ajax", new AjaxFilter()); shiroFilterFactoryBean.setFilters(filters); Map<String, String> filterChainDefinitionMap = new LinkedHashMap<>(); filterChainDefinitionMap.put("/login", "anon"); filterChainDefinitionMap.put("/logout", "logout"); filterChainDefinitionMap.put("/**", "ajax,user"); shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap); return shiroFilterFactoryBean; } } ``` 在上面的代码中,我们创建了一个自定义的ShiroFilter,并设置了一个名为“ajax”的Filter。同时,我们也添加了一个FilterChain,将所有请求拦截,并使用“ajax”和“user”两个Filter进行处理。 接下来,我们需要创建一个名为“AjaxFilter”的Filter,用于处理Ajax请求。在该Filter中,我们可以判断请求是否为Ajax请求,如果是,则返回一个JSON对象,表示操作被拦截。 ```java public class AjaxFilter extends AccessControlFilter { @Override protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) throws Exception { HttpServletRequest httpRequest = WebUtils.toHttp(request); String requestedWith = httpRequest.getHeader("x-requested-with"); return requestedWith != null && requestedWith.equalsIgnoreCase("XMLHttpRequest"); } @Override protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception { HttpServletResponse httpResponse = WebUtils.toHttp(response); httpResponse.setCharacterEncoding("UTF-8"); httpResponse.setContentType("application/json;charset=UTF-8"); PrintWriter out = httpResponse.getWriter(); out.println("{\"success\":false,\"message\":\"您没有权限进行该操作!\"}"); out.flush(); out.close(); return false; } } ``` 在上面的代码中,我们重写了“isAccessAllowed”和“onAccessDenied”两个方法。在“isAccessAllowed”方法中,我们判断请求是否为Ajax请求。如果是,则返回true,表示允许访问;否则,返回false,表示禁止访问。 在“onAccessDenied”方法中,我们返回一个JSON对象,表示操作被拦截。在该方法中,我们首先设置HTTP响应的字符编码和内容类型。然后,我们获取PrintWriter对象,并使用该对象输出JSON字符串。最后,我们关闭PrintWriter对象,并返回false,表示禁止访问。 最后,我们需要将“AjaxFilter”添加到Spring Boot的配置文件中: ```properties shiro.filter.ajax=com.example.shiro.AjaxFilter ``` 这样,我们就可以在Spring Boot中使用Shiro拦截Ajax请求了。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值