spring boot中shiro使用自定义注解屏蔽接口鉴权

20 篇文章 1 订阅
14 篇文章 1 订阅

2022-09-28更新:

经过后续使用发现,使用了这种办法后会导致spring boot事务失效!ApiContext会在shiro初始化过滤器的时候扫描全部的controller,这会导致controller类和controller依赖的service被提前实例化(正常情况下应该是spring实例化所有的BeanPostProcessor后再实例化业务类),然后你的启动日志里边可能会出现如下信息:

022-09-28 08:47:52.151  INFO 34092 --- [           main] trationDelegate$BeanPostProcessorChecker : Bean 'staffServiceImpl' of type [com.vanmesure.easyboot.program.service.impl.StaffServiceImpl] is not eligible for getting processed by all BeanPostProcessors (for example: not eligible for auto-proxying)

这样会导致此类不会被所有的BeanPostProcessor所代理,部分AOP会失效,这里我遇到的是@Transactional失效。最近实在太忙,后续会更新一下此问题的解决方案,以下内容现在只做参考,千万不要在生产环境中使用!

参考文章:解决:is not eligible for getting processed by all BeanPostProcessors_拾年一剑的博客-CSDN博客

传统做法

spring boot整合shiro后,如果某些接口需要屏蔽鉴权的话(比如登录)接口,我们一般会这么做:

@Bean(name = "shiroFilter")
public ShiroFilterFactoryBean shiroFilterFactoryBean(org.apache.shiro.mgt.SecurityManager securityManager) {
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
        shiroFilterFactoryBean.setSecurityManager(securityManager);
        Map<String, Filter> filters = shiroFilterFactoryBean.getFilters();
        filters.put("authc", new CorsAuthorizationFilter());
       
        Map<String, String> filterChainDefinitionMap = new LinkedHashMap<String, String>();
        /* -- 不去拦截的接口 --*/
        filterChainDefinitionMap.put("/statics/**", "anon");
        filterChainDefinitionMap.put("/auth/login", "anon");
        filterChainDefinitionMap.put("/auth/webLogin", "anon");
        filterChainDefinitionMap.put("/auth/loginPage", "anon");
        filterChainDefinitionMap.put("/projectTaskDefinition/list", "anon");
        /*需要拦截的接口*/
        filterChainDefinitionMap.put("/**", "authc");
        shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);

        shiroFilterFactoryBean.getFilters().put("authc", new CorsAuthorizationFilter());
        return shiroFilterFactoryBean;
    }

但是这样做起来不是很优雅,每次编写完新的不需要鉴权的方法后需要再回来改这个地方,所以我就想能不能通过接口上加注解的方式来标识此接口是否需要屏蔽鉴权。

使用自定义注解屏蔽接口鉴权

1.首先定义一个自定义注解AnnoApi 

/**
 * 将此注解加到controller的方法上,即可将方法对应的接口地址自动添加到白名单中
 * anno是anonymous的简称
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface AnnoApi {
}

因为此注解只起到标识作用,所以不需要成员属性。

2.在启动时获取全部的接口路径

为了实现这个功能我单独写了一个ApiContxt类来处理

import lombok.extern.slf4j.Slf4j;
import org.springframework.context.ApplicationContext;
import org.springframework.core.annotation.AnnotatedElementUtils;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.*;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;

@Component
@Slf4j
public class ApiContext {

    // 接口路径--方法 映射表
    private Map<String, Method> pathToMethodMap;

    private ApplicationContext applicationContext;

    /**
     * 扫描全部接口,并将其完整请求路径(不包含server.servlet.context-path)与方法的映射保存下来
     * 此方法默认所有打上@RequestMapping注解(或其派生注解)的类或方法都必须有至少一个访问路径,留空的话会抛出异常
     * @param applicationContext
     */
    public ApiContext(ApplicationContext applicationContext) {
        this.applicationContext = applicationContext;
        // 获取全部打了@RestController注解的类
        Map<String, Object> beansWithAnnotation = applicationContext.getBeansWithAnnotation(RestController.class);
        pathToMethodMap = new HashMap<>(beansWithAnnotation.size());
        for (Map.Entry<String, Object> entry : beansWithAnnotation.entrySet()) {
            Class<?> controller = entry.getValue().getClass();
            // 获取controller上的@RequestMapping注解
            RequestMapping controllerRequestMapping = controller.getAnnotation(RequestMapping.class);
            if (controllerRequestMapping != null) {
                Method[] controllerSubMethods = controller.getMethods();
                // 遍历controller下的所有方法,搜索所有加了@RequestMapping注解的方法
                for (Method method : controllerSubMethods) {
                    RequestMapping methodRequestionMapping = AnnotatedElementUtils.findMergedAnnotation(method, RequestMapping.class);
                    if (methodRequestionMapping == null) {
                        continue;
                    }
                    // 将controller的访问路径和method的访问路径进行拼接,并保存到一个map中
                    for (String controllerPath : controllerRequestMapping.value()) {
                        if (!controllerPath.startsWith("/")) {
                            controllerPath = "/" + controllerPath;
                        }
                        for (String methodPath : methodRequestionMapping.value()) {
                            if (!methodPath.startsWith("/")) {
                                methodPath = "/" + methodPath;
                            }
                            // API完整的请求路径
                            String fullPath = controllerPath + methodPath;
                            pathToMethodMap.put(fullPath, method);
                        }
                    }
                }
            }
        }
    }

    public Map<String, Method> getPathToMethodMap() {
        return pathToMethodMap;
    }
}

