Springboot整合SpringSecurity實(shí)現(xiàn)登錄認(rèn)證和鑒權(quán)全過(guò)程
一、Springboot整合SpringSecurity實(shí)現(xiàn)登錄認(rèn)證
1、springsecurity是通過(guò)在web一系列原生filter攔截器中增加自己的過(guò)濾器鏈來(lái)攔截web請(qǐng)求,然后請(qǐng)求會(huì)在經(jīng)過(guò)過(guò)濾器鏈的過(guò)程中會(huì)完成認(rèn)證與授權(quán),如果中間發(fā)現(xiàn)這條請(qǐng)求未認(rèn)證或者未授權(quán),會(huì)根據(jù)被保護(hù)API的權(quán)限去拋出異常,然后由異常處理器去處理這些異常。
2、SpringSecurity通過(guò)FilterChainProxy管理眾多SecurityFilterChain, 而FilterChainProxy則被DelegatingFilterProxy管理并被DelegatingFilterProxy放入web原生的過(guò)濾器鏈中;
每個(gè)SecurityFilterChain下則是具體的擁有攔截規(guī)則的filter,這些filter由SpringSecurity進(jìn)行代理操作,可以理解為他是"Security Filter",而不是原生的"Web Filter";
總結(jié)就是:
【DelegatingFilterProxy】——管理——>【FilterChainProxy】——管理——>【SecurityFilterChain】——管理——>【Security Filter】
3、springboot整合springsecurity,springboot會(huì)通過(guò)一系列xxxAutoConfiguration進(jìn)行自動(dòng)配置默認(rèn)的Spring Security的一系列底層組件,如WebSecurityConfigurerAdapter和一些默認(rèn)組件,有些"Security Filter"會(huì)自動(dòng)開(kāi)啟,有些則不會(huì);
整個(gè)認(rèn)證的過(guò)程其實(shí)一直在圍繞圖中過(guò)濾鏈的綠色部分,而動(dòng)態(tài)鑒權(quán)主要是圍繞其橙色部分;
Spring Security配置中有兩個(gè)叫formLogin和httpBasic的配置項(xiàng),這兩個(gè)配置項(xiàng)就分別對(duì)應(yīng)著圖中分的兩個(gè)過(guò)濾器
- formLogin對(duì)應(yīng)著你form表單認(rèn)證方式,即UsernamePasswordAuthenticationFilter。
- httpBasic對(duì)應(yīng)著B(niǎo)asic認(rèn)證方式,即BasicAuthenticationFilter。
4、我使用的就是UsernamePasswordAuthenticationFilter這個(gè)過(guò)濾器,springboot整合springsecurity時(shí)會(huì)自動(dòng)加載這個(gè)過(guò)濾器;
Spring Security 在自動(dòng)裝配后,會(huì)有默認(rèn)的攔截策略,未登陸的請(qǐng)求都會(huì)被攔截并跳轉(zhuǎn)到login登錄頁(yè),此時(shí)輸入賬號(hào)密碼登錄就會(huì)被這個(gè)UsernamePasswordAuthenticationFilter攔截,并驗(yàn)證賬號(hào)是否存在,密碼是否正確
進(jìn)入formlogin,發(fā)現(xiàn)有個(gè)**FormLoginConfigurer()**方法
進(jìn)入FormLoginConfigurer()方法,在這里用戶輸入賬號(hào)密碼就會(huì)被這個(gè)UsernamePasswordAuthenticationFilter攔截,并驗(yàn)證進(jìn)行認(rèn)證
發(fā)送登陸請(qǐng)求后,UsernamePasswordAuthenticationFilter會(huì)調(diào)用attemptAuthentication() 方法進(jìn)行認(rèn)證,失敗則拋出異常,成功則返回帶有用戶信息的Authentication對(duì)象
"Security Filter"中,認(rèn)證過(guò)程是由 " 主角 " AuthenticationManager(接口)去管理AuthenticationProvider(接口)去實(shí)現(xiàn)的,AuthenticationManager可以有多個(gè),他們?nèi)绻J(rèn)證失敗就會(huì)調(diào)用父親也就是全局的AuthenticationManager再去認(rèn)證看看,一般只用一個(gè)全局的.
一個(gè)AuthenticationProvider代表一種認(rèn)證方法,只要其中一個(gè)AuthenticationProvider認(rèn)證通過(guò)就算登陸成功,記住兩個(gè)主角的實(shí)現(xiàn)類ProviderManager和DaoAuthenticationProvider
回到attemptAuthentication()方法,調(diào)用拿到全局的AuthenticationMananger去執(zhí)行*authenticate()*方法,拿到ProviderManager中所有的AuthenticationProvider,交給他們?nèi)フJ(rèn)證
在遍歷provider這個(gè)過(guò)程中,調(diào)用了provider(DaoAuthenticationProvider)的authenticate方法,由provider去認(rèn)證,AuthenticationProvider的實(shí)現(xiàn)類DaoAuthenticationProvider繼承了AbstractUserDetailsAuthenticationProvider,所以自然也有父類方法的*authenticate()方法,因?yàn)闆](méi)有重寫(xiě)他,所以在源碼debug階段會(huì)進(jìn)入了他的父類的authenticate()方法,他的父類AbstractUserDetailsAuthenticationProvider實(shí)現(xiàn)了AuthenticationProvider
在provider(DaoAuthenticationProvider)的authenticate()方法中,先調(diào)用retrieveUser()通過(guò)用戶名來(lái)獲取我們存儲(chǔ)中是否有該用戶,如果有就封裝到UserDetail中,后面再拿請(qǐng)求中的密碼跟UserDetail用戶信息中的密碼進(jìn)行比較,如果沒(méi)有,密碼都不用比較了,因?yàn)橛脩舾静淮嬖?provider中有個(gè)叫UserDetailService的接口,通過(guò)用戶名可以獲取我們的用戶數(shù)據(jù)(他功能相當(dāng)于一個(gè)service層去調(diào)用dao層最終返回用戶數(shù)據(jù)),在自動(dòng)裝配中,默認(rèn)配了個(gè)基于內(nèi)存存儲(chǔ)的InMemoryUserDetailsManager,他是UserDetailService的實(shí)現(xiàn)類;
所以在使用springsecurity進(jìn)行登錄認(rèn)證的時(shí)候,除了要?jiǎng)?chuàng)建配置類進(jìn)行相關(guān)內(nèi)容的配置,還要?jiǎng)?chuàng)建UserDetailService的實(shí)現(xiàn)類用于到數(shù)據(jù)庫(kù)中查詢登錄認(rèn)證所需要的信息;
并且還要?jiǎng)?chuàng)建UserDetail的實(shí)現(xiàn)類用于封裝查詢出來(lái)的數(shù)據(jù),并把數(shù)據(jù)交給springsecurity框架拿去用于認(rèn)證
最后通過(guò)additionalAuthenticationChecks()方法進(jìn)行密碼比較
認(rèn)證失敗拋異常,認(rèn)證成功則將用戶詳細(xì)信息封裝進(jìn)Authentication返回
二、Springboot整合SpringSecurity實(shí)現(xiàn)鑒權(quán)
1、整個(gè)認(rèn)證的過(guò)程其實(shí)一直在圍繞圖中過(guò)濾鏈的綠色部分,而現(xiàn)在要說(shuō)的動(dòng)態(tài)鑒權(quán)主要是圍繞其橙色部分,也就是圖上標(biāo)的:FilterSecurityInterceptor
2、想知道怎么動(dòng)態(tài)鑒權(quán)首先我們要搞明白SpringSecurity的鑒權(quán)邏輯,從上圖中我們也可以看出:一個(gè)請(qǐng)求完成了認(rèn)證,且沒(méi)有拋出異常之后就會(huì)到達(dá)FilterSecurityInterceptor所負(fù)責(zé)的鑒權(quán)部分,也就是說(shuō)鑒權(quán)的入口就在FilterSecurityInterceptor。
先來(lái)看看FilterSecurityInterceptor的定義和主要方法:
public class FilterSecurityInterceptor extends AbstractSecurityInterceptor implements Filter { public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { FilterInvocation fi = new FilterInvocation(request, response, chain); invoke(fi); } }
上文代碼可以看出FilterSecurityInterceptor是抽象類AbstractSecurityInterceptor的一個(gè)實(shí)現(xiàn)類,這個(gè)AbstractSecurityInterceptor中預(yù)先寫(xiě)好了一段很重要的代碼(后面會(huì)說(shuō)到)。
FilterSecurityInterceptor的主要方法是doFilter方法,請(qǐng)求過(guò)來(lái)之后會(huì)執(zhí)行這個(gè)doFilter方法,F(xiàn)ilterSecurityInterceptor的doFilter方法出奇的簡(jiǎn)單,總共只有兩行:
- 第一行是創(chuàng)建了一個(gè)FilterInvocation對(duì)象,這個(gè)FilterInvocation對(duì)象你可以當(dāng)作它封裝了request,它的主要工作就是拿請(qǐng)求里面的信息,比如請(qǐng)求的URI和method
- 第二行就調(diào)用了自身的invoke方法,并將FilterInvocation對(duì)象傳入
所以我們主要邏輯肯定是在這個(gè)invoke方法里面了,我們來(lái)打開(kāi)看看:
public void invoke(FilterInvocation fi) throws IOException, ServletException { if ((fi.getRequest() != null) && (fi.getRequest().getAttribute(FILTER_APPLIED) != null) && observeOncePerRequest) { // filter already applied to this request and user wants us to observe // once-per-request handling, so don't re-do security checking fi.getChain().doFilter(fi.getRequest(), fi.getResponse()); } else { // first time this request being called, so perform security checking if (fi.getRequest() != null && observeOncePerRequest) { fi.getRequest().setAttribute(FILTER_APPLIED, Boolean.TRUE); } // 進(jìn)入鑒權(quán) InterceptorStatusToken token = super.beforeInvocation(fi); try { fi.getChain().doFilter(fi.getRequest(), fi.getResponse()); } finally { super.finallyInvocation(token); } super.afterInvocation(token, null); } }
invoke方法中只有一個(gè)if-else,一般都是不滿足if中的那三個(gè)條件的,然后執(zhí)行邏輯會(huì)來(lái)到else。
else的代碼也可以概括為兩部分:
- 調(diào)用了super.beforeInvocation(fi)。
- 調(diào)用完之后過(guò)濾器繼續(xù)往下走。
第二步可以不看,每個(gè)過(guò)濾器都有這么一步,所以我們主要看super.beforeInvocation(fi),前文我已經(jīng)說(shuō)過(guò), FilterSecurityInterceptor實(shí)現(xiàn)了抽象類AbstractSecurityInterceptor, 所以這個(gè)里super其實(shí)指的就是AbstractSecurityInterceptor, 那這段代碼其實(shí)調(diào)用了AbstractSecurityInterceptor.beforeInvocation(fi), 前文我說(shuō)過(guò)AbstractSecurityInterceptor中有一段很重要的代碼就是這一段, 那我們繼續(xù)來(lái)看這個(gè)beforeInvocation(fi)方法的源碼:
protected InterceptorStatusToken beforeInvocation(Object object) { Assert.notNull(object, "Object was null"); final boolean debug = logger.isDebugEnabled(); if (!getSecureObjectClass().isAssignableFrom(object.getClass())) { throw new IllegalArgumentException( "Security invocation attempted for object " + object.getClass().getName() + " but AbstractSecurityInterceptor only configured to support secure objects of type: " + getSecureObjectClass()); } Collection<ConfigAttribute> attributes = this.obtainSecurityMetadataSource() .getAttributes(object); Authentication authenticated = authenticateIfRequired(); try { // 鑒權(quán)需要調(diào)用的接口 this.accessDecisionManager.decide(authenticated, object, attributes); } catch (AccessDeniedException accessDeniedException) { publishEvent(new AuthorizationFailureEvent(object, attributes, authenticated, accessDeniedException)); throw accessDeniedException; } }
源碼較長(zhǎng),這段代碼大致可以分為三步:
拿到了一個(gè)Collection對(duì)象,這個(gè)對(duì)象是一個(gè)List,其實(shí)里面是通過(guò)我們?cè)谂渲梦募信渲玫倪^(guò)濾規(guī)則獲取到請(qǐng)求需要的角色權(quán)限。
public SecurityMetadataSource obtainSecurityMetadataSource() { return this.securityMetadataSource; }
拿到了Authentication,這里是調(diào)用authenticateIfRequired方法拿到了,其實(shí)里面是通過(guò)SecurityContextHolder拿到的
Authentication authenticated = authenticateIfRequired();
調(diào)用了accessDecisionManager.decide(authenticated, object, attributes),前兩步都是對(duì)decide方法做參數(shù)的準(zhǔn)備,第三步才是正式去到鑒權(quán)的邏輯,既然這里面才是真正鑒權(quán)的邏輯,那也就是說(shuō)鑒權(quán)其實(shí)是accessDecisionManager在做。
// 鑒權(quán)需要調(diào)用的接口 this.accessDecisionManager.decide(authenticated, object, attributes);
AccessDecisionManager是一個(gè)接口,它聲明了三個(gè)方法,除了第一個(gè)decide()鑒權(quán)方法以外,還有兩個(gè)是輔助性的方法,其作用都是甄別 decide方法中參數(shù)的有效性。
那既然是一個(gè)接口,上文中所調(diào)用的肯定是他的實(shí)現(xiàn)類了
它主要有三個(gè)實(shí)現(xiàn)類,分別代表了三種不同的鑒權(quán)邏輯:
- AffirmativeBased:一票通過(guò),只要有一票通過(guò)就算通過(guò),默認(rèn)是它。
- UnanimousBased:一票反對(duì),只要有一票反對(duì)就不能通過(guò)。
- ConsensusBased:少數(shù)票服從多數(shù)票。
這里的表述為什么要用票呢?因?yàn)樵趯?shí)現(xiàn)類里面采用了委托的形式,將請(qǐng)求委托給投票器,每個(gè)投票器拿著這個(gè)請(qǐng)求根據(jù)自身的邏輯來(lái)計(jì)算出能不能通過(guò)然后進(jìn)行投票,所以會(huì)有上面的表述。
也就是說(shuō)這三個(gè)實(shí)現(xiàn)類,其實(shí)還不是真正判斷請(qǐng)求能不能通過(guò)的類,真正判斷請(qǐng)求是否通過(guò)的是投票器,然后實(shí)現(xiàn)類把投票器的結(jié)果綜合起來(lái)來(lái)決定到底能不能通過(guò)。
剛剛已經(jīng)說(shuō)過(guò),實(shí)現(xiàn)類把投票器的結(jié)果綜合起來(lái)進(jìn)行決定,也就是說(shuō)投票器可以放入多個(gè),每個(gè)實(shí)現(xiàn)類里的投票器數(shù)量取決于構(gòu)造的時(shí)候放入了多少投票器,我們可以看看默認(rèn)的AffirmativeBased的源碼。
public class AffirmativeBased extends AbstractAccessDecisionManager { public AffirmativeBased(List<AccessDecisionVoter<?>> decisionVoters) { super(decisionVoters); } // 拿到所有的投票器,循環(huán)遍歷進(jìn)行投票 public void decide(Authentication authentication, Object object, Collection<ConfigAttribute> configAttributes) throws AccessDeniedException { int deny = 0; for (AccessDecisionVoter voter : getDecisionVoters()) { int result = voter.vote(authentication, object, configAttributes); if (logger.isDebugEnabled()) { logger.debug("Voter: " + voter + ", returned: " + result); } switch (result) { case AccessDecisionVoter.ACCESS_GRANTED: return; case AccessDecisionVoter.ACCESS_DENIED: deny++; break; default: break; } } if (deny > 0) { throw new AccessDeniedException(messages.getMessage( "AbstractAccessDecisionManager.accessDenied", "Access is denied")); } // To get this far, every AccessDecisionVoter abstained checkAllowIfAllAbstainDecisions(); } }
AffirmativeBased的構(gòu)造是傳入投票器List,其主要鑒權(quán)邏輯交給投票器去判斷,投票器返回不同的數(shù)字代表不同的結(jié)果,然后AffirmativeBased根據(jù)自身一票通過(guò)的策略決定放行還是拋出異常。
AffirmativeBased默認(rèn)傳入的構(gòu)造器只有一個(gè)->WebExpressionVoter,這個(gè)構(gòu)造器會(huì)根據(jù)你在配置文件中的配置進(jìn)行邏輯處理得出投票結(jié)果。
所以SpringSecurity默認(rèn)的鑒權(quán)邏輯就是根據(jù)配置文件中的配置進(jìn)行鑒權(quán),這是符合我們現(xiàn)有認(rèn)知的
3、總結(jié)一下就是:
FilterSecurityInterceptor執(zhí)行doFilter 方法創(chuàng)建FilterInvocation(req,resp,chain)對(duì)象;然后調(diào)用自身invoke方法,傳入對(duì)象
invoke方法中,在 chain().doFilter 前有 super.beforeInvocation(fi),調(diào)用 AbstractSecurityInterceptor 的beforeInvocation方法
beforeInvocation方法中
- 通過(guò)調(diào)用請(qǐng)求過(guò)濾接口obtainSecurityMetadataSource() 的getAttributes()方法獲取一個(gè)Collection對(duì)象,這個(gè)對(duì)象是一個(gè)list,里面封裝了請(qǐng)求所需要的角色權(quán)限
- 調(diào)用authenticateIfRequired方法拿到Authentication對(duì)象
- 調(diào)用了accessDecisionManager.decide(authenticated, object, attributes)正式進(jìn)行鑒權(quán)
4、在使用springsecurity進(jìn)行鑒權(quán)操作的時(shí)候,根據(jù)具體業(yè)務(wù)需求去自定義請(qǐng)求過(guò)濾器obtainSecurityMetadataSource()和投票器accessDecisionManager()
- 自定義請(qǐng)求過(guò)濾器,重寫(xiě)getAttributes()方法
@Override public Collection<ConfigAttribute> getAttributes(Object object) throws IllegalArgumentException { // 修改接口角色關(guān)系后重新加載 if (CollectionUtils.isEmpty(resourceRoleList)) { this.loadDataSource(); } //Spring Security 通過(guò)FilterInvocation對(duì)object進(jìn)行封裝,可以安全的拿到其HttpServletRequest 和 HttpServletResponse對(duì)象 FilterInvocation fi = (FilterInvocation) object; // 獲取用戶請(qǐng)求方式 String method = fi.getRequest().getMethod(); // 獲取用戶請(qǐng)求Url String url = fi.getRequest().getRequestURI(); //new一個(gè)工具類AntPathMatcher的實(shí)例化對(duì)象,把路徑匹配委托給AntPathMatcher實(shí)現(xiàn) AntPathMatcher antPathMatcher = new AntPathMatcher(); // 獲取接口角色信息,若為匿名接口則放行,若無(wú)對(duì)應(yīng)角色則禁止 for (ResourceRoleDTO resourceRoleDTO : resourceRoleList) { //判斷resourceRoleList中是否有和參數(shù)對(duì)象的URL和method完全相同的對(duì)象 if (antPathMatcher.match(resourceRoleDTO.getUrl(), url) && resourceRoleDTO.getRequestMethod().equals(method)) { //如果有對(duì)象匹配成功,則獲取該對(duì)象的角色列表RoleList List<String> roleList = resourceRoleDTO.getRoleList(); if (CollectionUtils.isEmpty(roleList)) { return SecurityConfig.createList("disable"); } return SecurityConfig.createList(roleList.toArray(new String[]{})); //rolelist集合轉(zhuǎn)換成String數(shù)組,通過(guò)SecurityConfig.createList(str)對(duì)結(jié)果進(jìn)行封裝,然后return } } return null; }
自定義投票器
@Override public void decide(Authentication authentication, Object o, Collection<ConfigAttribute> collection) throws AccessDeniedException, InsufficientAuthenticationException { // 獲取用戶權(quán)限列表 List<String> permissionList = authentication.getAuthorities() .stream() .map(GrantedAuthority::getAuthority) .collect(Collectors.toList()); for (ConfigAttribute item : collection) { //item.getAttribute()獲取當(dāng)前用戶訪問(wèn)資源所需要的權(quán)限 //如果用戶權(quán)限列表中包含該權(quán)限,則return,否則最后會(huì)提示沒(méi)有操作權(quán)限 if (permissionList.contains(item.getAttribute())) { return; } } throw new AccessDeniedException("沒(méi)有操作權(quán)限"); }
Config文件中,調(diào)用 postProcess 方法將自定義的請(qǐng)求過(guò)濾器和投票器注冊(cè)到 Spring 容器中去
http.authorizeRequests() .withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor>() { @Override public <O extends FilterSecurityInterceptor> O postProcess(O fsi) { fsi.setSecurityMetadataSource(securityMetadataSource()); //設(shè)置請(qǐng)求攔截規(guī)則 fsi.setAccessDecisionManager(accessDecisionManager()); //設(shè)置訪問(wèn)決策管理器,真正的鑒權(quán)操作在這里完成 return fsi; } })
總結(jié)
以上為個(gè)人經(jīng)驗(yàn),希望能給大家一個(gè)參考,也希望大家多多支持腳本之家。
- Spring Security 自定義短信登錄認(rèn)證的實(shí)現(xiàn)
- Springboot+Spring Security實(shí)現(xiàn)前后端分離登錄認(rèn)證及權(quán)限控制的示例代碼
- Java SpringSecurity+JWT實(shí)現(xiàn)登錄認(rèn)證
- SpringBoot security安全認(rèn)證登錄的實(shí)現(xiàn)方法
- SpringSecurity實(shí)現(xiàn)前后端分離登錄token認(rèn)證詳解
- springsecurity實(shí)現(xiàn)用戶登錄認(rèn)證快速使用示例代碼(前后端分離項(xiàng)目)
- Spring Security實(shí)現(xiàn)登錄認(rèn)證實(shí)戰(zhàn)教程
- SpringSecurity 自定義認(rèn)證登錄的項(xiàng)目實(shí)踐
- spring security登錄認(rèn)證授權(quán)的項(xiàng)目實(shí)踐
相關(guān)文章
Java中接口Set的特點(diǎn)及方法說(shuō)明
這篇文章主要介紹了Java中接口Set的特點(diǎn)及方法說(shuō)明,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-02-02IDEA實(shí)現(xiàn)序列化時(shí)如何自動(dòng)生成serialVersionUID的步驟
這篇文章主要介紹了IDEA實(shí)現(xiàn)序列化時(shí)如何自動(dòng)生成serialVersionUID的步驟,首先安裝GenerateSerialVersionUID插件,當(dāng)出現(xiàn)添加serialVersionUID選項(xiàng),選中則會(huì)自動(dòng)生成serialVersionUID,感興趣的朋友一起學(xué)習(xí)下吧2024-02-02IntelliJ IDEA安裝scala插件并創(chuàng)建scala工程的步驟詳細(xì)教程
這篇文章主要介紹了IntelliJ IDEA安裝scala插件并創(chuàng)建scala工程的步驟,本文通過(guò)圖文并茂的形式給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2020-07-07Java中Request請(qǐng)求轉(zhuǎn)發(fā)詳解
這篇文章主要介紹了Java中Request請(qǐng)求轉(zhuǎn)發(fā)詳解,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2020-07-07Java實(shí)現(xiàn)Excel導(dǎo)入導(dǎo)出操作詳解
在平常的辦公工作中,導(dǎo)入導(dǎo)出excel數(shù)據(jù)是常見(jiàn)的需求,今天就來(lái)看一下通過(guò)Java如何來(lái)實(shí)現(xiàn)這個(gè)功能,感興趣的朋友可以了解下2022-02-02