javascript
SpringSecurity鉴权流程分析 附源码注释,xdm,一起来看看吧
紙上得來終覺淺,絕知此事要躬行。
閱讀本文:
如需簡單使用👉:SpringBoot集成SpringSecurity做安全框架、附源碼
你能收獲:🛴
一、前言:
xdm,我還是沒有學會寫小故事😭,我只可以在這里請你們喝可樂🥤,請 xdm 賞個一鍵三連😁。
xdm,不知道你們在使用SpringSecurity安全框架的時候,有沒有想過 debug 一步一步看它是如何實現判斷是否可以訪問的?
如下:
@PreAuthorize("hasRole('ROLE_ADMIN')") @RequestMapping("/role/admin1") String admin() {return "role: ROLE_ADMIN"; }為什么我們寫上這個注解可以了呢?如何進行判斷的呢?
前面寫過一次👨?💻 SpringSecurity 登錄流程分析,寫那篇文章是為了寫👩?💻 SpringSecurity 實現多種登錄方式做鋪墊。
那么這次寫這個文章的原因呢?
在掘金看到了掘友的 和耳朵 寫的 SpringSecurity 動態鑒權流程分析,才發覺用注解其實也不是非常好的事情,直接固定在項目,無法做到動態的更改,是個要不得的事情(捂臉),之前只考慮到這么寫蠻好的,看完文章才恍然大悟。這兩天也準備實現一下Security的動態鑒權的小demo。
xdm,一定要記得,紙上得來終覺淺,絕知此事要躬行,尤其是一路 debug 的文章,親身踩坑。
對于一門技術,會使用是說明我們對它已經有了一個簡單了解,把脈絡、細節都掌握清楚,我們才能更好的使用。
接下來就讓👨?🏫來帶大家一起看看吧。
二、流程圖:
下圖是在百度找的一張關于 Security 原理圖
我接下來畫的流程圖是基于用戶已經登錄的狀態下的畫的。
整個認證的過程其實一直在圍繞圖中過濾鏈的綠色部分,而我們今天要說的鑒權主要是圍繞其橙色部分,也就是圖上標的:FilterSecurityInterceptor。
這也就是我流程圖的開始,如下圖:
上圖如有不妥之處,請大家批正,在此鄭重感謝。
關于上圖的粗略解釋,后文再一一道來:
1、登錄后,用戶訪問一個需要權限的接口,經過一連串過濾器,到達 FilterSecurityInterceptor, FilterSecurityInterceptor 的invoke()方法執行具體攔截行為,具體是 beforeInvocation、finallyInvocation、afterInvocation 這三個方法,這三個方法是定義在父類 AbstractSecurityInterceptor 中。
2、調用 AbstractSecurityInterceptor 的 beforeInvocation 方法。AbstractSecurityInterceptor將確保安全攔截器的正確啟動配置。它還將實現對安全對象調用的正確處理,即:
3、經過千辛萬苦后,到達MethodSecurityInterceptor,由它再次重新調用起 AbstractSecurityInterceptor.beforeInvocation(mi) 方法,來進行權限的驗證
- 鑒權的時候,投票者會換成 PreInvocationAuthorizationAdviceVoter
進入正題前先放張圖片緩一緩:
當烏云和白云相遇時,而我與你卻已經分離。
👨?💻
三、前半部分
前半部分作用是在檢測用戶的狀態,并非就是執行鑒權,不過兩次都十分相近。關于方法上注解的檢測是在后半部分。
1)入口:FilterSecurityInterceptor
第一步:FilterSecurityInterceptor void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
//過濾器鏈實際調用的方法。 簡單地委托給invoke(FilterInvocation)方法。 @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)throws IOException, ServletException {invoke(new FilterInvocation(request, response, chain)); }接著看 void invoke(FilterInvocation filterInvocation)
public void invoke(FilterInvocation filterInvocation) throws IOException, ServletException {if (isApplied(filterInvocation) && this.observeOncePerRequest) {//過濾器已應用于此請求,用戶希望我們觀察每個請求處理一次,因此不要重新進行安全檢查filterInvocation.getChain().doFilter(filterInvocation.getRequest(), filterInvocation.getResponse());return;}// 第一次調用這個請求,所以執行安全檢查if (filterInvocation.getRequest() != null && this.observeOncePerRequest) {filterInvocation.getRequest().setAttribute(FILTER_APPLIED, Boolean.TRUE);}//調用 beforeInvocation(filterInvocation) 方法 跟著這個方法往下看InterceptorStatusToken token = super.beforeInvocation(filterInvocation) ;try {//每個過濾器都有這么一步 filterInvocation.getChain().doFilter(filterInvocation.getRequest(), filterInvocation.getResponse());}finally {//在安全對象調用完成后清理AbstractSecurityInterceptor的工作。//無論安全對象調用是否成功返回,都應該在安全對象調用之后和 afterInvocation 之前調用此方法(即它應該在 finally 塊中完成)。super.finallyInvocation(token);}//當調用afterInvocation(InterceptorStatusToken,Object)時,AbstractSecurityInterceptor不會采取進一步的操作。super.afterInvocation(token, null); }2)進入:AbstractSecurityInterceptor
授權檢查 beforeInvocation() 方法
第二步:super.beforeInvocation(filterInvocation); 一些打印信息被精簡了,太長不適合閱讀
protected InterceptorStatusToken beforeInvocation(Object object) {//檢查操作Assert.notNull(object, "Object was null");if (!getSecureObjectClass().isAssignableFrom(object.getClass())) {//....}//這里獲取的信息看下圖示1 ://object 就是調用處傳過來的參數 FilterInvocation filterInvocation,它本身其實就是 HttpServletRequest 和 HttpServletResponse 的增強//object :filter invocation [GET /role/admin1] "//然后我們獲取到的就是受保護調用的列表 Collection<ConfigAttribute> attributes = this.obtainSecurityMetadataSource().getAttributes(object);if (CollectionUtils.isEmpty(attributes)) {//...return null; // no further work post-invocation}//在 SecurityContext 中未找到身份驗證對象,會發事件拋異常if (SecurityContextHolder.getContext().getAuthentication() == null) {credentialsNotFound(this.messages.getMessage("AbstractSecurityInterceptor.authenticationNotFound","An Authentication object was not found in the SecurityContext"), object, attributes);}//在這里拿到了 Authentication 對象登錄的信息 ,后文會簡單說是如何拿到的Authentication authenticated = authenticateIfRequired();if (this.logger.isTraceEnabled()) {this.logger.trace(LogMessage.format("Authorizing %s with attributes %s", object, attributes));}// Attempt authorization : 嘗試授權 這步本文重點,用我的話來說,這就是鑒權的入口 重點關注,下文繼續attemptAuthorization(object, attributes, authenticated);//...// Attempt to run as a different userAuthentication runAs = this.runAsManager.buildRunAs(authenticated, object, attributes);if (runAs != null) {//...}// 無后續動作return new InterceptorStatusToken(SecurityContextHolder.getContext(), false, attributes, object);}關于 Collection< ConfigAttribute > attributes = this.obtainSecurityMetadataSource().getAttributes(object);這段代碼。
第一次訪問這里的時候,FilterSecurityInterceptor是從 SecurityMetadataSource 的子類 DefaultFilterInvocationSecurityMetadataSource獲取到當前的是這樣的數據。它和我們第二次來執行這里有很大的區別。這里的表達式是 authenticated,翻譯過來就是認證過的。
在后文會進行比較的。
我們接著往下看:Authentication authenticateIfRequired() 獲取身份信息
//如果Authentication.isAuthenticated()返回 false 或屬性alwaysReauthenticate已設置為 true, //則檢查當前的身份驗證令牌并將其傳遞給 AuthenticationManager進行身份驗證 private Authentication authenticateIfRequired() {Authentication authentication = SecurityContextHolder.getContext().getAuthentication();if (authentication.isAuthenticated() && !this.alwaysReauthenticate) {return authentication;}authentication = this.authenticationManager.authenticate(authentication);SecurityContextHolder.getContext().setAuthentication(authentication);return authentication; }3)嘗試授權: attemptAuthorization()
第三步:嘗試授權: attemptAuthorization()
private void attemptAuthorization(Object object, Collection<ConfigAttribute> attributes,Authentication authenticated) {try {//接著套娃 我們去看 AccessDecisionManager 下的 decide() 方法this.accessDecisionManager.decide(authenticated, object, attributes);}catch (AccessDeniedException ex) {if (this.logger.isTraceEnabled()) {this.logger.trace(LogMessage.format("Failed to authorize %s with attributes %s using %s", object,attributes, this.accessDecisionManager));}else if (this.logger.isDebugEnabled()) {this.logger.debug(LogMessage.format("Failed to authorize %s with attributes %s", object, attributes));}publishEvent(new AuthorizationFailureEvent(object, attributes, authenticated, ex));throw ex;} }AccessDecisionManager 決策器說明:
this.accessDecisionManager 其實是個接口。我們一起看看它的源碼
public interface AccessDecisionManager {/** 為傳遞的參數解析訪問控制決策。 參數: 身份驗證 - 調用方法的調用者(非空) object – 被調用的安全對象 configAttributes – 與被調用的安全對象關聯的配置屬性*/void decide(Authentication authentication, Object object, Collection<ConfigAttribute> configAttributes)throws AccessDeniedException, InsufficientAuthenticationException;// 下面這兩個方法主要起輔助作用的。大都執行檢查操作boolean supports(ConfigAttribute attribute);boolean supports(Class<?> clazz);}我們先看看這個接口結構,之后再看它的實現類內部鑒權機制是如何執行的,需要獲取那些信息,又是如何判斷它是否可以通過的。
我們可以看到這個 AccessDecisionManager 接口,接口下有一個抽象類,然后再有了三個實現類。
他們分別代表不同的機制。
一起看看默認用的 AffirmativeBased:
public class AffirmativeBased extends AbstractAccessDecisionManager {public AffirmativeBased(List<AccessDecisionVoter<?>> decisionVoters) {super(decisionVoters);}/**這個具體的實現只是輪詢所有配置的AccessDecisionVoter并在任何AccessDecisionVoter投贊成票時授予訪問權限。 僅當存在拒絕投票且沒有贊成票時才拒絕訪問。 如果每個AccessDecisionVoter放棄投票,則決策將基于isAllowIfAllAbstainDecisions()屬性(默認為 false)。*/@Override@SuppressWarnings({ "rawtypes", "unchecked" })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);switch (result) {case AccessDecisionVoter.ACCESS_GRANTED:return;case AccessDecisionVoter.ACCESS_DENIED:deny++;break;default:break;}}if (deny > 0) {throw new AccessDeniedException(this.messages.getMessage("AbstractAccessDecisionManager.accessDenied", "Access is denied"));}// To get this far, every AccessDecisionVoter abstainedcheckAllowIfAllAbstainDecisions();} }到這里又會牽扯到 AccessDecisionVoter 出來,也就是能夠投票的選民們。
AccessDecisionVoter 投票觀眾接口
我們先一起來看它的源碼,再看看它的實現類:
//表示一個類負責對授權決定進行投票。 //投票的協調(即輪詢AccessDecisionVoter ,統計他們的響應,并做出最終授權決定)由AccessDecisionManager執行。 public interface AccessDecisionVoter<S> {int ACCESS_GRANTED = 1;int ACCESS_ABSTAIN = 0;int ACCESS_DENIED = -1;//這兩個用來執行check操作,判斷參數是否合法等等boolean supports(ConfigAttribute attribute);boolean supports(Class<?> clazz);/** 指示是否授予訪問權限。 決定必須是肯定的 ( ACCESS_GRANTED )、否定的 ( ACCESS_DENIED ) 或者 AccessDecisionVoter可以棄權 ( ACCESS_ABSTAIN ) 投票。 在任何情況下,實現類都不應返回任何其他值。 如果需要對結果進行加權,則應改為在自定義AccessDecisionManager處理。 除非AccessDecisionVoter由于傳遞的方法調用或配置屬性參數而專門用于對訪問控制決策進行投票,否則它必須返回ACCESS_ABSTAIN 。 這可以防止協調AccessDecisionManager計算來自那些AccessDecisionVoter的選票,而這些AccessDecisionVoter對訪問控制決策沒有合法利益。 雖然安全對象(例如MethodInvocation )作為參數傳遞以最大限度地提高訪問控制決策的靈活性,但實現類不應修改它或導致所表示的調用發生(例如,通過調用MethodInvocation.proceed() ) .*/int vote(Authentication authentication, S object, Collection<ConfigAttribute> attributes); }我們看看它的結構:
AffirmativeBased默認傳入的構造器只有一個 WebExpressionVoter,這個構造器會根據你在配置文件中的配置進行邏輯處理得出投票結果。
所以我們在執行第一次循環時,也是在這里處理的。
public class WebExpressionVoter implements AccessDecisionVoter<FilterInvocation> {private SecurityExpressionHandler<FilterInvocation> expressionHandler = new DefaultWebSecurityExpressionHandler();@Overridepublic int vote(Authentication authentication, FilterInvocation filterInvocation,Collection<ConfigAttribute> attributes) {//...執行的一些檢查//WebExpressionConfigAttribute webExpressionConfigAttribute = findConfigAttribute(attributes);if (webExpressionConfigAttribute == null) {return ACCESS_ABSTAIN;}//允許對EvaluationContext進行后處理。 實現可能會返回一個新的EvaluationContext實例或修改傳入的EvaluationContext 。EvaluationContext ctx = webExpressionConfigAttribute.postProcess(//調用內部模板方法來創建StandardEvaluationContext和SecurityExpressionRoot對象。this.expressionHandler.createEvaluationContext(authentication, filterInvocation), filterInvocation);//針對指定的根對象評估默認上下文中的表達式。 如果評估結果與預期結果類型不匹配(并且無法轉換為),則將返回異常。boolean granted = ExpressionUtils.evaluateAsBoolean(webExpressionConfigAttribute.getAuthorizeExpression(), ctx);// 投贊同票,返回if (granted) {return ACCESS_GRANTED;}return ACCESS_DENIED;}//循環判斷private WebExpressionConfigAttribute findConfigAttribute(Collection<ConfigAttribute> attributes) {for (ConfigAttribute attribute : attributes) {if (attribute instanceof WebExpressionConfigAttribute) {return (WebExpressionConfigAttribute) attribute;}}return null;}//... }在這里的數據也是如此,和我們上文就互相對應上了。
4)返回過程
4.1、先返回至AffirmativeBased.decide()方法處,投票通過,繼續 retrun
for (AccessDecisionVoter voter : getDecisionVoters()) {int result = voter.vote(authentication, object, configAttributes);switch (result) {case AccessDecisionVoter.ACCESS_GRANTED:return;case AccessDecisionVoter.ACCESS_DENIED:deny++;break;default:break;} }4.2、返回至 AbstractSecurityInterceptor 方法調用處,這里是無返回值,直接回到 beforeInvocation 方法中。
private void attemptAuthorization(Object object, Collection<ConfigAttribute> attributes,Authentication authenticated) {try {this.accessDecisionManager.decide(authenticated, object, attributes);} }4.3、再返回至beforeInvocation 方法中,
protected InterceptorStatusToken beforeInvocation(Object object) {// 返回到這里,我們再順著往下看,看如何執行 attemptAuthorization(object, attributes, authenticated);// Attempt to run as a different user :嘗試以其他用戶身份運行Authentication runAs = this.runAsManager.buildRunAs(authenticated, object, attributes);if (runAs != null) {SecurityContext origCtx = SecurityContextHolder.getContext();SecurityContextHolder.setContext(SecurityContextHolder.createEmptyContext());SecurityContextHolder.getContext().setAuthentication(runAs);// 需要恢復到 token.Authenticated 調用后 true的意思是:如果能以其他用戶運行 就執行刷新return new InterceptorStatusToken(origCtx, true, attributes, object);}return new InterceptorStatusToken(SecurityContextHolder.getContext(), false, attributes, object); }4.4、回到了我們夢開始的地方了:FilterSecurityInterceptor.invoke() 方法
public void invoke(FilterInvocation filterInvocation) throws IOException, ServletException {if (isApplied(filterInvocation) && this.observeOncePerRequest) {// 過濾器已應用于此請求,用戶希望我們觀察每個請求處理一次,因此不要重新進行安全檢查filterInvocation.getChain().doFilter(filterInvocation.getRequest(), filterInvocation.getResponse());return;}// 第一次調用這個請求,所以執行安全檢查if (filterInvocation.getRequest() != null && this.observeOncePerRequest) {filterInvocation.getRequest().setAttribute(FILTER_APPLIED, Boolean.TRUE);}//返回至此處 //InterceptorStatusToken類上的doc注釋說://AbstractSecurityInterceptor子類接收的返回對象。//這個類反映了安全攔截的狀態,以便最終調用AbstractSecurityInterceptor.afterInvocation(InterceptorStatusToken, Object)可以正確整理。InterceptorStatusToken token = super.beforeInvocation(filterInvocation);try {//每個過濾器的必備代碼filterInvocation.getChain().doFilter(filterInvocation.getRequest(), filterInvocation.getResponse());}finally {super.finallyInvocation(token);}super.afterInvocation(token, null);}四、后半部分
對方法注解的鑒權,是真的一步一步看它如何執行的,一直扒,真的是歷經千辛萬苦。
默認大家都能看的懂這個圖了,我們直接轉到 MethodSecurityInterceptor 里來看看它做了什么吧
4.1、入口:MethodSecurityInterceptor
//提供對基于 AOP 聯盟的方法調用的安全攔截。 //此安全攔截器所需的SecurityMetadataSource是MethodSecurityMetadataSource類型。 這與基于 AspectJ 的安全攔截器 ( AspectJSecurityInterceptor ) 共享,因為兩者都與 Java Method 。 public class MethodSecurityInterceptor extends AbstractSecurityInterceptor implements MethodInterceptor {private MethodSecurityMetadataSource securityMetadataSource;//此方法應用于對MethodInvocation強制實施安全性。@Overridepublic Object invoke(MethodInvocation mi) throws Throwable {//beforeInvocation 這個有沒有似曾相識 ,莫錯哈 就是我們之前在 FilterSecurityInterceptor 看到的那個 //需要注意到的是 之前我們傳的參是一個 FilterInvocation ,這里則是一個 MethodInvocation 。InterceptorStatusToken token = super.beforeInvocation(mi);Object result;try {result = mi.proceed();}finally {super.finallyInvocation(token);}return super.afterInvocation(token, result);}//... }MethodInvocation :doc注釋是"方法調用的描述,在方法調用時提供給攔截器。方法調用是一個連接點,可以被方法攔截器攔截".
4.2、進入 AbstractSecurityInterceptor
授權檢查 beforeInvocation() 方法
另外在這里debug獲取到的值也是不一樣的,這點上文我剛剛也說過了。
獲取資源訪問策略:FilterSecurityInterceptor 會從 SecurityMetadataSource 的子類 DefaultFilterInvocationSecurityMetadataSource 獲取要訪問當前資源所需要的權限 Collection< ConfigAttribute >。 SecurityMetadataSource 其實就是讀取訪問策略的抽象,而讀取的內容,其實就是我們配置的訪問規則, 讀取訪問策略如:
protected void configure(HttpSecurity http) throws Exception {http.csrf().disable().authorizeRequests().antMatchers("/r/r1").hasAuthority("r1").antMatchers("/r/r2").hasAuthority("r2").... }中間的過程同上半部分差不多,就不多說了。我們直接看 AffirmativeBased 情況如何。
4.3、轉戰:AffirmativeBasedl;
attemptAuthorization(object, attributes, authenticated); this.accessDecisionManager.decide(authenticated, object, attributes);接著往下,到此處就同之前稍有不同了,我們之前用到的是 WebExpressionVoter,在這里我們使用的是: PreInvocationAuthorizationAdviceVoter
我們接著進入:PreInvocationAuthorizationAdviceVoter,它的類上的doc注釋如下:
Voter 使用從 @PreFilter 和 @PreAuthorize 注釋生成的 PreInvocationAuthorizationAdvice 實現來執行操作。
在實踐中,如果使用這些注解,它們通常會包含所有必要的訪問控制邏輯,因此基于投票者的系統并不是真正必要的,包含相同邏輯的單個AccessDecisionManager就足夠了。 然而,這個類很容易與 Spring Security 使用的傳統的基于投票者的AccessDecisionManager實現相適應。
我們可以很容易的看出,這個就是處理方法上注解的那個類。接著看下它的源碼。
public class PreInvocationAuthorizationAdviceVoter implements AccessDecisionVoter<MethodInvocation> {private final PreInvocationAuthorizationAdvice preAdvice;public PreInvocationAuthorizationAdviceVoter(PreInvocationAuthorizationAdvice pre) {this.preAdvice = pre;}//...一些檢查方法@Overridepublic int vote(Authentication authentication, MethodInvocation method, Collection<ConfigAttribute> attributes) {// 查找 prefilter 和 preauth(或組合)屬性,如果兩者都為 null,則棄權使用它們調用建議PreInvocationAttribute preAttr = findPreInvocationAttribute(attributes);if (preAttr == null) {// 沒有基于表達式的元數據,所以棄權return ACCESS_ABSTAIN;}//在這里又委托給 PreInvocationAuthorizationAdvice接口的before方法來做判斷return this.preAdvice.before(authentication, method, preAttr) ? ACCESS_GRANTED : ACCESS_DENIED;}private PreInvocationAttribute findPreInvocationAttribute(Collection<ConfigAttribute> config) {for (ConfigAttribute attribute : config) {if (attribute instanceof PreInvocationAttribute) {return (PreInvocationAttribute) attribute;}}return null;} }簡單看一下PreInvocationAuthorizationAdvice接口的before方法的默認實現:
before方法的說明是:應該執行的“before”建議以執行任何必要的過濾并決定方法調用是否被授權。
我們先說說它的參數:(Authentication authentication,MethodInvocation mi,PreInvocationAttribute attr),第一個就是當前登錄的用戶,二就是要執行的方法,三就是方法上的注解信息。
我們可以很簡單的看出這段代碼的含義,就是在比較已經登錄的用戶,是否擁有這個方法上所需要的權限。
另外簡單說明一下:
createEvaluationContext 的dco注釋:提供評估上下文,在其中評估調用類型的安全表達式(即 SpEL 表達式)。我個人對這塊沒有特別深入過,沒法說清楚,大家可以查一查。
另外我們看一下debug的詳細信息,大家應該就差不多能懂啦。
接下來就是一步一步返回啦
最后就是:
這里的 result 就是方法執行的返回結果。緊接著就是一步一步返回過濾器鏈啦。
對于這里 proceed方法就不再深入了。這個點拉出來說,怕是直接可以寫上一篇完整的文章啦。
內部很多動態代理啊、反射啊這些相關的,一層套一層的,不是咱研究重點。溜啦溜啦。
五、小結
這張圖是在百度上搜到的,大致流程其實就是如此。
其實內部還有很多很多值得推敲的東西,不是在這一篇簡單的文章中能夠寫出來的。
六、自我感受
還記得我第一次說要看源碼是在準備研究 Mybatis 的時候,那時候上頭看了大概幾天吧,看著看著就看不下去了,找不到一個合適的方法,什么都想看,沒有一個非常具體的目標,導致連續受挫,結果就是不了了之了。
第二次真正意義看源碼就是看 Security 。原因是當時在寫項目的時,我的前端小伙伴說,現在大部分網站都有多種登錄方式,你能實現不?
男人肯定是不能說不行,然后我就一口答應下來了。結果就是瘋狂百度、google,到處看博客。互聯網這么龐大,當然也有找到非常多的例子,也有源碼解析。但是找到的文章,要么只貼出了核心代碼,要么就是不合適(龐大,難以抽取),總之一句話沒法運行。就很煩操。
不過文章中都提到了要理解 Security 的登錄過程,然后進行仿寫,俗稱抄作業。最后,真就是一步一步 debug 去看 Security 的登錄過程,寫出了 第一篇 Security登錄認證流程分析,緊接著又去用 SpringSecurity實現多種登錄方式,如郵件驗證碼、電話號碼登錄。這次即是機緣巧合,也是心有所念,耗費不少時間寫出了這篇文章。感覺還是非常不錯的。
希望大家能夠喜歡,如果 xdm 對此也感興趣,希望大家在有時間的情況,debug 幾次,記憶會深刻很多。并竟 紙上得來終覺淺,絕知此事要躬行。
相關文章:
今天的文章就到這里了。
你好,我是博主寧在春:主頁
如若在文章中遇到疑惑,請留言或私信,或者加主頁聯系方式,都會盡快回復。
如若發現文章中存在問題,望你能夠指正,不勝感謝。
如果覺得對你有所幫助的話,請點個贊再走吧!
總結
以上是生活随笔為你收集整理的SpringSecurity鉴权流程分析 附源码注释,xdm,一起来看看吧的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: VsCode 配置 C/C++ 开发环境
- 下一篇: gradle idea java ssm