大致意思就是将所有接口路径与对应方法的映射保存下来,供其他类使用。

细心的小伙伴可能会发现一个小问题,就是我在获取方法路径时取得是@RequestMapping注解的值,那么如果我的方法使用的是@PostMapping或@GetMappbing的话该怎么处理?

实际上上述代码是可以获取@GetMapping @PostMapping @PutMapping @DeleteMapping @PatchMapping @RequestMapping这些注解的路径值的,在本文最后会简单说一下里边的原理,现在暂时认为是可以全部获取的就可以了。

3.配置shiro时使用ApiContext提取的接口信息配合自定义注解来动态添加白名单

    @Bean(name = "shiroFilter")
    public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager,
                                                         ApiContext apiContext) {
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
        shiroFilterFactoryBean.setSecurityManager(securityManager);


        Map<String, String> filterChainDefinitionMap = new LinkedHashMap<String, String>();
        shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
        // 无需拦截的接口
        for (Map.Entry<String, Method> entry : apiContext.getPathToMethodMap().entrySet()) {
            // 判断方法上是否存在AnnoApi注解
            AnnoApi annoApi = entry.getValue().getAnnotation(AnnoApi.class);
            if (annoApi != null) {
                // 接口地址是比较敏感的信息,将这个打印到日志里边不是很安全,可以考虑关掉
                log.info("添加白名单接口:" + entry.getKey());
                filterChainDefinitionMap.put(entry.getKey(), "anon");
            }
        }
        // 需要拦截的接口
        filterChainDefinitionMap.put("/**", "authc");
        // 使用自定义拦截器
        shiroFilterFactoryBean.getFilters().put("authc", new CorsAuthorizationFilter());
        return shiroFilterFactoryBean;
    }

循环遍历ApiContext提供的所有接口路径,然后判断每一个方法上是否有@AnnoApi标识,如果有的话就将其路径添加到白名单中,大功告成!

4.使用

使用方法很简单,在需要屏蔽鉴权的方法上添加上注解就可以了

拓展内容:关于spring中的派生注解

在上边第二步时我提到过这样一个问题

细心的小伙伴可能会发现一个小问题,就是我在获取方法路径时取得是@RequestMapping注解的值,那么如果我的方法使用的是@PostMapping或@GetMappbing的话该怎么处理?

为什么我获取@RequestMapping可以捎带着将@PostMapping或@GetMappbing一并获取了呢?

简单解释就是@PostMapping,@GetMapping等注解是@RequestMapping的派生注解。我们随便点开@PostMapping方法可以看到,这个注解上边被打上了@RequestMapping注解。派生注解是spring框架中的一个概念,与java本身无关,这里我们不去探究其原理(主要是我也不会),只知道@PostMapping与@RequestMapping实际上是有关联的就可以了。这个地方为了好理解也可以简单的认为@RequestMapping相当于是@PostMapping的父注解.

而spring框架中的工具类AnnotatedElementUtils中的findMergedAnnotation()可以获取一个方法上的某个特定注解,如果没有的话该方法会尝试查找已存在注解的父注解是否满足。所以下边这行代码在打了@PostMapping注解的方法上也是有效的了。

RequestMapping methodRequestionMapping = AnnotatedElementUtils.findMergedAnnotation(method, RequestMapping.class);

另外AnnotatedElementUtils.findMergedAnnotation()还对@AliasFor注解做了处理,简单说就是你的方法上打上了@PostMapping("add"),但是你拿到的父注解@RequestMapping中是没有“add”这个值的,@PostMapping的源码中通过@AliasFor注解指定了映射关系(如下图), 

然后AnnotatedElementUtils.findMergedAnnotation()方法对其进行了处理,所以我们才能在@RequestMapping中取到路径值。 

spring中还有个类似的工具方法,AnnotationUtils.findAnnotation(),也能获取父注解,但是这个方法并没有对@AliasFor注解做处理,所以拿到的父注解是没有属性值的。

```省略部分代码

String methodRequestPath = null; // 方法路径
RequestMapping requestMapping = method.getAnnotation(RequestMapping.class);
if (requestMapping != null) {
    methodRequestPath = mapping.value()[0];
}

if (methodRequestPath == null) {
    GetMapping mapping = method.getAnnotation(GetMapping.class);
    if (mapping != null) {
        methodRequestPath = mapping.value()[0];
    }
}
if (methodRequestPath == null) {
    PostMapping mapping = method.getAnnotation(PostMapping.class);
    if (mapping != null) {
        methodRequestPath = mapping.value()[0];
    }
}
if (methodRequestPath == null) {
    PutMapping mapping = method.getAnnotation(PutMapping.class);
    if (mapping != null) {
        methodRequestPath = mapping.value()[0];
    }
}
if (methodRequestPath == null) {
    DeleteMapping mapping = method.getAnnotation(DeleteMapping.class);
    if (mapping != null) {
        methodRequestPath = mapping.value()[0];
    }
}
```省略部分代码

这个获取方法路径的方法将每个注解都判断了一下,然后取出路径,显然这个代码看着很难受。

后边修改为通过 AnnotatedElementUtils.findMergedAnnotation(method, RequestMapping.class);获取后就优雅多了。

首先需要明确的是,java中的注解是不可以继承的,所以spring中的派生注解应该是对注解继承的一个拓展。当然以上提到的注解继承、父注解等概念都是为了方便理解胡诌出来的,笔者并不保证其准确性。

  • 5
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 5
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值