springboot中shiro使用自定義注解屏蔽接口鑒權(quán)實(shí)現(xiàn)
傳統(tǒng)做法
spring boot整合shiro后,如果某些接口需要屏蔽鑒權(quán)的話(huà)(比如登錄)接口,我們一般會(huì)這么做:
@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;
}但是這樣做起來(lái)不是很優(yōu)雅,每次編寫(xiě)完新的不需要鑒權(quán)的方法后需要再回來(lái)改這個(gè)地方,所以我就想能不能通過(guò)接口上加注解的方式來(lái)標(biāo)識(shí)此接口是否需要屏蔽鑒權(quán)。
使用自定義注解屏蔽接口鑒權(quán)
1.首先定義一個(gè)自定義注解AnnoApi
/**
* 將此注解加到controller的方法上,即可將方法對(duì)應(yīng)的接口地址自動(dòng)添加到白名單中
* anno是anonymous的簡(jiǎn)稱(chēng)
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface AnnoApi {
}因?yàn)榇俗⒔庵黄鸬綐?biāo)識(shí)作用,所以不需要成員屬性。
2.在啟動(dòng)時(shí)獲取全部的接口路徑
為了實(shí)現(xiàn)這個(gè)功能我單獨(dú)寫(xiě)了一個(gè)ApiContxt類(lèi)來(lái)處理
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;
/**
* 掃描全部接口,并將其完整請(qǐng)求路徑(不包含server.servlet.context-path)與方法的映射保存下來(lái)
* 此方法默認(rèn)所有打上@RequestMapping注解(或其派生注解)的類(lèi)或方法都必須有至少一個(gè)訪問(wèn)路徑,留空的話(huà)會(huì)拋出異常
* @param applicationContext
*/
public ApiContext(ApplicationContext applicationContext) {
this.applicationContext = applicationContext;
// 獲取全部打了@RestController注解的類(lèi)
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的訪問(wèn)路徑和method的訪問(wèn)路徑進(jìn)行拼接,并保存到一個(gè)map中
for (String controllerPath : controllerRequestMapping.value()) {
if (!controllerPath.startsWith("/")) {
controllerPath = "/" + controllerPath;
}
for (String methodPath : methodRequestionMapping.value()) {
if (!methodPath.startsWith("/")) {
methodPath = "/" + methodPath;
}
// API完整的請(qǐng)求路徑
String fullPath = controllerPath + methodPath;
pathToMethodMap.put(fullPath, method);
}
}
}
}
}
}
public Map<String, Method> getPathToMethodMap() {
return pathToMethodMap;
}
}大致意思就是將所有接口路徑與對(duì)應(yīng)方法的映射保存下來(lái),供其他類(lèi)使用。
細(xì)心的小伙伴可能會(huì)發(fā)現(xiàn)一個(gè)小問(wèn)題,就是我在獲取方法路徑時(shí)取得是@RequestMapping注解的值,那么如果我的方法使用的是@PostMapping或@GetMappbing的話(huà)該怎么處理?
實(shí)際上上述代碼是可以獲取@GetMapping @PostMapping @PutMapping @DeleteMapping @PatchMapping @RequestMapping這些注解的路徑值的,在本文最后會(huì)簡(jiǎn)單說(shuō)一下里邊的原理,現(xiàn)在暫時(shí)認(rèn)為是可以全部獲取的就可以了。
3.配置shiro時(shí)使用ApiContext提取的接口信息配合自定義注解來(lái)動(dòng)態(tài)添加白名單
@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);
// 無(wú)需攔截的接口
for (Map.Entry<String, Method> entry : apiContext.getPathToMethodMap().entrySet()) {
// 判斷方法上是否存在AnnoApi注解
AnnoApi annoApi = entry.getValue().getAnnotation(AnnoApi.class);
if (annoApi != null) {
// 接口地址是比較敏感的信息,將這個(gè)打印到日志里邊不是很安全,可以考慮關(guān)掉
log.info("添加白名單接口:" + entry.getKey());
filterChainDefinitionMap.put(entry.getKey(), "anon");
}
}
// 需要攔截的接口
filterChainDefinitionMap.put("/**", "authc");
// 使用自定義攔截器
shiroFilterFactoryBean.getFilters().put("authc", new CorsAuthorizationFilter());
return shiroFilterFactoryBean;
}循環(huán)遍歷ApiContext提供的所有接口路徑,然后判斷每一個(gè)方法上是否有@AnnoApi標(biāo)識(shí),如果有的話(huà)就將其路徑添加到白名單中,大功告成!
4.使用
使用方法很簡(jiǎn)單,在需要屏蔽鑒權(quán)的方法上添加上注解就可以了

