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


相关文章
|
7月前
|
安全 NoSQL Java
SpringBoot接口安全:限流、重放攻击、签名机制分析
本文介绍如何在Spring Boot中实现API安全机制,涵盖签名验证、防重放攻击和限流三大核心。通过自定义注解与拦截器,结合Redis,构建轻量级、可扩展的安全防护方案,适用于B2B接口与系统集成。
1055 3
|
7月前
|
XML 安全 Java
使用 Spring 的 @Aspect 和 @Pointcut 注解简化面向方面的编程 (AOP)
面向方面编程(AOP)通过分离横切关注点,如日志、安全和事务,提升代码模块化与可维护性。Spring 提供了对 AOP 的强大支持,核心注解 `@Aspect` 和 `@Pointcut` 使得定义切面与切入点变得简洁直观。`@Aspect` 标记切面类,集中处理通用逻辑;`@Pointcut` 则通过表达式定义通知的应用位置,提高代码可读性与复用性。二者结合,使开发者能清晰划分业务逻辑与辅助功能,简化维护并提升系统灵活性。Spring AOP 借助代理机制实现运行时织入,与 Spring 容器无缝集成,支持依赖注入与声明式配置,是构建清晰、高内聚应用的理想选择。
724 0
|
7月前
|
缓存 监控 Java
SpringBoot @Scheduled 注解详解
使用`@Scheduled`注解实现方法周期性执行,支持固定间隔、延迟或Cron表达式触发,基于Spring Task,适用于日志清理、数据同步等定时任务场景。需启用`@EnableScheduling`,注意线程阻塞与分布式重复问题,推荐结合`@Async`异步处理,提升任务调度效率。
1143 128
|
7月前
|
Java 测试技术 API
将 Spring 的 @Embedded 和 @Embeddable 注解与 JPA 结合使用的指南
Spring的@Embedded和@Embeddable注解简化了JPA中复杂对象的管理,允许将对象直接嵌入实体,减少冗余表与连接操作,提升数据库设计效率。本文详解其用法、优势及适用场景。
414 126
|
6月前
|
XML Java 应用服务中间件
【SpringBoot(一)】Spring的认知、容器功能讲解与自动装配原理的入门,带你熟悉Springboot中基本的注解使用
SpringBoot专栏开篇第一章,讲述认识SpringBoot、Bean容器功能的讲解、自动装配原理的入门,还有其他常用的Springboot注解!如果想要了解SpringBoot,那么就进来看看吧!
683 2
|
7月前
|
Java 测试技术 数据库
使用Spring的@Retryable注解进行自动重试
在现代软件开发中,容错性和弹性至关重要。Spring框架提供的`@Retryable`注解为处理瞬时故障提供了一种声明式、可配置的重试机制,使开发者能够以简洁的方式增强应用的自我恢复能力。本文深入解析了`@Retryable`的使用方法及其参数配置,并结合`@Recover`实现失败回退策略,帮助构建更健壮、可靠的应用程序。
876 1
使用Spring的@Retryable注解进行自动重试
|
7月前
|
XML Java 数据格式
常用SpringBoot注解汇总与用法说明
这些注解的使用和组合是Spring Boot快速开发和微服务实现的基础,通过它们,可以有效地指导Spring容器进行类发现、自动装配、配置、代理和管理等核心功能。开发者应当根据项目实际需求,运用这些注解来优化代码结构和服务逻辑。
503 12
|
7月前
|
传感器 Java 数据库
探索Spring Boot的@Conditional注解的上下文配置
Spring Boot 的 `@Conditional` 注解可根据不同条件动态控制 Bean 的加载,提升应用的灵活性与可配置性。本文深入解析其用法与优势,并结合实例展示如何通过自定义条件类实现环境适配的智能配置。
393 0
探索Spring Boot的@Conditional注解的上下文配置
|
7月前
|
智能设计 Java 测试技术
Spring中最大化@Lazy注解,实现资源高效利用
本文深入探讨了 Spring 框架中的 `@Lazy` 注解,介绍了其在资源管理和性能优化中的作用。通过延迟初始化 Bean,`@Lazy` 可显著提升应用启动速度,合理利用系统资源,并增强对 Bean 生命周期的控制。文章还分析了 `@Lazy` 的工作机制、使用场景、最佳实践以及常见陷阱与解决方案,帮助开发者更高效地构建可扩展、高性能的 Spring 应用程序。
306 0
Spring中最大化@Lazy注解,实现资源高效利用
|
7月前
|
安全 IDE Java
Spring 的@FieldDefaults和@Data:Lombok 注解以实现更简洁的代码
本文介绍了如何在 Spring 应用程序中使用 Project Lombok 的 `@Data` 和 `@FieldDefaults` 注解来减少样板代码,提升代码可读性和可维护性,并探讨了其适用场景与限制。
275 0
Spring 的@FieldDefaults和@Data:Lombok 注解以实现更简洁的代码

热门文章

最新文章