在使用 Shiro 的过程中, 遇到一个痛点, 就是对 restful 支持不太好, 也查了很多资料, 各种各样的方法都有, 要不就是功能不完整, 要不就是解释不清楚, 还有一些对原有功能的侵入性太强, 经过一番探索, 算是最简的配置下完成了需要的功能, 这里给大家分享下。大家如果又更好的方案, 也可以在评论区留言, 互相探讨下。
虽然深入到了源码进行分析, 但过程并不复杂, 希望大家可以跟着我的思路捋顺了耐心看下去, 而不是看见源码贴就抵触.
首先先回顾下 Shiro 的过滤器链, 一般我们都有如下配置:
/login.html = anon /login = anon /users = perms[user:list] /** = authc
不太熟悉的朋友可以了解下这篇文章:Shiro 过滤器。
其中 /users
请求对应到 perms
过滤器, 对应的类: org.apache.shiro.web.filter.authz.PermissionsAuthorizationFilter
其中有一个方法是在没有权限时被调用的, 源码如下:
protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws IOException { Subject subject = getSubject(request, response); // 如果未登录, 则重定向到配置的 loginUrl if (subject.getPrincipal() == null) { saveRequestAndRedirectToLogin(request, response); } else { // 如果当前用户没有权限, 则跳转到 UnauthorizedUrl // 如果没有配置 UnauthorizedUrl, 则返回 401 状态码. String unauthorizedUrl = getUnauthorizedUrl(); if (StringUtils.hasText(unauthorizedUrl)) { WebUtils.issueRedirect(request, response, unauthorizedUrl); } else { WebUtils.toHttp(response).sendError(HttpServletResponse.SC_UNAUTHORIZED); } } return false; }
我们可以在这里可以判断当前请求是否时 AJAX 请求, 如果是, 则不跳转到 logoUrl 或 UnauthorizedUrl 页面, 而是返回 JSON 数据.
还有一个方法是 pathsMatch, 是将当前请求的 url 与所有配置的 perms 过滤器链进行匹配, 是则进行权限检查, 不是则接着与下一个过滤器链进行匹配, 源码如下:
protected boolean pathsMatch(String path, ServletRequest request) { String requestURI = getPathWithinApplication(request); log.trace("Attempting to match pattern '{}' with current requestURI '{}'...", path, requestURI); return pathsMatch(path, requestURI); }
了解完这两个方法, 我来说说如何利用这两个方法来实现功能。
我们可以从配置的过滤器链来入手, 原先的配置如:
/users = perms[user:list]
我们可以改为 /user==GET
、 /user==POST
, ==
用来分隔, 后面的部分指 HTTP Method
.
使用这种方式还要注意一个方法, 即: org.apache.shiro.web.filter.mgt.PathMatchingFilterChainResolver
中的 getChain
方法, 用来获取当前请求的 URL 应该使用的过滤器, 源码如下:
public FilterChain getChain(ServletRequest request, ServletResponse response, FilterChain originalChain) { // 1. 判断有没有配置过滤器链, 没有一个过滤器都没有则直接返回 null FilterChainManager filterChainManager = getFilterChainManager(); if (!filterChainManager.hasChains()) { return null; } // 2. 获取当前请求的 URL String requestURI = getPathWithinApplication(request); // 3. 遍历所有的过滤器链 for (String pathPattern : filterChainManager.getChainNames()) { // 4. 判断当前请求的 URL 与过滤器链中的 URL 是否匹配. if (pathMatches(pathPattern, requestURI)) { if (log.isTraceEnabled()) { log.trace("Matched path pattern [" + pathPattern + "] for requestURI [" + requestURI + "]. " + "Utilizing corresponding filter chain..."); } // 5. 如果路径匹配, 则获取其实现类.(如 perms[user:list] 或 perms[user:delete] 都返回 perms) // 具体对 perms[user:list] 或 perms[user:delete] 的判断是在上面讲到的 PermissionsAuthorizationFilter 的 pathsMatch 方法中. return filterChainManager.proxy(originalChain, pathPattern); } } return null; }
这里大家需要注意, 第四步的判断, 我们已经将过滤器链, 也就是这里的 pathPattern
改为了 /xxx==GET
这种方式, 而请求的 URL 却仅包含 /xxx
, 那么这里的 pathMatches
方法是肯定无法匹配成功, 所以我们需要在第四步判断的时候, 只判断前面的 URL
部分。
整个过程如下:
在过滤器链上对 restful 请求配置需要的 HTTP Method
, 如: /user==DELETE
.
修改 PathMatchingFilterChainResolver
的 getChain
方法, 当前请求的 URL 与过滤器链匹配时, 过滤器只取 URL 部分进行判断.
修改过滤器的 pathsMatch
方法, 判断当前请求的 URL 与请求方式是否与过滤器链中配置的一致.
修改过滤器的 onAccessDenied
方法, 当访问被拒绝时, 根据普通请求和 AJAX
请求分别返回 HTML
和 JSON
数据.
下面我们逐步来实现:
在我的项目中是从数据库获取的过滤器链, 所以有如下代码:
public Map<String, String> getUrlPermsMap() { Map<String, String> filterChainDefinitionMap = new LinkedHashMap<>(); filterChainDefinitionMap.put("/favicon.ico", "anon"); filterChainDefinitionMap.put("/css/**", "anon"); filterChainDefinitionMap.put("/fonts/**", "anon"); filterChainDefinitionMap.put("/images/**", "anon"); filterChainDefinitionMap.put("/js/**", "anon"); filterChainDefinitionMap.put("/lib/**", "anon"); filterChainDefinitionMap.put("/login", "anon"); List<Menu> menus = selectAll(); for (Menu menu : menus) { String url = menu.getUrl(); if (!"".equals(menu.getMethod())) { url += ("==" + menu.getMethod()); } String perms = "perms[" + menu.getPerms() + "]"; filterChainDefinitionMap.put(url, perms); } filterChainDefinitionMap.put("/**", "authc"); return filterChainDefinitionMap; }
如: /xxx==GET = perms[user:list]
这里的 getUrl
、 getMethod
、 getPerms
分别对应 /xxx
、 GET
、 user:list
.
不过需要注意的是, 如果在 XML 里配置, 会被 Shiro 解析成 /xxx
和 =GET = perms[user:list]
, 解决办法是使用其他符号代替 ==
.
由于 Shiro 没有提供相应的接口, 且我们不能直接修改源码, 所以我们需要新建一个类继承 PathMatchingFilterChainResolver
并重写 getChain
方法, 然后替换掉 PathMatchingFilterChainResolver
即可.
首先继承并重写方法:
package im.zhaojun.shiro; import org.apache.shiro.web.filter.mgt.FilterChainManager; import org.apache.shiro.web.filter.mgt.PathMatchingFilterChainResolver; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.servlet.FilterChain; import javax.servlet.ServletRequest; import javax.servlet.ServletResponse; public class RestPathMatchingFilterChainResolver extends PathMatchingFilterChainResolver { private static final Logger log = LoggerFactory.getLogger(RestPathMatchingFilterChainResolver.class); @Override public FilterChain getChain(ServletRequest request, ServletResponse response, FilterChain originalChain) { FilterChainManager filterChainManager = getFilterChainManager(); if (!filterChainManager.hasChains()) { return null; } String requestURI = getPathWithinApplication(request); //the 'chain names' in this implementation are actually path patterns defined by the user. We just use them //as the chain name for the FilterChainManager's requirements for (String pathPattern : filterChainManager.getChainNames()) { String[] pathPatternArray = pathPattern.split("=="); // 只用过滤器链的 URL 部分与请求的 URL 进行匹配 if (pathMatches(pathPatternArray[0], requestURI)) { if (log.isTraceEnabled()) { log.trace("Matched path pattern [" + pathPattern + "] for requestURI [" + requestURI + "]. " + "Utilizing corresponding filter chain..."); } return filterChainManager.proxy(originalChain, pathPattern); } } return null; } }
然后替换掉 PathMatchingFilterChainResolver
, 它是在 ShiroFilterFactoryBean
的 createInstance
方法里初始化的.
所以同样的套路, 继承 ShiroFilterFactoryBean
并重写 createInstance
方法, 将 new PathMatchingFilterChainResolver();
改为 new RestPathMatchingFilterChainResolver();
即可.
代码如下:
package im.zhaojun.shiro; import org.apache.shiro.mgt.SecurityManager; import org.apache.shiro.spring.web.ShiroFilterFactoryBean; import org.apache.shiro.web.filter.mgt.FilterChainManager; import org.apache.shiro.web.filter.mgt.FilterChainResolver; import org.apache.shiro.web.filter.mgt.PathMatchingFilterChainResolver; import org.apache.shiro.web.mgt.WebSecurityManager; import org.apache.shiro.web.servlet.AbstractShiroFilter; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.BeanInitializationException; public class RestShiroFilterFactoryBean extends ShiroFilterFactoryBean { private static final Logger log = LoggerFactory.getLogger(RestShiroFilterFactoryBean.class); @Override protected AbstractShiroFilter createInstance() { log.debug("Creating Shiro Filter instance."); SecurityManager securityManager = getSecurityManager(); if (securityManager == null) { String msg = "SecurityManager property must be set."; throw new BeanInitializationException(msg); } if (!(securityManager instanceof WebSecurityManager)) { String msg = "The security manager does not implement the WebSecurityManager interface."; throw new BeanInitializationException(msg); } FilterChainManager manager = createFilterChainManager(); //Expose the constructed FilterChainManager by first wrapping it in a // FilterChainResolver implementation. The AbstractShiroFilter implementations // do not know about FilterChainManagers - only resolvers: PathMatchingFilterChainResolver chainResolver = new RestPathMatchingFilterChainResolver(); chainResolver.setFilterChainManager(manager); //Now create a concrete ShiroFilter instance and apply the acquired SecurityManager and built //FilterChainResolver. It doesn't matter that the instance is an anonymous inner class //here - we're just using it because it is a concrete AbstractShiroFilter instance that accepts //injection of the SecurityManager and FilterChainResolver: return new SpringShiroFilter((WebSecurityManager) securityManager, chainResolver); } private static final class SpringShiroFilter extends AbstractShiroFilter { protected SpringShiroFilter(WebSecurityManager webSecurityManager, FilterChainResolver resolver) { super(); if (webSecurityManager == null) { throw new IllegalArgumentException("WebSecurityManager property cannot be null."); } setSecurityManager(webSecurityManager); if (resolver != null) { setFilterChainResolver(resolver); } } } }
最后记得将 ShiroFilterFactoryBean
改为 RestShiroFilterFactoryBean
.
XML 方式:
<bean id="shiroFilter" class="im.zhaojun.shiro.RestShiroFilterFactoryBean"> <!-- 参数配置略 --> </bean>
Bean 方式:
@Bean public ShiroFilterFactoryBean shirFilter(SecurityManager securityManager) { ShiroFilterFactoryBean shiroFilterFactoryBean = new RestShiroFilterFactoryBean(); // 参数配置略 return shiroFilterFactoryBean; }
同样新建一个类继承原有的 PermissionsAuthorizationFilter
并重写 pathsMatch
方法:
package im.zhaojun.shiro.filter; import org.apache.shiro.subject.Subject; import org.apache.shiro.util.StringUtils; import org.apache.shiro.web.filter.authz.PermissionsAuthorizationFilter; import org.apache.shiro.web.util.WebUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.servlet.ServletRequest; import javax.servlet.ServletResponse; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.util.HashMap; import java.util.Map; /** * 修改后的 perms 过滤器, 添加对 AJAX 请求的支持. */ public class RestAuthorizationFilter extends PermissionsAuthorizationFilter { private static final Logger log = LoggerFactory .getLogger(RestAuthorizationFilter.class); @Override protected boolean pathsMatch(String path, ServletRequest request) { String requestURI = this.getPathWithinApplication(request); String[] strings = path.split("=="); if (strings.length <= 1) { // 普通的 URL, 正常处理 return this.pathsMatch(strings[0], requestURI); } else { // 获取当前请求的 http method. String httpMethod = WebUtils.toHttp(request).getMethod().toUpperCase(); // 匹配当前请求的 http method 与 过滤器链中的的是否一致 return httpMethod.equals(strings[1].toUpperCase()) && this.pathsMatch(strings[0], requestURI); } } }
同样是上一步的类, 重写 onAccessDenied
方法即可:
/** * 当没有权限被拦截时: * 如果是 AJAX 请求, 则返回 JSON 数据. * 如果是普通请求, 则跳转到配置 UnauthorizedUrl 页面. */ @Override protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws IOException { Subject subject = getSubject(request, response); // 如果未登录 if (subject.getPrincipal() == null) { // AJAX 请求返回 JSON if (im.zhaojun.util.WebUtils.isAjaxRequest(WebUtils.toHttp(request))) { if (log.isDebugEnabled()) { log.debug("用户: [{}] 请求 restful url : {}, 未登录被拦截.", subject.getPrincipal(), this.getPathWithinApplication(request)); } Map<String, Object> map = new HashMap<>(); map.put("code", -1); im.zhaojun.util.WebUtils.writeJson(map, response); } else { // 其他请求跳转到登陆页面 saveRequestAndRedirectToLogin(request, response); } } else { // 如果已登陆, 但没有权限 // 对于 AJAX 请求返回 JSON if (im.zhaojun.util.WebUtils.isAjaxRequest(WebUtils.toHttp(request))) { if (log.isDebugEnabled()) { log.debug("用户: [{}] 请求 restful url : {}, 无权限被拦截.", subject.getPrincipal(), this.getPathWithinApplication(request)); } Map<String, Object> map = new HashMap<>(); map.put("code", -2); map.put("msg", "没有权限啊!"); im.zhaojun.util.WebUtils.writeJson(map, response); } else { // 对于普通请求, 跳转到配置的 UnauthorizedUrl 页面. // 如果未设置 UnauthorizedUrl, 则返回 401 状态码 String unauthorizedUrl = getUnauthorizedUrl(); if (StringUtils.hasText(unauthorizedUrl)) { WebUtils.issueRedirect(request, response, unauthorizedUrl); } else { WebUtils.toHttp(response).sendError(HttpServletResponse.SC_UNAUTHORIZED); } } } return false; }
重写完 pathsMatch
和 onAccessDenied
方法后, 将这个类替换原有的 perms
过滤器的类:
XML 方式:
<bean id="shiroFilter" class="im.zhaojun.shiro.RestShiroFilterFactoryBean"> <!-- 参数配置略 --> <property name="filters"> <map> <entry key="perms" value-ref="restAuthorizationFilter"/> </map> </property> </bean> <bean id="restAuthorizationFilter" class="im.zhaojun.shiro.filter.RestAuthorizationFilter"/>
Bean 方式:
@Bean public ShiroFilterFactoryBean shirFilter(SecurityManager securityManager) { ShiroFilterFactoryBean shiroFilterFactoryBean = new RestShiroFilterFactoryBean(); Map<String, Filter> filters = shiroFilterFactoryBean.getFilters(); filters.put("perms", new RestAuthorizationFilter()); // 其他配置略 return shiroFilterFactoryBean; }
基本的过程就是这些, 这是我在学习 Shiro 的过程中的一些见解, 希望可以帮助到大家. 具体应用的项目地址为 : https://github.com/zhaojun1998/Shiro-Action , 功能在不断完善中, 代码可能有些粗糙, 还请见谅.