Spring Security 快速瞭解

在Spring Security以前


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

Spring Security的相關結構


這裏你們能夠參考Spring Security的官方介紹文檔:spring-security-architecture
簡單的來講:node

  • Spring Security是一個單一的Filter,其具體的類型是FilterChainProxy,其是做爲@BeanApplicationContext中配置的。
  • 從容器的角度來看,Spring Security是一個單一的Filter,可是在其中有不少額外的Filter,每個都扮演着他們各自的角色,以下圖所示:
    git

  • Spring Security的身份驗證,主要由AuthenticationManager這個接口完成,其驗證的主要方法是authenticate()web

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()方法。spring

  • 在同一個程序中,一個ProviderManager經過委託一系列的AuthenticaitonProviders,以此來支支持多個不一樣的認證機制,若是ProviderManager沒法識別一個特定的Authentication實例類型,則會跳過它。
  • 不少時候,一個程序含有多個資源保護邏輯組,每個組都有他們獨有的AuthenticationManager,一般他們共享父級,那麼父級就成爲了了一個"global"資源,做爲全部provider的後背。
    框架

  • Spring Security提供了一些配置幫助咱們快速的開啓驗證功能,最經常使用的就是AuthenticationManagerBuiler,它在內存(in-memory)、JDBC、LDAP或者我的定製的UserDetailService這些領域都很擅長。ssh


使用Spring Security實現訪問和權限控制

注意:本後續代碼以SpringBoot爲框架實現,其DEMO Git: Spring-Security-Demoide

  • 主要經過重載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等方法實現。ui


經過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

相關文章
相關標籤/搜索