拓展內(nèi)容:關(guān)于spring中的派生注解
在上邊第二步時(shí)我提到過(guò)這樣一個(gè)問(wèn)題
細(xì)心的小伙伴可能會(huì)發(fā)現(xiàn)一個(gè)小問(wèn)題,就是我在獲取方法路徑時(shí)取得是@RequestMapping注解的值,那么如果我的方法使用的是@PostMapping或@GetMappbing的話(huà)該怎么處理?
為什么我獲取@RequestMapping可以捎帶著將@PostMapping或@GetMappbing一并獲取了呢?
簡(jiǎn)單解釋就是@PostMapping,@GetMapping等注解是@RequestMapping的派生注解。我們隨便點(diǎn)開(kāi)@PostMapping方法可以看到,這個(gè)注解上邊被打上了@RequestMapping注解。派生注解是spring框架中的一個(gè)概念,與java本身無(wú)關(guān),這里我們不去探究其原理(主要是我也不會(huì)),只知道@PostMapping與@RequestMapping實(shí)際上是有關(guān)聯(lián)的就可以了。這個(gè)地方為了好理解也可以簡(jiǎn)單的認(rèn)為@RequestMapping相當(dāng)于是@PostMapping的父注解.

而spring框架中的工具類(lèi)AnnotatedElementUtils中的findMergedAnnotation()可以獲取一個(gè)方法上的某個(gè)特定注解,如果沒(méi)有的話(huà)該方法會(huì)嘗試查找已存在注解的父注解是否滿(mǎn)足。所以下邊這行代碼在打了@PostMapping注解的方法上也是有效的了。
RequestMapping methodRequestionMapping = AnnotatedElementUtils.findMergedAnnotation(method, RequestMapping.class);
另外AnnotatedElementUtils.findMergedAnnotation()還對(duì)@AliasFor注解做了處理,簡(jiǎn)單說(shuō)就是你的方法上打上了@PostMapping("add"),但是你拿到的父注解@RequestMapping中是沒(méi)有“add”這個(gè)值的,@PostMapping的源碼中通過(guò)@AliasFor注解指定了映射關(guān)系(如下圖),

然后AnnotatedElementUtils.findMergedAnnotation()方法對(duì)其進(jìn)行了處理,所以我們才能在@RequestMapping中取到路徑值。
spring中還有個(gè)類(lèi)似的工具方法,AnnotationUtils.findAnnotation(),也能獲取父注解,但是這個(gè)方法并沒(méi)有對(duì)@AliasFor注解做處理,所以拿到的父注解是沒(méi)有屬性值的。
```省略部分代碼
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];
}
}
```省略部分代碼這個(gè)獲取方法路徑的方法將每個(gè)注解都判斷了一下,然后取出路徑,顯然這個(gè)代碼看著很難受。
后邊修改為通過(guò) AnnotatedElementUtils.findMergedAnnotation(method, RequestMapping.class);獲取后就優(yōu)雅多了。
首先需要明確的是,java中的注解是不可以繼承的,所以spring中的派生注解應(yīng)該是對(duì)注解繼承的一個(gè)拓展。當(dāng)然以上提到的注解繼承、父注解等概念都是為了方便理解胡謅出來(lái)的,筆者并不保證其準(zhǔn)確性。
到此這篇關(guān)于springboot中shiro使用自定義注解屏蔽接口鑒權(quán)實(shí)現(xiàn)的文章就介紹到這了,更多相關(guān)springboot shiro自定義注解屏蔽接口鑒權(quán)內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
screw?Maven插件方式運(yùn)行時(shí)在編譯打包時(shí)跳過(guò)執(zhí)行的問(wèn)題解決方法
這篇文章主要介紹了screw?Maven插件方式運(yùn)行時(shí)在編譯打包時(shí)跳過(guò)執(zhí)行的問(wèn)題解決方法,本文給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2023-03-03
springboot項(xiàng)目docker分層構(gòu)建的配置方式
在使用dockerfile構(gòu)建springboot項(xiàng)目時(shí),速度較慢,用時(shí)比較長(zhǎng),為了加快構(gòu)建docker鏡像的速度,采用分層構(gòu)建的方式,這篇文章主要介紹了springboot項(xiàng)目docker分層構(gòu)建,需要的朋友可以參考下2024-03-03
開(kāi)放封閉原則_動(dòng)力節(jié)點(diǎn)Java學(xué)院整理
這篇文章主要介紹了開(kāi)放封閉原則,開(kāi)放-封閉原則是面向?qū)ο笤O(shè)計(jì)的核心所在,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-08-08
Spring Boot中Elasticsearch的連接配置原理與使用詳解
在Spring Boot中,我們可以通過(guò)Elasticsearch實(shí)現(xiàn)對(duì)數(shù)據(jù)的搜索和分析,本文將介紹Spring Boot中Elasticsearch的連接配置、原理和使用方法,感興趣的可以了解一下2023-09-09
使用MUI框架構(gòu)建App請(qǐng)求http接口實(shí)例代碼
下面小編就為大家分享一篇使用MUI框架構(gòu)建App請(qǐng)求http接口實(shí)例代碼,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2018-01-01
淺談StringBuilder類(lèi)的capacity()方法和length()方法的一些小坑
這篇文章主要介紹了StringBuilder類(lèi)的capacity()方法和length()方法的一些小坑,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-07-07
Java實(shí)現(xiàn)文件名倒序排序的技術(shù)指南
在實(shí)際開(kāi)發(fā)過(guò)程中,我們經(jīng)常需要對(duì)文件進(jìn)行操作和處理,一個(gè)常見(jiàn)的需求是按文件名倒序排列文件列表,以便于文件的管理和查找,本文將介紹如何在Java中實(shí)現(xiàn)文件名倒序排序,并提供詳細(xì)的代碼案例,需要的朋友可以參考下2024-08-08

