您好,登錄后才能下訂單哦!
Spring Security怎么解析授權過程,很多新手對此不是很清楚,為了幫助大家解決這個難題,下面小編將為大家詳細講解,有這方面需求的人可以來學習下,希望你能有所收獲。
> ??在學習Spring Cloud 時,遇到了授權服務oauth 相關內容時,總是一知半解,因此決定先把Spring Security 、Spring Security Oauth3 等權限、認證相關的內容、原理及設計學習并整理一遍。
> 項目環境: > - JDK1.8 > - Spring boot 2.x > - Spring Security 5.x
??自定義MyUserDetailsUserService類,實現 UserDetailsService 接口的 loadUserByUsername()方法,這里就簡單的返回一個Spring Security 提供的 User 對象。為了后面方便演示Spring Security 的權限控制,這里使用AuthorityUtils.commaSeparatedStringToAuthorityList("admin") 設置了user賬號有一個admin的角色權限信息。實際項目中可以在這里通過訪問數據庫獲取到用戶及其角色、權限信息。
@Component public class MyUserDetailsUserService implements UserDetailsService { @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { // 不能直接使用 創建 BCryptPasswordEncoder 對象來加密, 這種加密方式 沒有 {bcrypt} 前綴, // 會導致在 matches 時導致獲取不到加密的算法出現 // java.lang.IllegalArgumentException: There is no PasswordEncoder mapped for the id "null" 問題 // 問題原因是 Spring Security5 使用 DelegatingPasswordEncoder(委托) 替代 NoOpPasswordEncoder, // 并且 默認使用 BCryptPasswordEncoder 加密(注意 DelegatingPasswordEncoder 委托加密方法BCryptPasswordEncoder 加密前 添加了加密類型的前綴) https://blog.csdn.net/alinyua/article/details/80219500 return new User("user", PasswordEncoderFactories.createDelegatingPasswordEncoder().encode("123456"), AuthorityUtils.commaSeparatedStringToAuthorityList("admin")); } }
??注意Spring Security 5 開始沒有使用 NoOpPasswordEncoder作為其默認的密碼編碼器,而是默認使用 DelegatingPasswordEncoder 作為其密碼編碼器,其 encode 方法是通過 密碼編碼器的名稱作為前綴 + 委托各類密碼編碼器來實現encode的。
public String encode(CharSequence rawPassword) { return "{" + this.idForEncode + "}" + this.passwordEncoderForEncode.encode(rawPassword); }
??這里的 idForEncode 就是密碼編碼器的簡略名稱,可以通過 PasswordEncoderFactories.createDelegatingPasswordEncoder() 內部實現看到默認是使用的前綴是 bcrypt 也就是 BCryptPasswordEncoder
public class PasswordEncoderFactories { public static PasswordEncoder createDelegatingPasswordEncoder() { String encodingId = "bcrypt"; Map<string, passwordencoder> encoders = new HashMap(); encoders.put(encodingId, new BCryptPasswordEncoder()); encoders.put("ldap", new LdapShaPasswordEncoder()); encoders.put("MD4", new Md4PasswordEncoder()); encoders.put("MD5", new MessageDigestPasswordEncoder("MD5")); encoders.put("noop", NoOpPasswordEncoder.getInstance()); encoders.put("pbkdf2", new Pbkdf2PasswordEncoder()); encoders.put("scrypt", new SCryptPasswordEncoder()); encoders.put("SHA-1", new MessageDigestPasswordEncoder("SHA-1")); encoders.put("SHA-256", new MessageDigestPasswordEncoder("SHA-256")); encoders.put("sha256", new StandardPasswordEncoder()); return new DelegatingPasswordEncoder(encodingId, encoders); } }
??定義SpringSecurityConfig 配置類,并繼承WebSecurityConfigurerAdapter覆蓋其configure(HttpSecurity http) 方法。
@Configuration @EnableWebSecurity //1 public class SpringSecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http.formLogin() //2 .and() .authorizeRequests() //3 .antMatchers("/index","/").permitAll() //4 .anyRequest().authenticated(); //6 } }
配置解析:
@EnableWebSecurity 查看其注解源碼,主要是引用WebSecurityConfiguration.class 和 加入了@EnableGlobalAuthentication 注解 ,這里就不介紹了,我們只要明白添加 @EnableWebSecurity 注解將開啟 Security 功能。
formLogin() 使用表單登錄(默認請求地址為 /login),在Spring Security 5 里其實已經將舊版本默認的 httpBasic() 更換成 formLogin() 了,這里為了表明表單登錄還是配置了一次。
authorizeRequests() 開始請求權限配置
antMatchers() 使用Ant風格的路徑匹配,這里配置匹配 / 和 /index
permitAll() 用戶可任意訪問
anyRequest() 匹配所有路徑
authenticated() 用戶登錄后可訪問
?? 在 resources/static 目錄下新建 index.html , 其內部定義一個訪問測試接口的按鈕
<meta charset="UTF-8"> <title>歡迎</title> Spring Security 歡迎你! <p> <a href="/get_user/test">測試驗證Security 權限控制</a></p>
??創建 rest 風格的獲取用戶信息接口
@RestController public class TestController { @GetMapping("/get_user/{username}") public String getUser(@PathVariable String username){ return username; } }
1、訪問 localhost:8080 無任何阻攔直接成功
2、點擊測試驗證權限控制按鈕 被重定向到了 Security默認的登錄頁面
3、使用 MyUserDetailsUserService定義的默認賬戶 user : 123456 進行登錄后成功跳轉到 /get_user 接口
?? 還記得之前講過 @EnableWebSecurity 引用了 WebSecurityConfiguration 配置類 和 @EnableGlobalAuthentication 注解嗎? 其中 WebSecurityConfiguration 就是與授權相關的配置,@EnableGlobalAuthentication 配置了 認證相關的我們下節再細討。
?? 首先我們查看 WebSecurityConfiguration 源碼,可以很清楚的發現 springSecurityFilterChain() 方法。
@Bean(name = AbstractSecurityWebApplicationInitializer.DEFAULT_FILTER_NAME) public Filter springSecurityFilterChain() throws Exception { boolean hasConfigurers = webSecurityConfigurers != null && !webSecurityConfigurers.isEmpty(); if (!hasConfigurers) { WebSecurityConfigurerAdapter adapter = objectObjectPostProcessor .postProcess(new WebSecurityConfigurerAdapter() { }); webSecurity.apply(adapter); } return webSecurity.build(); //1 }
??這個方法首先會判斷 webSecurityConfigurers 是否為空,為空加載一個默認的 WebSecurityConfigurerAdapter對象,由于自定義的 SpringSecurityConfig 本身是繼承 WebSecurityConfigurerAdapter對象 的,所以我們自定義的 Security 配置肯定會被加載進來的(如果想要了解如何加載進來可以看下WebSecurityConfiguration.setFilterChainProxySecurityConfigurer() 方法)。
?? 我們看下 webSecurity.build() 方法實現 實際調用的是 AbstractConfiguredSecurityBuilder.doBuild() 方法,其方法內部實現如下:
@Override protected final O doBuild() throws Exception { synchronized (configurers) { buildState = BuildState.INITIALIZING; beforeInit(); init(); buildState = BuildState.CONFIGURING; beforeConfigure(); configure(); buildState = BuildState.BUILDING; O result = performBuild(); // 1 創建 DefaultSecurityFilterChain (Security Filter 責任鏈 ) buildState = BuildState.BUILT; return result; } }
?? 我們把關注點放到 performBuild() 方法,看其實現子類 HttpSecurity.performBuild() 方法,其內部排序 filters 并創建了 DefaultSecurityFilterChain 對象。
@Override protected DefaultSecurityFilterChain performBuild() throws Exception { Collections.sort(filters, comparator); return new DefaultSecurityFilterChain(requestMatcher, filters); }
?? 查看DefaultSecurityFilterChain 的構造方法,我們可以看到有記錄日志。
public DefaultSecurityFilterChain(RequestMatcher requestMatcher, List<filter> filters) { logger.info("Creating filter chain: " + requestMatcher + ", " + filters); // 按照正常情況,我們可以看到控制臺輸出 這條日志 this.requestMatcher = requestMatcher; this.filters = new ArrayList<>(filters); }
?? 我們可以回頭看下項目啟動日志。可以看到下圖明顯打印了 這條日志,并且把所有 Filter名都打印出來了。==(請注意這里打印的 filter 鏈,接下來我們的所有授權過程都是依靠這條filter 鏈展開 )==
??那么還有個疑問: HttpSecurity.performBuild() 方法中的 filters 是怎么加載的呢? 這個時候需要查看 WebSecurityConfigurerAdapter.init() 方法,這個方法內部 調用 getHttp() 方法返回 HttpSecurity 對象(看到這里我們應該能想到 filters 就是這個方法中添加好了數據),具體如何加載的也就不介紹了。
public void init(final WebSecurity web) throws Exception { final HttpSecurity http = getHttp(); // 1 web.addSecurityFilterChainBuilder(http).postBuildAction(new Runnable() { public void run() { FilterSecurityInterceptor securityInterceptor = http .getSharedObject(FilterSecurityInterceptor.class); web.securityInterceptor(securityInterceptor); } }); }
?? 用了這么長時間解析 @EnableWebSecurity ,其實最關鍵的一點就是創建了 DefaultSecurityFilterChain 也就是我們常 security filter 責任鏈,接下來我們圍繞這個 DefaultSecurityFilterChain 中 的 filters 進行授權過程的解析。
> ??Security的授權過程可以理解成各種 filter 處理最終完成一個授權。那么我們再看下之前 打印的filter 鏈,這里為了方便,再次貼出圖片
??這里我們只關注以下幾個重要的 filter : > - SecurityContextPersistenceFilter > - UsernamePasswordAuthenticationFilter (AbstractAuthenticationProcessingFilter) > - BasicAuthenticationFilter > - AnonymousAuthenticationFilter > - ExceptionTranslationFilter > - FilterSecurityInterceptor
??SecurityContextPersistenceFilter 這個filter的主要負責以下幾件事:
> - 通過 (SecurityContextRepository)repo.loadContext() 方法從請求Session中獲取 SecurityContext(Security 上下文 ,類似 ApplicaitonContext ) 對象,如果請求Session中沒有默認創建一個 authentication(認證的關鍵對象,由于本節只講授權,暫不介紹) 屬性為 null 的 SecurityContext 對象 > - SecurityContextHolder.setContext() 將 SecurityContext 對象放入 SecurityContextHolder進行管理(SecurityContextHolder默認使用ThreadLocal 策略來存儲認證信息) > - 由于在 finally 里實現 會在最后通過 SecurityContextHolder.clearContext() 將 SecurityContext 對象 從 SecurityContextHolder中清除 > - 由于在 finally 里實現 會在最后通過 repo.saveContext() 將 SecurityContext 對象 放入Session中
HttpRequestResponseHolder holder = new HttpRequestResponseHolder(request, response); //從Session中獲取SecurityContxt 對象,如果Session中沒有則創建一個 authtication 屬性為 null 的SecurityContext對象 SecurityContext contextBeforeChainExecution = repo.loadContext(holder); try { // 將 SecurityContext 對象放入 SecurityContextHolder進行管理 (SecurityContextHolder默認使用ThreadLocal 策略來存儲認證信息) SecurityContextHolder.setContext(contextBeforeChainExecution); chain.doFilter(holder.getRequest(), holder.getResponse()); } finally { SecurityContext contextAfterChainExecution = SecurityContextHolder .getContext(); // 將 SecurityContext 對象 從 SecurityContextHolder中清除 SecurityContextHolder.clearContext(); // 將 SecurityContext 對象 放入Session中 repo.saveContext(contextAfterChainExecution, holder.getRequest(), holder.getResponse()); request.removeAttribute(FILTER_APPLIED); if (debug) { logger.debug("SecurityContextHolder now cleared, as request processing completed"); } }
??我們在 SecurityContextPersistenceFilter 中打上斷點,啟動項目,訪問 localhost:8080 , 來debug看下實現:
?? 我們可以清楚的看到創建了一個authtication 為null 的 SecurityContext對象,并且可以看到請求調用的filter鏈具體有哪些。接下來看下 finally 內部處理
?? 你會發現這里的SecurityContxt中的 authtication 是一個名為 anonymousUser (匿名用戶)的認證信息,這是因為 請求調用到了 AnonymousAuthenticationFilter , Security默認創建了一個匿名用戶訪問。
??看filter字面意思就知道這是一個通過獲取請求中的賬戶密碼來進行授權的filter,按照慣例,整理了這個filter的職責: > - 通過 requiresAuthentication()判斷 是否以POST 方式請求 /login > - 調用 attemptAuthentication() 方法進行認證,內部創建了 authenticated 屬性為 false(即未授權)的UsernamePasswordAuthenticationToken 對象, 并傳遞給 AuthenticationManager().authenticate() 方法進行認證,認證成功后 返回一個 authenticated = true (即授權成功的)UsernamePasswordAuthenticationToken 對象 > - 通過 sessionStrategy.onAuthentication() 將 Authentication 放入Session中 > - 通過 successfulAuthentication() 調用 AuthenticationSuccessHandler 的 onAuthenticationSuccess 接口 進行成功處理( 可以 通過 繼承 AuthenticationSuccessHandler 自行編寫成功處理邏輯 )successfulAuthentication(request, response, chain, authResult); > - 通過 unsuccessfulAuthentication() 調用AuthenticationFailureHandler 的 onAuthenticationFailure 接口 進行失敗處理(可以通過繼承AuthenticationFailureHandler 自行編寫失敗處理邏輯 )
??我們再看下官方源碼的處理邏輯:
// 1 AbstractAuthenticationProcessingFilter 的 doFilter 方法 public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException { HttpServletRequest request = (HttpServletRequest) req; HttpServletResponse response = (HttpServletResponse) res; // 2 判斷請求地址是否是 /login 和 請求方式為 POST (UsernamePasswordAuthenticationFilter 構造方法 確定的) if (!requiresAuthentication(request, response)) { chain.doFilter(request, response); return; } Authentication authResult; try { // 3 調用 子類 UsernamePasswordAuthenticationFilter 的 attemptAuthentication 方法 // attemptAuthentication 方法內部創建了 authenticated 屬性為 false (即未授權)的 UsernamePasswordAuthenticationToken 對象, 并傳遞給 AuthenticationManager().authenticate() 方法進行認證, //認證成功后 返回一個 authenticated = true (即授權成功的) UsernamePasswordAuthenticationToken 對象 authResult = attemptAuthentication(request, response); if (authResult == null) { return; } // 4 將認證成功的 Authentication 存入Session中 sessionStrategy.onAuthentication(authResult, request, response); } catch (InternalAuthenticationServiceException failed) { // 5 認證失敗后 調用 AuthenticationFailureHandler 的 onAuthenticationFailure 接口 進行失敗處理( 可以 通過 繼承 AuthenticationFailureHandler 自行編寫失敗處理邏輯 ) unsuccessfulAuthentication(request, response, failed); return; } catch (AuthenticationException failed) { // 5 認證失敗后 調用 AuthenticationFailureHandler 的 onAuthenticationFailure 接口 進行失敗處理( 可以 通過 繼承 AuthenticationFailureHandler 自行編寫失敗處理邏輯 ) unsuccessfulAuthentication(request, response, failed); return; } ...... // 6 認證成功后 調用 AuthenticationSuccessHandler 的 onAuthenticationSuccess 接口 進行失敗處理( 可以 通過 繼承 AuthenticationSuccessHandler 自行編寫成功處理邏輯 ) successfulAuthentication(request, response, chain, authResult); }
??從源碼上看,整個流程其實是很清晰的:從判斷是否處理,到認證,最后判斷認證結果分別作出認證成功和認證失敗的處理。
??debug 調試下看 結果,這次我們請求 localhast:8080/get_user/test , 由于沒權限會直接跳轉到登錄界面,我們先輸入錯誤的賬號密碼,看下認證失敗是否與我們總結的一致。
??結果與預想時一致的,也許你會奇怪這里的提示為啥時中文,這就不得不說Security 5 開始支持 中文,說明咋中國程序員在世界上越來越有地位了!!!
?? 這次輸入正確的密碼, 看下返回的Authtication 對象信息:
?? 可以看到這次成功返回一個 authticated = ture ,沒有密碼的 user賬戶信息,而且還包含我們定義的一個admin權限信息。放開斷點,由于Security默認的成功處理器是SimpleUrlAuthenticationSuccessHandler ,這個處理器會重定向到之前訪問的地址,也就是 localhast:8080/get_user/test。 至此整個流程結束。不,我們還差一個,Session,我們從瀏覽器Cookie中看到 Session:
??BasicAuthenticationFilter 與UsernameAuthticationFilter類似,不過區別還是很明顯,BasicAuthenticationFilter 主要是從Header 中獲取 Authorization 參數信息,然后調用認證,認證成功后最后直接訪問接口,不像UsernameAuthticationFilter過程一樣通過AuthenticationSuccessHandler 進行跳轉。這里就不在貼代碼了,想了解的同學可以直接看源碼。不過有一點要注意的是,BasicAuthenticationFilter 的 onSuccessfulAuthentication() 成功處理方法是一個空方法。
?? 為了試驗BasicAuthenticationFilter, 我們需要將 SpringSecurityConfig 中的formLogin()更換成httpBasic()以支持BasicAuthenticationFilter,重啟項目,同樣訪問 localhast:8080/get_user/test,這時由于沒權限訪問這個接口地址,頁面上會彈出一個登陸框,熟悉Security4的同學一定很眼熟吧,同樣,我們輸入賬戶密碼后,看下debug數據:
?? 這時,我們就能夠獲取到 Authorization 參數,進而解析獲取到其中的賬戶和密碼信息,進行認證,我們查看認證成功后返回的Authtication對象信息其實是和UsernamePasswordAuthticationFilter中的一致,最后再次調用下一個filter,由于已經認證成功了會直接進入FilterSecurityInterceptor 進行權限驗證。
??這里為什么要提下 AnonymousAuthenticationFilter呢,主要是因為在Security中不存在沒有賬戶這一說法(這里可能描述不是很清楚,但大致意思是這樣的),針對這個Security官方專門指定了這個AnonymousAuthenticationFilter ,用于前面所有filter都認證失敗的情況下,自動創建一個默認的匿名用戶,擁有匿名訪問權限。還記得 在講解 SecurityContextPersistenceFilter 時我們看到得匿名 autication信息么?如果不記得還得回頭看下哦,這里就不再敘述了。
??ExceptionTranslationFilter 其實沒有做任何過濾處理,但別小看它得作用,它最大也最牛叉之處就在于它捕獲AuthenticationException 和AccessDeniedException,如果發生的異常是這2個異常 會調用 handleSpringSecurityException()方法進行處理。 我們模擬下 AccessDeniedException(無權限,禁止訪問異常)情況,首先我們需要修改下 /get_user 接口:
在Controller 上添加 @EnableGlobalMethodSecurity(prePostEnabled =true) 啟用Security 方法級別得權限控制
在 接口上添加 @PreAuthorize("hasRole('user')") 只允許有user角色得賬戶訪問(還記得我們默認得user 賬戶時admin角色么?)
@RestController @EnableGlobalMethodSecurity(prePostEnabled =true) // 開啟方法級別的權限控制 public class TestController { @PreAuthorize("hasRole('user')") //只允許user角色訪問 @GetMapping("/get_user/{username}") public String getUser(@PathVariable String username){ return username; } }
??重啟項目,重新訪問 /get_user 接口,輸入正確的賬戶密碼,發現返回一個 403 狀態的錯誤頁面,這與我們之前將的流程時一致的。debug,看下處理:
??可以明顯的看到異常對象是 AccessDeniedException ,異常信息是不允許訪問,我們再看下 AccessDeniedException 異常后的處理方法accessDeniedHandler.handle(),進入到了 AccessDeniedHandlerImpl 的handle()方法,這個方法會先判斷系統是否配置了 errorPage (錯誤頁面),沒有的話直接往 response 中設置403 狀態碼。
??FilterSecurityInterceptor 是整個Security filter鏈中的最后一個,也是最重要的一個,它的主要功能就是判斷認證成功的用戶是否有權限訪問接口,其最主要的處理方法就是 調用父類(AbstractSecurityInterceptor)的 super.beforeInvocation(fi),我們來梳理下這個方法的處理流程:
> - 通過 obtainSecurityMetadataSource().getAttributes() 獲取 當前訪問地址所需權限信息 > - 通過 authenticateIfRequired() 獲取當前訪問用戶的權限信息 > - 通過 accessDecisionManager.decide() 使用 投票機制判權,判權失敗直接拋出 AccessDeniedException 異常
protected InterceptorStatusToken beforeInvocation(Object object) { ...... // 1 獲取訪問地址的權限信息 Collection<configattribute> attributes = this.obtainSecurityMetadataSource() .getAttributes(object); if (attributes == null || attributes.isEmpty()) { ...... return null; } ...... // 2 獲取當前訪問用戶權限信息 Authentication authenticated = authenticateIfRequired(); try { // 3 默認調用AffirmativeBased.decide() 方法, 其內部 使用 AccessDecisionVoter 對象 進行投票機制判權,判權失敗直接拋出 AccessDeniedException 異常 this.accessDecisionManager.decide(authenticated, object, attributes); } catch (AccessDeniedException accessDeniedException) { publishEvent(new AuthorizationFailureEvent(object, attributes, authenticated, accessDeniedException)); throw accessDeniedException; } ...... return new InterceptorStatusToken(SecurityContextHolder.getContext(), false, attributes, object); }
?? 整個流程其實看起來不復雜,主要就分3個部分,首選獲取訪問地址的權限信息,其次獲取當前訪問用戶的權限信息,最后通過投票機制判斷出是否有權。
整個授權流程核心的就在于這幾次核心filter的處理,這里我用序列圖來概況下這個授權流程
看完上述內容是否對您有幫助呢?如果還想對相關知識有進一步的了解或閱讀更多相關文章,請關注億速云行業資訊頻道,感謝您對億速云的支持。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。