前言
在使用 Shiro 的过程中, 遇到一个痛点, 就是对 restful 支持不太好, 也查了很多资料, 各种各样的方法都有, 要不就是功能不完整, 要不就是解释不清楚, 还有一些对原有功能的侵入性太强, 经过一番探索, 算是最简的配置下完成了需要的功能, 这里给大家分享下. 大家如果又更好的方案, 也可以在评论区留言, 互相探讨下.
虽然深入到了源码进行分析, 但过程并不复杂, 希望大家可以跟着我的思路捋顺了耐心看下去, 而不是看见源码贴就抵触.
分析
首先先回顾下 Shiro 的过滤器链, 一般我们都有如下配置:
- /login.html = anon
- /login = anon
- /users = perms[user:list]
- /** = authc
- 不太熟悉的朋友可以了解下这篇文章: Shiro 过滤器 http://www.zhaojun.im/shiro-07/ .
- 其中 /users 请求对应到 perms 过滤器, 对应的类: org.apache.shiro.web.filter.authz.PermissionsAuthorizationFilter, 其中的 onAccessDenied 方法是在没有权限时被调用的, 源码如下:
- 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 数据.
- 下面我们逐步来实现:
- 实现
- 过滤器链添加 http method
- 在我的项目中是从数据库获取的过滤器链, 所以有如下代码:
- 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], 解决办法是使用其他符号代替 ==.
- 修改 PathMatchingFilterChainResolver 的 getChain 方法
- 由于 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;
- }
- 修改过滤器的 pathsMatch 方法
- 同样新建一个类继承原有的 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 方法
同样是上一步的类, 重写 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; }
这里只改了 perms 过滤器, 对于其他过滤器也是同样的道理, 重写过滤器的 pathsMatch 和 onAccessDenied 方法, 并覆盖原有过滤器即可.
结语
基本的过程就是这些, 这是我在学习 Shiro 的过程中的一些见解, 希望可以帮助到大家. 具体应用的项目地址为: https://github.com/zhaojun1998/Shiro-Action , 功能在不断完善中, 代码可能有些粗糙, 还请见谅.
来源: https://juejin.im/entry/5bf04b14f265da615f76dd1e