SpringSecurity 基礎
什么是安全框架
安全框架顧名思義,就是解決系統安全問題的框架。任何應用開發的計划階段都應該確定一組特定的安全需求,如身份驗證、授權和加密方式。不使用安全框架之前,我們需要手動處理每個資源的訪問控制,針對不同的項目都需要做不同對處理,此時就會顯得非常麻煩,並且低效率引起的額外開銷會延緩開發周期。使用安全框架,使開發團隊能夠選擇最適合這些需求的框架,可以通過配置的方式實現對資源的訪問限制,使得開發更加的高效。
常用的安全框架
Spring Security: Spring 家族一員,是一個能夠為基於 Spring 的企業應用系統提供聲明式的安全訪問控制解決方案的安全框架。它提供了一組可以在 Spring 應用上下文中配置的 Bean,充分利用了 Spring IoC(控制反轉)、DI(依賴注入)和 AOP(面向切面編程)功能,為應用系統提供聲明式的安全訪問控制功能,減少了為企業系統安全控制編寫大量重復代碼的工作。
Apache Shiro:一個功能強大且易於使用的Java安全框架,提供了認證、授權、加密和會話管理功能。使用 Shiro 的易於理解的API,您可以快速、輕松地獲得任何應用程序,從最小的移動應用程序到最大的網絡和企業應用程序。
SpringSecurity 簡介
Spring Security 是一個高度自定義的安全框架,利用 Spring loC、DI 和 AOP 功能,為系統提供了聲明式安全訪問控制功能,減少了為系統安全而編寫大星重復代碼的工作。
使用 Spring Secruity 的原因有很多,但大部分都是發現了 javaEE 的 Servlet 規范或 EJ8 規范中的安全功能缺乏典型企業應用場景。同時認識到他們在WAR 或 EAR 級別無法移植,因此如果你更換服務器環境,還有大星工作去重新配置你的應用程序,使用 Spring Security 解決了這些問題,並且提供了可定制的安全功能,比如認證和授權。
SpringBoot 沒有發布之前,Shiro 應用更加廣泛,因為 Shiro 是一個強大且易用的 Java 安全框架,能夠非常清晰的處理身份驗證、授權、管理會話以及密碼加密。利用其易於理解的API,可以快速、輕松地獲得任何應用程序,從最小的移動應用程序到最大的網絡和企業應用程序。但是 Shiro 只是一個框架而已,其中的內容需要自己的去構建,前后是自己的,中間是Shiro幫我們去搭建和配置好的。
SpringBoot 發布后,隨着其快速發展,Spring Security 重新進入人們的視野。SpringBoot 解決了 Spring Security 各種復雜的配置,Spring Security 在我們進行用戶認證以及授予權限的時候,通過各種各樣的攔截器來控制權限的訪問,從而實現安全,也就是說 Spring Security 除了不能脫離 Spring,Shiro 的功能它都有。
-
在用戶認證方面,Spring Security 框架支持主流的認證方式,包括 HTTP 基本認證、HTTP 表單驗證、HTTP 摘要認證、OpenID 和 LDAP 等。
-
在用戶授權方面,Spring Security 提供了基於角色的訪問控制和訪問控制列表(Access Control List,ACL),可以對應用中的領域對象進行細粒度的控制。
SpringSecurity 入門
引入依賴
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
導入配置
@Configuration
@EnableGlobalMethodSecurity(securedEnabled = true, prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity httpSecurity) throws Exception {
httpSecurity
// 禁用csrf(跨站請求偽造)
.cors().and().csrf().disable()
// 設置表單登陸以及登錄頁面,自動開啟登錄頁,如果沒有登錄,沒有權限就會來到登錄頁面
.formLogin().loginPage("/login.html").and()
// 過濾請求
.authorizeRequests()
// 訪問此地址不需要進行身份認證,允許直接訪問,防止重定向死循環
.antMatchers("/login.html").permitAll()
// 除上面外的所有請求全部需要鑒權認證,訪問任何資源都需要身份認證
.anyRequest().authenticated();
}
}
登錄測試
啟動服務之后,如果只實現一個 WebSecurityConfigurerAdapter 然后重寫一下 configure 方法,效果會默認使用Spring Security 的登錄頁 ,同時項目啟動時后台會打印出一個默認的密碼,然后使用任意賬號就可以進行登錄訪問指定的資源。
SpringSecurity 核心
基本原理
Spring Security 所解決的問題就是安全訪問控制,而安全訪問控制功能其實就是對所有進入系統的請求進行攔截,校驗每個請求是否能夠訪問它所期望的資源,並且采用的是責任鏈的設計模式。
Spring Security 對 Web 資源的保護是靠過濾器鏈(Filter Chain)實現的。當初始化 Spring Security 時,會創建一個名為 springSecurityFilterChain 的 Servlet 過濾器鏈,類型 FilterChainProxy,它實現 Filter,因此外部的請求會經過此類,下圖是 Spring Security 過慮器鏈結構圖:
FilterChainProxy 是一個代理,真正起作用的是 FilterChainProxy 中 SecurityFilterChain 所包含的各個 Filter,同時這些 Filter 作為 Bean 被 Spring 管理,它們是 Spring Security 核心,各有各的職責,但他們並不直接處理用戶的認證,也不直接處理用戶的授權,而是把它們交給了認證管理器(AuthenticationManager)和決策管理器 (AccessDecisionManager)進行處理。
Spring Security 功能的實現主要是由一系列過濾器鏈相互配合完成,如下圖:
下面介紹過濾器鏈中主要的幾個過濾器及其作用:
-
WebAsyncManagerIntegrationFilter:將 Security 上下文與 Spring Web 中用於處理異步請求映射的 WebAsyncManager 進行集成。
-
SecurityContextPersistenceFilter :每次請求處理之前將該請求相關的安全上下文信息加載到 SecurityContextHolder 中,然后在該次請求處理完成之后,將 SecurityContextHolder 中關於這次請求的信息存儲到一個“倉儲”中,然后將 SecurityContextHolder 中的信息清除。
-
UsernamePasswordAuthenticationFilter :用於處理基於表單的登錄請求,從表單中獲取用戶名和密碼。默認情況下處理來自 /login 的請求。從表單中獲取用戶名和密碼時,默認使用的表單 name 值為 username 和 password,這兩個值可以通過設置這個過濾器的usernameParameter 和 passwordParameter 兩個參數的值進行修改。其內部還有登錄成功或失敗后進行處理的 AuthenticationSuccessHandler 和 AuthenticationFailureHandler,這些都可以根據需求做相關改變。
-
HeaderWriterFilter:用於將頭信息加入響應中。
-
CsrfFilter:用於處理跨站請求偽造。
-
LogoutFilter:用於處理退出登錄。
-
DefaultLoginPageGeneratingFilter:如果沒有配置登錄頁面,那系統初始化時就會配置這個過濾器,並且用於在需要進行登錄時生成一個登錄表單頁面。
-
BasicAuthenticationFilter:檢測和處理 http basic 認證。
-
RequestCacheAwareFilter:用來處理請求的緩存。
-
SecurityContextHolderAwareRequestFilter:主要是包裝請求對象 request。
-
AnonymousAuthenticationFilter:檢測 SecurityContextHolder 中是否存在 Authentication 對象,如果不存在為其提供一個匿名 Authentication。
-
SessionManagementFilter:管理 session 的過濾器
-
ExceptionTranslationFilter:處理 AccessDeniedException 和 AuthenticationException 異常。
-
FilterSecurityInterceptor: 是用於保護web資源的,使用 AccessDecisionManager 對當前用戶進行授權訪問。
-
RememberMeAuthenticationFilter:當用戶沒有登錄而直接訪問資源時, 從 cookie 里找出用戶的信息, 如果 Spring Security 能夠識別出用戶提供的 remember me cookie, 用戶將不必填寫用戶名和密碼, 而是直接登錄進入系統,該過濾器默認不開啟。
下面看一下 Spring Security 整個執行流程圖,只要把 Spring Security 的執行過程弄明白了,這個框架就會變得很簡單:
流程說明:
-
客戶端發起一個請求,進入 Security 過濾器鏈。
-
當到 LogoutFilter 的時候判斷是否是登出路徑,如果是登出路徑則到 logoutHandler ,如果登出成功則到 logoutSuccessHandler 登出成功處理,如果登出失敗則由 ExceptionTranslationFilter ;如果不是登出路徑則直接進入下一個過濾器。
-
當到 UsernamePasswordAuthenticationFilter 的時候判斷是否為登錄路徑,如果是,則進入該過濾器進行登錄操作,如果登錄失敗則到 AuthenticationFailureHandler 登錄失敗處理器處理,如果登錄成功則到 AuthenticationSuccessHandler 登錄成功處理器處理,如果不是登錄請求則不進入該過濾器。
-
當到 FilterSecurityInterceptor 的時候會拿到 uri ,根據 uri 去找對應的鑒權管理器,鑒權管理器做鑒權工作,鑒權成功則到 Controller 層,否則到 AccessDeniedHandler 鑒權失敗處理器處理。
核心配置
系統集成 Spring Security 后,通過創建配置類並繼承 WebSecurityConfigurerAdapter 類,這個類里面可以完成上述流程圖的所有配置,也就是認證及授權。接下來我們通過一個完整的配置類來進行詳細認識:
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
/**
* 自定義用戶認證邏輯
*/
@Autowired
private UserDetailsService userDetailsService;
/**
* 認證失敗處理類
*/
@Autowired
private AuthenticationEntryPointImpl unauthorizedHandler;
/**
* 退出處理類
*/
@Autowired
private LogoutSuccessHandlerImpl logoutSuccessHandler;
/**
* 跨域過濾器
*/
@Autowired
private CorsFilter corsFilter;
/**
* 資源請求配置
*/
@Override
protected void configure(HttpSecurity httpSecurity) throws Exception {
// ...
}
/**
* 全局安全性配置
*/
public void configure(WebSecurity web) {
web.ignoring().antMatchers(new String[]{"/v3/api-docs", "/swagger-resources/configuration/ui",
"/swagger-resources", "/swagger-ui.html", "/swagger-ui/*", "/modeler/**", "/**/doc.html",
"/favicon.ico", "/definition/**", "/activiti/**", "/**/*.css", "/**/*.js", "/**/*.png",
"/**/*.gif", "/swagger-resources/**", "/**/*.ttf", "/upload/**", "/process/read-resource/**",
"/ueditor/**", "/**/export/**", "/**importGdCache/**", "/**/sysGlobalConfig/**", "/OAuth/**", "/v1/**"});
}
/**
* 身份認證接口
*/
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService).passwordEncoder(bCryptPasswordEncoder());
}
configure(AuthenticationManagerBuilder auth):身份認證接口
AuthenticationManager 的建造器,配置 AuthenticationManagerBuilder 會讓 Spring Security 自動構建一個 AuthenticationManager(該類的功能參考流程圖);
如果想要使用該功能你需要配置一個 UserDetailService 和 PasswordEncoder。UserDetailsService 用於在認證器中根據用戶傳過來的用戶名查找一個用戶, PasswordEncoder 用於密碼的加密與比對,我們存儲用戶密碼的時候用PasswordEncoder.encode() 加密存儲,在認證器里會調用 PasswordEncoder.matches() 方法進行密碼比對。
如果重寫了該方法,Spring Security 會啟用 DaoAuthenticationProvider 這個認證器,該認證就是先調用 UserDetailsService.loadUserByUsername 然后使用 PasswordEncoder.matches() 進行密碼比對,如果認證成功成功則返回一個 Authentication 對象。
configure(WebSecurity web):全局安全性配置
此方法用於配置影響全局安全性的配置,比如配置資源,設置調試模式,通過實現自定義防火牆定義拒絕請求,一般用於配置全局的某些通用事物,比如靜態資源等。
configure(HttpSecurity http):資源請求配置
這個方法是整個 Spring Security 的核心,也是最復雜的部分,通過案例簡單的說明一些常用配置,詳細說明會在權限控制中說明。
@Configuration
@EnableGlobalMethodSecurity(securedEnabled = true, prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.formLogin().loginPage("/login_page") // 自定義登錄頁
.passwordParameter("username") // 用戶名屬性名
.passwordParameter("password") // 密碼屬性名
.loginProcessingUrl("/sign_in") // 登錄請求路徑
.permitAll(); // 代表任意用戶可訪問
.cors().and()).csrf().disable()) // 禁用csrf(跨站請求偽造)
.authorizeRequests() // 過濾請求
.antMatchers(new String[]{"/upload/**", "/definition/**", "/activiti/**"}).permitAll() // 放行匹配請求
.anyRequest().authenticated().and() //除上面外的所有請求全部需要鑒權認證
.exceptionHandling().accessDeniedHandler((AccessDeniedHandler) this.accessDeniedHandler); // 異常解析器
http.logout().logoutUrl("/logout").logoutSuccessHandler(logoutSuccessHandler); // 退出登錄
}
}
權限控制
匹配規則
-
anyRequest():表示匹配所有的請求,一般情況都會使用此方法,設置全部內容都需要進行認證,比如
anyRequest().authenticated();
。 -
antMatcher(String... antPatterns): 表示匹配指定的請求,參數是不定向參數,每個參數是一個 ant 表達式,? 表示匹配一個字符,* 表示匹配 0~N 個字符,** 表示匹配 0~N 個目錄。例如
antMatchers( "/**/*.js").permitAll()
。 -
regexMatchers(String... regexPatterns):使用正則表達式進行匹配,與 antMatchers() 主要的區別就是參數,antMatchers()參數是 ant 表達式,二 regexMatchers() 參數是正則表達式。例如
.regexMatchers( ".+[.]js").permitAll()
。 -
mvcMatchers():適用於配置了 servletPath 的情況。.servletPath() 是 mvcMatchers() 返回值特有的方法,例如
.mvcMatchers( "demo").servletPath( "/bjsxt").permitAll()
等價於antMatchers( "/bjsxt/demo").permitAll()
。
訪問控制
-
permitAll():表示所匹配的 URL 任何人都允許訪問,也就是不需要認證,隨意訪問。
-
anonymous():表示可以匿名訪問匹配的 URL,只是設置為 anonymous() 的 URL 會執行 filter 鏈中,比如說瀏覽商城時。
-
authenticated():表示所匹配的 URL 都需要被認證才能訪問,也就是用戶登錄后可訪問。
-
denyAll():表示所匹配的 URL 都不允許被訪問。
-
rememberMe():只有被 remember me 的用戶才能訪問。
-
fullyAuthenticated():如果用戶不是被 remember me 的,才可以訪問。
角色控制
-
hasRole():用戶具備某個角色即可訪問資源,此方法會自動給傳入的字符串加上 ROLE_ 前綴,例如 hasRole("list"),表示擁有 ROLE_list 權限即可訪問。
-
hasAnyRole():用戶具備多個角色中的任意一個即可訪問資源,例如 hasAnyRole("admin", "save"),只要具備其中一個角色,即可訪問資源。
-
hasAuthority():類似於 hasRole,但是不會添加 ROLE_ 前綴,也就是說,使用 hasAuthority 更具有一致性,你不用考慮要不要加 ROLE_ 前綴,數據庫什么樣這里就是什么樣!
-
hasAnyAuthority():類似於 hasAnyRole,只是沒有前綴。
注解控制
當我們使用注解之前,必須通過 @EnableGlobalMethodSecurity(securedEnabled = true, prePostEnabled = true) 開啟注解。
-
@PreAuthorize:方法執行前進行權限檢查,允許使用 SpEL(pring表達式語言)。
-
@PostAuthorize:方法執行后進行權限檢查,基本不用。
-
@Secured:類似於 @PreAuthorize,但是不允許使用 SpEL(pring表達式語言)。
例如:
@Controller
public class HelloController {
// 只有當前登錄用戶名為 javaboy 的用戶才可以訪問該方法。
@PreAuthorize("principal.username.equals('javaboy')")
public String hello() {
return "hello";
}
// 表示訪問該方法的用戶必須具備 admin 角色
@PreAuthorize("hasRole('admin')")
public String admin() {
return "admin";
}
// 表示訪問該方法的 age 參數必須大於 98,否則請求不予通過。
@PreAuthorize("#age>98")
public String getAge(Integer age) {
return String.valueOf(age);
}
// 表示該方法的用戶必須具備 user 角色,但是注意 user 角色需要加上 ROLE_ 前綴。
@Secured({"ROLE_user"})
public String user() {
return "user";
}
}
請求認證
讓我們仔細分析認證過程:
-
當用戶發送登錄請求的時候,首先進入到 UsernamePasswordAuthenticationFilter 中進行校驗。
-
UsernamePasswordAuthenticationFilter 通過 attemptAuthentication 方法會獲取用戶的username以及password參數的信息,封裝為 UsernamePasswordAuthenticationToken 對象,最后會進入 AuthenticationManager 接口的實現類 ProviderManager 中。AuthenticationManager(認證管理器) 本身不包含驗證的邏輯,它的作用是用來管理 AuthenticationProvider。
-
進入 ProviderManager 類調用 authenticate() 方法,通過循環遍歷判斷它是否支持這種登錄方式,具體的登錄方式有表單登錄,qq登錄,微信登錄等。如果支持則會進入A uthenticationProvider 接口的抽象實現類 AbstractUserDetailsAuthenticationProvider 中調用 authenticate() 方法對用戶的身份進入校驗。
-
進入 AbstractUserDetailsAuthenticationProvider 的 authenticate方法之后,UserDetail 的 user 對象是否為空,如果為空,表示還沒有認證,就需要調用 DaoAuthenticationProvider 類的 retrieveUser 方法去獲取用戶的信息。
-
該擴展類的 retrieveUser 方法中調用 UserDetailsService 這個接口的實現類的 loadUserByUsername 方法去獲取用戶信息。如果需要自定義實現,則可以實現 UserDetails 接口,編寫自己的邏輯,從數據庫中獲取用戶密碼等權限信息返回。
-
獲取到用戶信息之后,返回到 AbstractUserDetailsAuthenticationProvider 類中調用 createSuccessAuthentication() 方法,通過 PasswordEncoder 對比用戶信息是否與 AuthenticationManager 一直,如果一直,則認證通過(setAuthenticated(true))。
-
認證成功后, AuthenticationManager 身份管理器返回一個被填充滿了信息的(包括上面提到的權限信息, 身份信息,細節信息,但密碼通常會被移除) Authentication 實例。
請求授權
讓我們仔細分析授權過程:
-
已認證用戶訪問受保護的 web 資源將被 SecurityFilterChain 中的 FilterSecurityInterceptor 的子類攔截。
-
FilterSecurityInterceptor 會從 SecurityMetadataSource 的 getAttributes() 方法,獲取要訪問當前資源所需要的權限。其實就是讀取訪問策略的抽象,而讀取的內容,其實就是我們配置的訪問規則。
-
FilterSecurityInterceptor 會調用 AccessDecisionManager(授權決策器)的 decide() 方法進行授權決策,若決策通過,則允許訪問資源,否則將禁止訪問。