Spring Security 快速了解


在Spring Security之前


我曾經使用 Interceptor 實現了一個簡單網站Demo的登錄攔截和Session處理工作,雖然能夠實現相應的功能,但是無疑Spring Security提供的配置方法更加簡單明確,能夠更好的保護Web應用。

Spring Security的相關結構


這里大家可以參考Spring Security的官方介紹文檔:spring-security-architecture
簡單的來說:

  • Spring Security是一個單一的Filter,其具體的類型是FilterChainProxy,其是作為@BeanApplicationContext中配置的。

  • 從容器的角度來看,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這個實例,AuthenticationProviderAuthenticationManager有一點像,但是含有一些額外的方法,來允許調用者來查詢是否支持該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的設置等等,比如我現在打印一條登錄信息,並將請求重定向到首頁
@Component
public 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 UserAuthProvider
starting supports
class java.lang.Class
-----------------------------------------------------------------------
This is UserAuthProvider
starting authenticate ... ...
Credentials:1234
Name:root
Class:class org.springframework.security.authentication.UsernamePasswordAuthenticationToken
Details:org.springframework.security.web.authentication.WebAuthenticationDetails@166c8: RemoteIpAddress: 0:0:0:0:0:0:0:1; SessionId: node04v47liue6knt1oghnzgiqb9dx0
Principal:root
-----------------------------------------------------------------------
root is loging , role is[]

  • 即成功登錄了,因為我們在authenticate()方法中直接聲明了一個Authentication的實例UsernamePasswordAuthenticationToken,並返回了,正如上面所說,當返回Authentication實例時,則默認為授權成功,而如果我們返回null,則說明無法判斷,不會登錄成功。

  • 此時我們再創建一個對象UserAfterProvider,其也實現AuthenticationProvider接口,並將UserAfterProviderUserAuthProviderauthenticate()返回值都設置為null,我們再次使用上面的數據進行登錄,控制台輸出如下:

This is UserAuthProvider
starting supports
class java.lang.Class
-----------------------------------------------------------------------
This is UserAuthProvider
starting authenticate ... ...
Credentials:1234
Name:root
Class:class org.springframework.security.authentication.UsernamePasswordAuthenticationToken
Details:org.springframework.security.web.authentication.WebAuthenticationDetails@43458: RemoteIpAddress: 0:0:0:0:0:0:0:1; SessionId: node01m47f3t6xq5a470fu07jaipzb0
Principal:root
-----------------------------------------------------------------------
This is UserAfterProvider
starting supports
class java.lang.Class
-----------------------------------------------------------------------
This is UserAfterProvider
starting authenticate ... ...
Credentials:1234
Name:root
Class:class org.springframework.security.authentication.UsernamePasswordAuthenticationToken
Details:org.springframework.security.web.authentication.WebAuthenticationDetails@43458: RemoteIpAddress: 0:0:0:0:0:0:0:1; SessionId: node01m47f3t6xq5a470fu07jaipzb0
Principal:root
-----------------------------------------------------------------------

  • 即兩個Porvider都進行了驗證,都沒有通過(返回null),說明所有加入AuthenticationManagerBuilder的驗證都會進行一遍,那么如果我們將其中一個Provider的authenticate()返回值還原為Authentication實例,再次登錄,則控制台會輸出如下結果:
This is UserAuthProvider
starting supports
class java.lang.Class
-----------------------------------------------------------------------
This is UserAuthProvider
starting authenticate ... ...
Credentials:1234
Name:root
Class:class org.springframework.security.authentication.UsernamePasswordAuthenticationToken
Details:org.springframework.security.web.authentication.WebAuthenticationDetails@166c8: RemoteIpAddress: 0:0:0:0:0:0:0:1; SessionId: node04v47liue6knt1oghnzgiqb9dx0
Principal:root
-----------------------------------------------------------------------
root is loging , role is[]
This is UserAuthProvider
starting supports
class java.lang.Class
-----------------------------------------------------------------------
This is UserAuthProvider
starting authenticate ... ...
Credentials:null
Name:root
Class:class org.springframework.security.authentication.UsernamePasswordAuthenticationToken
Details:org.springframework.security.web.authentication.WebAuthenticationDetails@166c8: RemoteIpAddress: 0:0:0:0:0:0:0:1; SessionId: node04v47liue6knt1oghnzgiqb9dx0
Principal:root
-----------------------------------------------------------------------
  • 因為我們重寫了AuthenticationSuccessHandler,所以驗證成功后悔重定向到/,而我Controller里對/又做了一次重定向到/index,所以發生了兩次驗證,而這次我們發現因為UserAuthProvider通過了,所以UserAfterProvider並沒有進行驗證,所以我們可以知道,只要有一個Provider通過了驗證我們就可以認為通過了驗證。

  • 因此,我們可以通過實現AuthenticationProvider來寫入自己的一些認證邏輯,甚至可以@Autowire相關Service來輔助實現。


我的博客即將搬運同步至騰訊雲+社區,邀請大家一同入駐:https://cloud.tencent.com/developer/support-plan?invite_code=1353hw8jzy7ee


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM