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

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

传统做法

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中的派生注解应该是对注解继承的一个拓展。当然以上提到的注解继承、父注解等概念都是为了方便理解胡诌出来的,笔者并不保证其准确性。


相关文章
|
3月前
|
安全 Java Apache
微服务——SpringBoot使用归纳——Spring Boot中集成 Shiro——Shiro 身份和权限认证
本文介绍了 Apache Shiro 的身份认证与权限认证机制。在身份认证部分,分析了 Shiro 的认证流程,包括应用程序调用 `Subject.login(token)` 方法、SecurityManager 接管认证以及通过 Realm 进行具体的安全验证。权限认证部分阐述了权限(permission)、角色(role)和用户(user)三者的关系,其中用户可拥有多个角色,角色则对应不同的权限组合,例如普通用户仅能查看或添加信息,而管理员可执行所有操作。
135 0
|
3月前
|
安全 Java 数据安全/隐私保护
微服务——SpringBoot使用归纳——Spring Boot中集成 Shiro——Shiro 三大核心组件
本课程介绍如何在Spring Boot中集成Shiro框架,主要讲解Shiro的认证与授权功能。Shiro是一个简单易用的Java安全框架,用于认证、授权、加密和会话管理等。其核心组件包括Subject(认证主体)、SecurityManager(安全管理员)和Realm(域)。Subject负责身份认证,包含Principals(身份)和Credentials(凭证);SecurityManager是架构核心,协调内部组件运作;Realm则是连接Shiro与应用数据的桥梁,用于访问用户账户及权限信息。通过学习,您将掌握Shiro的基本原理及其在项目中的应用。
135 0
|
6月前
|
XML Java 数据格式
探索Spring之利剑:ApplicationContext接口
本文深入介绍了Spring框架中的核心接口ApplicationContext,解释了其作为应用容器的功能,包括事件发布、国际化支持等,并通过基于XML和注解的配置示例展示了如何使用ApplicationContext管理Bean实例。
269 6
|
5月前
|
缓存 Java 数据库
SpringBoot缓存注解使用
Spring Boot 提供了一套方便的缓存注解,用于简化缓存管理。通过 `@Cacheable`、`@CachePut`、`@CacheEvict` 和 `@Caching` 等注解,开发者可以轻松地实现方法级别的缓存操作,从而提升应用的性能和响应速度。合理使用这些注解可以大大减少数据库的访问频率,优化系统性能。
263 89
|
3月前
|
Java 测试技术 Spring
SpringBoot+@Async注解一起用,速度提升
本文介绍了异步调用在高并发Web应用性能优化中的重要性,对比了同步与异步调用的区别。同步调用按顺序执行,每一步需等待上一步完成;而异步调用无需等待,可提升效率。通过Spring Boot示例,使用@Async注解实现异步任务,并借助Future对象处理异步回调,有效减少程序运行时间。
|
4月前
|
Java Maven 开发者
编写SpringBoot的自定义starter包
通过本文的介绍,我们详细讲解了如何创建一个Spring Boot自定义Starter包,包括自动配置类、配置属性类、`spring.factories`文件的创建和配置。通过自定义Starter,可以有效地复用公共配置和组件,提高开发效率。希望本文能帮助您更好地理解和应用Spring Boot自定义Starter,在实际项目中灵活使用这一强大的功能。
144 17
|
4月前
|
监控 Java Spring
SpringBoot:SpringBoot通过注解监测Controller接口
本文详细介绍了如何通过Spring Boot注解监测Controller接口,包括自定义注解、AOP切面的创建和使用以及具体的示例代码。通过这种方式,可以方便地在Controller方法执行前后添加日志记录、性能监控和异常处理逻辑,而无需修改方法本身的代码。这种方法不仅提高了代码的可维护性,还增强了系统的监控能力。希望本文能帮助您更好地理解和应用Spring Boot中的注解监测技术。
121 16
|
3月前
|
JSON Java 数据格式
微服务——SpringBoot使用归纳——Spring Boot中的全局异常处理——拦截自定义异常
本文介绍了在实际项目中如何拦截自定义异常。首先,通过定义异常信息枚举类 `BusinessMsgEnum`,统一管理业务异常的代码和消息。接着,创建自定义业务异常类 `BusinessErrorException`,并在其构造方法中传入枚举类以实现异常信息的封装。最后,利用 `GlobalExceptionHandler` 拦截并处理自定义异常,返回标准的 JSON 响应格式。文章还提供了示例代码和测试方法,展示了全局异常处理在 Spring Boot 项目中的应用价值。
76 0
|
6月前
|
设计模式 XML Java
【23种设计模式·全精解析 | 自定义Spring框架篇】Spring核心源码分析+自定义Spring的IOC功能,依赖注入功能
本文详细介绍了Spring框架的核心功能,并通过手写自定义Spring框架的方式,深入理解了Spring的IOC(控制反转)和DI(依赖注入)功能,并且学会实际运用设计模式到真实开发中。
【23种设计模式·全精解析 | 自定义Spring框架篇】Spring核心源码分析+自定义Spring的IOC功能,依赖注入功能