Shiro 方法上有权限注解的时候才鉴权

Shiro 方法上有权限注解的时候才鉴权

最近参与的新项目 REST API 需要集成 Shiro 权限框架,在集成过程中发现 Shiro 好像只能通过对过滤器指定 Url Path Pattern 的方式针对 URL 进行权限校验。在指定 Filter 的 URL 后,Fitler 会对所有的 URL 进行处理并不会根据需要对 URL 进行略过,但是 API 中并不是所有的接口都需要进行鉴权。

针对这个需求,翻遍了自带的默认过滤器都没有对应合适的 Filter 进行处理,只能自己实现 Filter,好在 Shiro 在扩展方面做得相当不错。本文主要讲述实现一个根据是否有权限注解来进行是否鉴权的方法,在没有 Shiro 权限注解的方法或者类上不进行鉴权。

实现

过滤器

在实现这个 Filter 的时候发现,Shiro 的过滤器是在 Servlet 的 Filter 之上实现的,不像其他的一样是基于 Spring Interceptor 来进行拦截过滤,可以取到相应处理的 Controller 方法上的注解,进而进行判断处理。

同时这个 Filter 需要交给 Shiro 进行管理而不能声明为 Spring Bean 进行管理,这样的后果就是无法通过简单的注入取得 Controller 和一些 Service。 所以在实现过滤器之前先实现 Spring Context Holder,用于存储 Spring 的上下文环境,方便在需要的时候在任意地方取得 Bean。

SpringContextHolder

实现这个类的方法其实很简单,只需要实现 ApplicationContextAware 接口,Spring 会自动将 applicationContext 注入 setApplicationContext 方法,样例代码如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
package tech.imoe.blog.shiro.commons;

import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component;

/**
* SpringContextHolder
*
* @author jakes at 2018/4/25 10:14 PM
*/
@Component
public class SpringContextHolder implements ApplicationContextAware {
private static ApplicationContext context;

@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
context = applicationContext;
}

/**
* 获取 spring bean
* @param clazz bean 类
* @param <T> 类型
* @return 返回 BEAN 结果
*/
public static <T> T getBean(Class<T> clazz) {
return context.getBean(clazz);
}
}

ShiroService

将 Shiro 需要使用的特殊功能抽离出来,单独放在一个 Service 类里可以方便扩展和维护。ShiroService 的第一个方法是 requireAuthentication,用于判断是否需要进行鉴权。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// ShiroService.java

package tech.imoe.blog.shiro.service;

import javax.servlet.ServletRequest;

/**
* ShiroService
*
* @author jakes at 2018/4/25 7:38 PM
*/
public interface ShiroService {
/**
* 判断是否需要鉴权
*
* @param request 请求
* @return true 需要,false 不需要
*/
boolean requireAuthentication(ServletRequest request);
}

对应的实现类如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
package tech.imoe.blog.shiro.service.impl;

import tech.imoe.blog.shiro.commons.SpringContextHolder;
import tech.imoe.blog.shiro.service.ShiroService;
import com.google.common.collect.Lists;
import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.authz.annotation.RequiresAuthentication;
import org.apache.shiro.authz.annotation.RequiresPermissions;
import org.apache.shiro.authz.annotation.RequiresRoles;
import org.apache.shiro.authz.annotation.RequiresUser;
import org.springframework.stereotype.Service;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerExecutionChain;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;

import javax.annotation.PostConstruct;
import javax.servlet.ServletRequest;
import javax.servlet.http.HttpServletRequest;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.util.List;

/**
* ShiroServiceImpl
*
* @author jakes at 2018/4/25 7:40 PM
*/
@Service
@Slf4j
public class ShiroServiceImpl implements ShiroService {

private RequestMappingHandlerMapping handlerMapping;

private static final List<Class<? extends Annotation>> SHIRO_ANNOTATION =
Lists.newArrayList(
RequiresAuthentication.class,
RequiresPermissions.class,
RequiresRoles.class,
RequiresUser.class
);


/**
* 判断是否需要鉴权
*
* @param request 请求
* @return true 需要,false 不需要
*/
@Override
public boolean requireAuthentication(ServletRequest request) {
if (handlerMapping == null)
handlerMapping = SpringContextHolder.getBean(RequestMappingHandlerMapping.class);

HttpServletRequest httpServletRequest = (HttpServletRequest) request;
try {
HandlerExecutionChain handlerChain = handlerMapping.getHandler(httpServletRequest);

// 判断方法上是否存在权限注解
HandlerMethod handler = (HandlerMethod) handlerChain.getHandler();

for (Class<? extends Annotation> clazz : SHIRO_ANNOTATION) {
if (handler.hasMethodAnnotation(clazz))
return true;
}

// 判断类上是否存在权限注解
Class<?> methodClazz = handler.getMethod().getDeclaringClass();

for (Class<? extends Annotation> _clazz : SHIRO_ANNOTATION) {
if (methodClazz.isAnnotationPresent(_clazz))
return true;
}
} catch (Exception e) {
log.warn("get request handler error!", e);
}

// 默认过滤的或报错的不需要鉴权
return false;
}
}

Spring 的 URL Mapping 默认是由 RequestMappingHandlerMapping 处理的,所有的 @RequestMapping 注解的 Controller 方法都被维护在里面。所以如果需要从 URI 反推获取到该请求响应的方法,需要取得 RequestMappingHandlerMapping 的实例对象,并通过 getHandler 方法取得 HandlerMethod,这样才可以进一步进行处理。

Spring 的 RequestMappingHandlerMapping 通过实现 Ordered 接口,将优先级调为最低级以实现在所有 Bean 初始化完毕后才初始化它。上面的 ShiroService 实现类显然优先级要高于 RequestMappingHandlerMapping,所以不能通过 @Autowired 方式进行注入,要等全部初始化完后在调用时再从 Spring 上下文中取出才能取到正确的值。这种情况就算用懒加载的方式(@Lazy)也是有点问题的。

WebTokenFilter

Shiro Filter 中只需要在 isAccessAllowed 方法中实现我们的判断逻辑,然后在 onAccessDenied 中实现 subject.login() 的逻辑即可达到我们的目标。前面提到 Filter 并不是在 Spring 容器中的 Bean,因此需要手动从 holder 中取出。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
package tech.imoe.blog.shiro.filter;

import tech.imoe.blog.shiro.commons.SpringContextHolder;
import tech.imoe.blog.shiro.service.ShiroService;
import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.web.filter.AccessControlFilter;

import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;

/**
* WebTokenFilter
*
* @author jakes at 2018/4/25 8:13 PM
*/
@Slf4j
public class WebTokenFilter extends AccessControlFilter {
private ShiroService shiroService;

@Override
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) throws Exception {
if (tokenService == null)
tokenService = SpringContextHolder.getBean(ShiroService.class);

return !shiroService.requireAuthentication(request);
}

@Override
protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
// 检查 token
}
}

总结

Shiro 的扩展性很强但是由于实现方式导致有些功能实现起来会比较绕,需要花点时间研究。这次集成 Spring 的 RequestMappingHandlerMapping 坑了我不少,卡在这里查了好久原因,报的异常也是奇怪,写下这篇当折腾笔记。

作者

Jakes Lee

发布于

2018-04-27

更新于

2021-11-18

许可协议

评论