Spring Security 快速瞭解
在Spring Security之前
我曾經使用 Interceptor
實現了一個簡單網站Demo的登入攔截和Session處理工作,雖然能夠實現相應的功能,但是無疑Spring Security提供的配置方法更加簡單明確,能夠更好的保護Web應用。
Spring Security的相關結構
這裡大家可以參考Spring Security的官方介紹文件:spring-security-architecture
簡單的來說:
Spring Security是一個單一的
Filter
,其具體的型別是FilterChainProxy
,其是作為@Bean
在ApplicationContext
中配置的。從容器的角度來看,Spring Security是一個單一的Filter,但是在其中有很多額外的Filter,每一個都扮演著他們各自的角色,如下圖所示:
Spring Security的身份驗證,主要由
AuthenticationManager
這個介面完成,其驗證的主要方法是authenticate()
public interface AuthenticationManager { Authentication authenticate(Authentication authentication) throws AuthenticationException; }
-
該方法可以完成三件事:
如果它可以驗證輸入代表一個有效的主體,就返回一個
Authentication
(通常包含authenticated=true
)如果它可以驗證輸入代表一個無效的主體,就throw一個
AuthenticationException
如果它不能決斷,就返回
null
最常用的
AuthicationManager
的實現是ProviderManager
,它將其委託給AuthticationProvider
這個例項,AuthenticationProvider
和AuthenticationManager
有一點像,但是含有一些額外的方法,來允許呼叫者來查詢是否支援該Authenticaion
形式。
public interface AuthenticationProvider { Authentication authenticate(Authentication authentication) throws AuthenticationException; boolean supports(Class> authentication); }
supports()
方法中的Class>
引數是Class extends Authentication>
,它只會詢問其是否支援傳遞給authenticate()
方法。
在同一個程式中,一個
ProviderManager
透過委託一系列的AuthenticaitonProviders
,以此來支支援多個不同的認證機制,如果ProviderManager
無法識別一個特定的Authentication
例項型別,則會跳過它。很多時候,一個程式含有多個資源保護邏輯組,每一個組都有他們獨有的
AuthenticationManager
,通常他們共享父級,那麼父級就成為了了一個"global"資源
,作為所有provider
的後背。Spring Security提供了一些配置幫助我們快速的開啟驗證功能,最常用的就是
AuthenticationManagerBuiler
,它在記憶體(in-memory)、JDBC、LDAP或者個人定製的UserDetailService
這些領域都很擅長。
使用Spring Security實現訪問和許可權控制
注意:本後續程式碼以SpringBoot為框架實現,其DEMO Git: Spring-Security-Demo
主要透過過載WebSecurityConfigurerAdapter的configure方法進行訪問和許可權控制
方法 | 描述 |
---|---|
configure(WebSecurity) | 透過過載,配置Spring Security的Filter鏈 |
configure(HttpSecurity) | 透過過載,配置如何攔截器保護請求 |
configure(AuthenticationManagerBuilder) | 透過過載,配置user-detail服務 |
我們重寫如下方法:
@Override protected void configure(HttpSecurity http) throws Exception { http .authorizeRequests() .antMatchers("/index").hasAnyAuthority("ROLE_USER","ROLE_ADMIN") .antMatchers("/oss").hasAuthority("ROLE_ADMIN") .antMatchers(HttpMethod.GET, "/login").permitAll() .anyRequest().authenticated() .and() .formLogin() .loginPage("/login") .permitAll()//.successHandler(successHandler) .and() .logout() .logoutSuccessUrl("/") .permitAll(); } @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.inMemoryAuthentication().passwordEncoder(new BCryptPasswordEncoder()) .withUser("root").password(new BCryptPasswordEncoder().encode("root")).roles("USER","ADMIN").and() .withUser("normal").password(new BCryptPasswordEncoder().encode("normal")).roles("USER"); //auth.authenticationProvider(userProvider); //auth.authenticationProvider(afterProvider); }
- 透過`antMatchers()`進行URL匹配,再進行相應的處理,比如見上程式碼,我們將**/index**和**/oss**兩個連結進行了攔截,並分別要求擁有`ROLE_USER`或`ROLE_ADMIN`、`ROLE_ADMIN`這兩個身份才能訪問。- `anyRequest().authenticated()`指其他請求都會需要驗證- `formLogin()`使其有了登入頁面,如果沒有後面的`loginPage()`,則會預設生成一個Spring Security的頁面,而後面註釋掉的`successHandler`則是後續會講到的。- `permitAll()`則表示當前連線不需要認證。- `logout()`會攔截所以的**logout**請求,完成登出操作,`logoutSuccessUrl()`則是登出後的重定向地址。- `and()`在其中起連線作用。
-
一些常用的保護路徑配置方法
authenticated() : 允許認證過的使用者訪問
denyAll() : 無條件拒絕所有訪問
fullyAuthenticated() : 如果使用者是完整認證(不透過Remeber me)訪問
hasIpAdress(String) : 如果騎牛來自給定IP地址,就可以訪問
hasAnyAuthority(String ...) : 如果用於具備任意一個給定角色,就可以訪問
hasAnthority(String) : 如果使用者具備給定角色,就可以訪問
permitAl() : 無條件允許方法
remeberMe():如果使用者是透過Remeber-me認證的,就可以訪問
另外,與Autheority對應有一個Role,兩者是一個概念,Autheority必須以“ROLE_”開頭,而Role不需要,見上程式碼。
則此時我們的root賬號既能夠訪問index也能夠訪問oss,而normal賬號只能訪問index,不能訪問oss,如果訪問oss會出現:
There was an unexpected error (type=Forbidden, status=403).上面我們透過過載configure(AuthenticationManagerBuilder auth)生成了兩個記憶體使用者root和normal,我們也可以透過jdbc等方法實現。
透過AuthenticationSuccessHandler實現認證成功後的處理
透過實現AuthenticationSuccessHandler介面,我們可以在驗證成功後執行相應的程式碼,比如
Token
的設定等等,比如我現在列印一條登入資訊,並將請求重定向到首頁
@Componentpublic class SuccessHandler implements AuthenticationSuccessHandler{ @Override public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException { System.out.println(authentication.getName()+" is loging , role is"+authentication.getAuthorities()); response.sendRedirect("/"); }
並將其新增到
formLogin()
後,即:
.formLogin() .loginPage("/login") .permitAll().successHandler(successHandler)
再次登入root賬戶,則會在控制檯看到: root is loging , role is[ROLE_ADMIN, ROLE_USER]
透過AuthenticationProvider實現個性化認證
我們建立一個
UserAuthProvider
,並讓其實現AuthenticationProvider
介面:
@Override public Authentication authenticate(Authentication authentication) throws AuthenticationException { System.out.println("-----------------------------------------------------------------------"); System.out.println("This is UserAuthProvider"); System.out.println("starting authenticate ... ..."); System.out.println("Credentials:"+authentication.getCredentials()); System.out.println("Name:"+authentication.getName()); System.out.println("Class:"+authentication.getClass()); System.out.println("Details:"+authentication.getDetails()); System.out.println("Principal:"+authentication.getPrincipal()); System.out.println("-----------------------------------------------------------------------"); UsernamePasswordAuthenticationToken auth=new UsernamePasswordAuthenticationToken(authentication.getPrincipal(), authentication.getCredentials()); return auth; } @Override public boolean supports(Class> authentication) { System.out.println("This is UserAuthProvider"); System.out.println("starting supports"); System.out.println(authentication.getClass()); return false; }
同時,我們註釋掉以前的
auth.inMemoryAuthentication()
,將UserAuthProvider加入到AuthenticationManagerBuilder
中,即:
@Override protected void configure(AuthenticationManagerBuilder auth) throws Exception {// auth.inMemoryAuthentication().passwordEncoder(new BCryptPasswordEncoder())// .withUser("root").password(new BCryptPasswordEncoder().encode("root")).roles("USER","ADMIN").and()// .withUser("normal").password(new BCryptPasswordEncoder().encode("normal")).roles("USER"); auth.authenticationProvider(userProvider); auth.authenticationProvider(afterProvider); }
此時我們再次登入,會發現控制檯會輸出
This is UserAuthProvider starting supports java.lang. Class
其原因是我們重寫的
supports()
方法,永遠返回false,而返回false時,即不會再呼叫authenticate()
進行認證操作(正如上面所介紹的),我們將supports()
的返回值變成true,再次登入(username: root password: 1234),則控制檯會輸出
This is UserAuthProviderstarting supportsclass java.lang.Class-----------------------------------------------------------------------This is UserAuthProviderstarting authenticate ... ...Credentials:1234Name:rootClass:class org.springframework.security.authentication.UsernamePasswordAuthenticationTokenDetails:org.springframework.security.web.authentication.WebAuthenticationDetails@166c8: RemoteIpAddress: 0:0:0:0:0:0:0:1; SessionId: node04v47liue6knt1oghnzgiqb9dx0Principal:root-----------------------------------------------------------------------root is loging , role is[]
即成功登入了,因為我們在
authenticate()
方法中直接宣告瞭一個Authentication
的例項UsernamePasswordAuthenticationToken
,並返回了,正如上面所說,當返回Authentication
例項時,則預設為授權成功,而如果我們返回null
,則說明無法判斷,不會登入成功。此時我們再建立一個物件
UserAfterProvider
,其也實現AuthenticationProvider
介面,並將UserAfterProvider
和UserAuthProvider
的authenticate()
返回值都設定為null
,我們再次使用上面的資料進行登入,控制檯輸出如下:
This is UserAuthProviderstarting supportsclass java.lang.Class-----------------------------------------------------------------------This is UserAuthProviderstarting authenticate ... ...Credentials:1234Name:rootClass:class org.springframework.security.authentication.UsernamePasswordAuthenticationTokenDetails:org.springframework.security.web.authentication.WebAuthenticationDetails@43458: RemoteIpAddress: 0:0:0:0:0:0:0:1; SessionId: node01m47f3t6xq5a470fu07jaipzb0Principal:root-----------------------------------------------------------------------This is UserAfterProviderstarting supportsclass java.lang.Class-----------------------------------------------------------------------This is UserAfterProviderstarting authenticate ... ...Credentials:1234Name:rootClass:class org.springframework.security.authentication.UsernamePasswordAuthenticationTokenDetails:org.springframework.security.web.authentication.WebAuthenticationDetails@43458: RemoteIpAddress: 0:0:0:0:0:0:0:1; SessionId: node01m47f3t6xq5a470fu07jaipzb0Principal:root-----------------------------------------------------------------------
即兩個Porvider都進行了驗證,都沒有透過(返回null),說明所有加入
AuthenticationManagerBuilder
的驗證都會進行一遍,那麼如果我們將其中一個Provider的authenticate()
返回值還原為Authentication
例項,再次登入,則控制檯會輸出如下結果:
This is UserAuthProviderstarting supportsclass java.lang.Class-----------------------------------------------------------------------This is UserAuthProviderstarting authenticate ... ...Credentials:1234Name:rootClass:class org.springframework.security.authentication.UsernamePasswordAuthenticationTokenDetails:org.springframework.security.web.authentication.WebAuthenticationDetails@166c8: RemoteIpAddress: 0:0:0:0:0:0:0:1; SessionId: node04v47liue6knt1oghnzgiqb9dx0Principal:root-----------------------------------------------------------------------root is loging , role is[]This is UserAuthProviderstarting supportsclass java.lang.Class-----------------------------------------------------------------------This is UserAuthProviderstarting authenticate ... ...Credentials:nullName:rootClass:class org.springframework.security.authentication.UsernamePasswordAuthenticationTokenDetails:org.springframework.security.web.authentication.WebAuthenticationDetails@166c8: RemoteIpAddress: 0:0:0:0:0:0:0:1; SessionId: node04v47liue6knt1oghnzgiqb9dx0Principal:root-----------------------------------------------------------------------
因為我們重寫了
AuthenticationSuccessHandler
,所以驗證成功後悔重定向到/,而我Controller裡對/又做了一次重定向到/index,所以發生了兩次驗證,而這次我們發現因為UserAuthProvider
透過了,所以UserAfterProvider
並沒有進行驗證,所以我們可以知道,只要有一個Provider透過了驗證我們就可以認為透過了驗證。因此,我們可以透過實現
AuthenticationProvider
來寫入自己的一些認證邏輯,甚至可以@Autowire相關Service來輔助實現。
作者:
出處:http://www.cnblogs.com/rekent/
本文版權歸作者和部落格園共有,歡迎轉載、點贊,但未經作者同意必須保留此段申明,且在文章頁面明顯位置給出原文連結,否則保留追究法律責任的權利。
來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/2768/viewspace-2806627/,如需轉載,請註明出處,否則將追究法律責任。
相關文章
- Spring Security 快速入門Spring
- 【Spring Security】1.快速入門Spring
- Spring Security詳解Spring
- Spring Boot Security 詳解Spring Boot
- 【詳解】Spring Security 之 SecurityContextSpringContext
- 快速瞭解jQueryjQuery
- 快速瞭解XMLXML
- Spring Security(三)--核心配置解讀Spring
- Spring SecuritySpring
- Spring Boot —— Spring SecuritySpring Boot
- Spring Security原始碼分析八:Spring Security 退出Spring原始碼
- Spring Security原始碼分析九:Spring Security Session管理Spring原始碼Session
- Spring Security 實戰乾貨:圖解Spring Security中的Servlet過濾器體系Spring圖解Servlet過濾器
- Spring 指南(瞭解REST)SpringREST
- Spring Boot整合Spring SecuritySpring Boot
- Spring Security(8)Spring
- Spring Security(6)Spring
- Spring Security(7)Spring
- Spring Security + JWTSpringJWT
- spring security(一)Spring
- 初探Spring SecuritySpring
- Spring Security 上Spring
- Spring Security(二)Spring
- Spring Boot SecuritySpring Boot
- 如何從Spring Security 5遷移到Spring Security 6/Spring Boot 3Spring Boot
- Spring security(四)-spring boot +spring security簡訊認證+redis整合Spring BootRedis
- 快速瞭解 React Hooks 原理ReactHook
- 帶你快速瞭解HTMLHTML
- SpringBoot整合Spring SecuritySpring Boot
- [譯]Spring Security ArchitectureSpring
- Spring Security進階Spring
- Spring Security OAuth 2.0SpringOAuth
- 快速瞭解什麼是MVCMVC
- 快速瞭解雲原生架構架構
- 五分鐘快速瞭解Less
- 快速瞭解雲端計算
- 5分鐘快速瞭解 RedisRedis
- 30分鐘快速瞭解webpackWeb