在實(shí)際項(xiàng)目中使用到了springsecurity作為安全框架,我們會遇到需要放行一些接口,使其能匿名訪問的業(yè)務(wù)需求。但是每當(dāng)需要當(dāng)需要放行時,都需要在security的配置類中進(jìn)行修改,感覺非常的不優(yōu)雅。
例如這樣:
所以想通過自定義一個注解,來進(jìn)行接口匿名訪問。在實(shí)現(xiàn)需求前,我們先了解一下security的兩種方行思路。
第一種就是在configure(WebSecurity web)
方法中配置放行,像下面這樣:
@Overridepublicvoidconfigure(WebSecurityweb)throwsException{
web.ignoring().antMatchers("/css/**","/js/**","/index.html","/img/**","/fonts/**","/favicon.ico","/verifyCode");
}
第二種方式是在configure(HttpSecurity http)
方法中進(jìn)行配置:
@Overrideprotectedvoidconfigure(HttpSecurityhttpSecurity)throwsException
{
httpSecurity
.authorizeRequests()
.antMatchers("/hello").permitAll()
.anyRequest().authenticated()
}
兩種方式最大的區(qū)別在于,第一種方式是不走 Spring Security 過濾器鏈,而第二種方式走 Spring Security 過濾器鏈,在過濾器鏈中,給請求放行。
在我們使用 Spring Security 的時候,有的資源可以使用第一種方式額外放行,不需要驗(yàn)證,例如前端頁面的靜態(tài)資源,就可以按照第一種方式配置放行。
有的資源放行,則必須使用第二種方式,例如登錄接口。大家知道,登錄接口也是必須要暴露出來的,不需要登錄就能訪問到的,但是我們卻不能將登錄接口用第一種方式暴露出來,登錄請求必須要走 Spring Security 過濾器鏈,因?yàn)樵谶@個過程中,還有其他事情要做,具體的登錄流程想了解的可以自行百度。
了解完了security的兩種放行策略后,我們開始實(shí)現(xiàn)
首先創(chuàng)建一個自定義注解
@Target({ElementType.METHOD})//注解放置的目標(biāo)位置,METHOD是可注解在方法級別上@Retention(RetentionPolicy.RUNTIME)//注解在哪個階段執(zhí)行@Documented//生成文檔public@interfaceIgnoreAuth{
}
這里說明一下,@Target({ElementType.METHOD})
我的實(shí)現(xiàn)方式,注解只能標(biāo)記在帶有@RequestMapping
注解的方法上。具體為什么下面的實(shí)現(xiàn)方式看完就懂了。
接下來創(chuàng)建一個security的配置類SecurityConfig并繼承WebSecurityConfigurerAdapter
@EnableGlobalMethodSecurity(prePostEnabled=true,securedEnabled=true)
publicclassSecurityConfigextendsWebSecurityConfigurerAdapter
{
@Autowired
privateRequestMappingHandlerMappingrequestMappingHandlerMapping;
/**
*@description:使用這種方式放行的接口,不走SpringSecurity過濾器鏈,
*無法通過SecurityContextHolder獲取到登錄用戶信息的,
*因?yàn)樗婚_始沒經(jīng)過 SecurityContextPersistenceFilter 過濾器鏈。
*@dateTime:2021/7/1910:22
*/
@Override
publicvoidconfigure(WebSecurityweb)throwsException{
WebSecurityand=web.ignoring().and();
MaphandlerMethods=requestMappingHandlerMapping.getHandlerMethods();
handlerMethods.forEach((info,method)->{
//帶IgnoreAuth注解的方法直接放行
if(StringUtils.isNotNull(method.getMethodAnnotation(IgnoreAuth.class))){
//根據(jù)請求類型做不同的處理
info.getMethodsCondition().getMethods().forEach(requestMethod->{
switch(requestMethod){
caseGET:
//getPatternsCondition得到請求url數(shù)組,遍歷處理
info.getPatternsCondition().getPatterns().forEach(pattern->{
//放行
and.ignoring().antMatchers(HttpMethod.GET,pattern);
});
break;
casePOST:
info.getPatternsCondition().getPatterns().forEach(pattern->{
and.ignoring().antMatchers(HttpMethod.POST,pattern);
});
break;
caseDELETE:
info.getPatternsCondition().getPatterns().forEach(pattern->{
and.ignoring().antMatchers(HttpMethod.DELETE,pattern);
});
break;
casePUT:
info.getPatternsCondition().getPatterns().forEach(pattern->{
and.ignoring().antMatchers(HttpMethod.PUT,pattern);
});
break;
default:
break;
}
});
}
});
}
}
在這里使用Spring為我們提供的RequestMappingHandlerMapping
類,我們可以通過requestMappingHandlerMapping.getHandlerMethods();
獲取到所有的RequestMappingInfo
信息。
以下是源碼部分,可不看,看了可以加深理解
這里簡單說一下RequestMappingHandlerMapping
的工作流程,便于理解。我們通過翻看源碼
繼承關(guān)系如上圖所示。
AbstractHandlerMethodMapping
實(shí)現(xiàn)了InitializingBean
接口
publicinterfaceInitializingBean{
voidafterPropertiesSet()throwsException;
}
AbstractHandlerMethodMapping
類中通過afterPropertiesSet
方法調(diào)用initHandlerMethods
進(jìn)行初始化
publicvoidafterPropertiesSet(){
this.initHandlerMethods();
}
protectedvoidinitHandlerMethods(){
String[]var1=this.getCandidateBeanNames();
intvar2=var1.length;
for(intvar3=0;var3if(!beanName.startsWith("scopedTarget.")){
this.processCandidateBean(beanName);
}
}
this.handlerMethodsInitialized(this.getHandlerMethods());
}
再調(diào)用processCandidateBean
方法:
protectedvoidprocessCandidateBean(StringbeanName){
ClassbeanType=null;
try{
beanType=this.obtainApplicationContext().getType(beanName);
}catch(Throwablevar4){
if(this.logger.isTraceEnabled()){
this.logger.trace("Couldnotresolvetypeforbean'"+beanName+"'",var4);
}
}
if(beanType!=null&&this.isHandler(beanType)){
this.detectHandlerMethods(beanName);
}
}
通過調(diào)用方法中的isHandler方法是不是requestHandler
方法,可以看到源碼是通過RequestMapping
,Controller 注解進(jìn)行判斷的。
protectedbooleanisHandler(Class>beanType){
returnAnnotatedElementUtils.hasAnnotation(beanType,Controller.class)||AnnotatedElementUtils.hasAnnotation(beanType,RequestMapping.class);
}
判斷通過后,調(diào)用detectHandlerMethods
方法將handler注冊到HandlerMethod的緩存中。
protectedvoiddetectHandlerMethods(Objecthandler){
Class>handlerType=handlerinstanceofString?this.obtainApplicationContext().getType((String)handler):handler.getClass();
if(handlerType!=null){
Class>userType=ClassUtils.getUserClass(handlerType);
Mapmethods=MethodIntrospector.selectMethods(userType,(method)->{
try{
returnthis.getMappingForMethod(method,userType);
}catch(Throwablevar4){
thrownewIllegalStateException("Invalidmappingonhandlerclass["+userType.getName()+"]:"+method,var4);
}
});
if(this.logger.isTraceEnabled()){
this.logger.trace(this.formatMappings(userType,methods));
}
methods.forEach((method,mapping)->{
MethodinvocableMethod=AopUtils.selectInvocableMethod(method,userType);
this.registerHandlerMethod(handler,invocableMethod,mapping);
});
}
}
通過registerHandlerMethod
方法將handler放到private final Map
map中。
而requestMappingHandlerMapping.getHandlerMethods()
方法就是獲取所有的HandlerMapping。
publicMapgetHandlerMethods() {
this.mappingRegistry.acquireReadLock();
Mapvar1;
try{
var1=Collections.unmodifiableMap(this.mappingRegistry.getMappings());
}finally{
this.mappingRegistry.releaseReadLock();
}
returnvar1;
}
最后就是對map進(jìn)行遍歷,判斷是否帶有IgnoreAuth.class
注解,然后針對不同的請求方式進(jìn)行放行。
handlerMethods.forEach((info,method)->{
//帶IgnoreAuth注解的方法直接放行
if(StringUtils.isNotNull(method.getMethodAnnotation(IgnoreAuth.class))){
//根據(jù)請求類型做不同的處理
info.getMethodsCondition().getMethods().forEach(requestMethod->{
switch(requestMethod){
caseGET:
//getPatternsCondition得到請求url數(shù)組,遍歷處理
info.getPatternsCondition().getPatterns().forEach(pattern->{
//放行
and.ignoring().antMatchers(HttpMethod.GET,pattern);
});
break;
casePOST:
info.getPatternsCondition().getPatterns().forEach(pattern->{
and.ignoring().antMatchers(HttpMethod.POST,pattern);
});
break;
caseDELETE:
info.getPatternsCondition().getPatterns().forEach(pattern->{
and.ignoring().antMatchers(HttpMethod.DELETE,pattern);
});
break;
casePUT:
info.getPatternsCondition().getPatterns().forEach(pattern->{
and.ignoring().antMatchers(HttpMethod.PUT,pattern);
});
break;
default:
break;
}
});
}
});
看到這里就能理解我最開始的強(qiáng)調(diào)的需標(biāo)記在帶有@RequestMapping
注解的方法上。我這里使用到的是configure(WebSecurity web)
的放行方式。它是不走security的過濾鏈,是無法通過SecurityContextHolder
獲取到登錄用戶信息的,這點(diǎn)問題是需要注意的。
審核編輯:郭婷
-
接口
+關(guān)注
關(guān)注
33文章
8596瀏覽量
151145
原文標(biāo)題:如何利用自定義注解放行 Spring Security 項(xiàng)目的接口
文章出處:【微信號:AndroidPush,微信公眾號:Android編程精選】歡迎添加關(guān)注!文章轉(zhuǎn)載請注明出處。
發(fā)布評論請先 登錄
相關(guān)推薦
評論