Spring Security權限控制

Spring Security官網 : https://projects.spring.io/spring-security/css

Spring Security簡介:html

Spring Security是一個可以爲基於Spring的企業應用系統提供聲明式的安全訪問控制解決方案的安全框架。它提供了一組能夠在Spring應用上下文中配置的Bean,充分利用了Spring IoC,DI(控制反轉Inversion of Control ,DI:Dependency Injection 依賴注入)和AOP(面向切面編程)功能,爲應用系統提供聲明式的安全訪問控制功能,減小了爲企業系統安全控制編寫大量重複代碼的工做。java

spring security認證:web

Basic:spring

客戶端以「 : 」鏈接用戶名和密碼後,再經BASE64加密經過Authorization請求頭髮送該密文至服務端進行驗證,每次請求都須要重複發送該密文。可見Basic認證過程簡單,安全性也低,存在泄露我的帳號信息以及其餘諸多安全問題。數據庫

Digest:編程

TTP協議規範的另外一種認證模式是Digest模式,在HTTP1.1時被提出來,它主要是爲了解決Basic模式安全問題,用於替代原來的Basic認證模式,Digest認證也是採用challenge/response認證模式,基本的認證流程比較相似,整個過程以下:瀏覽器

①瀏覽器發送http報文請求一個受保護的資源。緩存

②服務端的web容器將http響應報文的響應碼設爲401,響應頭部比Basic模式複雜,WWW-Authenticate: Digest realm=」myTomcat」,qop="auth",nonce="xxxxxxxxxxx",opaque="xxxxxxxx" 。其中qop的auth表示鑑別方式;nonce是隨機字符串;opaque服務端指定的值,客戶端須要原值返回。安全

③瀏覽器彈出對話框讓用戶輸入用戶名和密碼,瀏覽器對用戶名、密碼、nonce值、HTTP請求方法、被請求資源URI等組合後進行MD5運算,把計算獲得的摘要信息發送給服務端。請求頭部相似以下,Authorization: Digest username="xxxxx",realm="myTomcat",qop="auth",nonce="xxxxx",uri="xxxx",cnonce="xxxxxx",nc=00000001,response="xxxxxxxxx",opaque="xxxxxxxxx" 。其中username是用戶名;cnonce是客戶端生成的隨機字符串;nc是運行認證的次數;response就是最終計算獲得的摘要。

④服務端web容器獲取HTTP報文頭部相關認證信息,從中獲取到username,根據username獲取對應的密碼,一樣對用戶名、密碼、nonce值、HTTP請求方法、被請求資源URI等組合進行MD5運算,計算結果和response進行比較,若是匹配則認證成功並返回相關資源,不然再執行②,從新進行認證。

⑤之後每次訪問都要帶上認證頭部。

X.509:

X.509格式證書是被普遍使用的數字證書標準,是用於標誌通信各方身份信息的一系列數據。

LDAP:

和利用數據庫進行驗證相似,LDAP中也是利用登錄名和密碼進行驗證,LDAP中會定義一個屬性password,用來存放用戶密碼,而登錄名使用較多的都是mail地址。那怎麼樣才能正確的用LDAP進行身份驗證呢,下面是一個正確而又通用的步驟:

1. 從客戶端獲得登錄名和密碼。注意這裏的登錄名和密碼一開始並無被用到。

2. 先匿名綁定到LDAP服務器,若是LDAP服務器沒有啓用匿名綁定,通常會提供一個默認的用戶,用這個用戶進行綁定便可。

3. 以前輸入的登錄名在這裏就有用了,當上一步綁定成功之後,須要執行一個搜索,而filter就是用登錄名來構造,形如: "(|(uid=$login)(mail=$login))" ,這裏的login就是登錄名。搜索執行完畢後,須要對結果進行判斷,若是隻返回一個entry,這個就是包含了該用戶信息的entry,能夠獲得該entry的DN,後面使用。若是返回不止一個或者沒有返回,說明用戶名輸入有誤,應該退出驗證並返回錯誤信息。

4. 若是能進行到這一步,說明用相應的用戶,而上一步執行時獲得了用戶信息所在的entry的DN,這裏就須要用這個DN和第一步中獲得的password從新綁定LDAP服務器。

5. 執行完上一步,驗證的主要過程就結束了,若是能成功綁定,那麼就說明驗證成功,若是不行,則應該返回密碼錯誤的信息。

這5大步就是基於LDAP的一個 「兩次綁定」 驗證方法。

Form:

上面介紹的幾種模式都屬於HTTP協議規範範疇,因爲它的規範使得不少東西沒法自定義,例如登陸窗口、錯誤展現頁面。因此須要另一種模式提供更加靈活的認證,也就是基於Form的認證模式。

Form模式的認證流程以下:

①瀏覽器發送http報文請求一個受保護的資源。

②服務端的web容器判斷此uri爲受保護資源,因而將請求重定向到自定義的登錄頁面上,例如login.html頁面,能夠自定義登錄頁面的樣式,但要遵照的約定是表單的action必須以j_security_check結尾,即<form action='xxxxxx/j_security_check' method='POST'>。用戶名和密碼輸入框元素的name必須爲'j_username' 和'j_password'。

③瀏覽器展現自定義的登錄頁面讓用戶輸入用戶名和密碼,而後提交表單。

④服務端web容器獲取表單的用戶名和密碼,匹配此用戶名與密碼是否正確,是否有相應資源的權限,若是認證成功則返回相關資源,不然再執行②,從新進行認證。

⑤後面在同個會話期間的訪問都不用再進行認證,由於認證的結果已經保存在服務端的session裏面。     

Form模式跳出了HTTP規範提供了自定義的更加靈活的認證模式,但因爲Form模式屬於J2EE範疇,通常出如今java體系中,並且它也存在密碼明文傳輸安全問題。

 

 

Spring Security的幾個Filter

       Spring Security已經定義了一些Filter,無論實際應用中你用到了哪些,它們應當保持以下順序。

       (1)ChannelProcessingFilter,若是你訪問的channel錯了,那首先就會在channel之間進行跳轉,如http變爲https。

       (2)SecurityContextPersistenceFilter,這樣的話在一開始進行request的時候就能夠在SecurityContextHolder中創建一個SecurityContext,而後在請求結束的時候,任何對SecurityContext的改變均可以被copy到HttpSession。

       (3)ConcurrentSessionFilter,由於它須要使用SecurityContextHolder的功能,並且更新對應session的最後更新時間,以及經過SessionRegistry獲取當前的SessionInformation以檢查當前的session是否已通過期,過時則會調用LogoutHandler。

       (4)認證處理機制,如UsernamePasswordAuthenticationFilter,CasAuthenticationFilter,BasicAuthenticationFilter等,以致於SecurityContextHolder能夠被更新爲包含一個有效的Authentication請求。

       (5)SecurityContextHolderAwareRequestFilter,它將會把HttpServletRequest封裝成一個繼承自HttpServletRequestWrapper的SecurityContextHolderAwareRequestWrapper,同時使用SecurityContext實現了HttpServletRequest中與安全相關的方法。

       (6)JaasApiIntegrationFilter,若是SecurityContextHolder中擁有的Authentication是一個JaasAuthenticationToken,那麼該Filter將使用包含在JaasAuthenticationToken中的Subject繼續執行FilterChain。

       (7)RememberMeAuthenticationFilter,若是以前的認證處理機制沒有更新SecurityContextHolder,而且用戶請求包含了一個Remember-Me對應的cookie,那麼一個對應的Authentication將會設給SecurityContextHolder。

       (8)AnonymousAuthenticationFilter,若是以前的認證機制都沒有更新SecurityContextHolder擁有的Authentication,那麼一個AnonymousAuthenticationToken將會設給SecurityContextHolder。

       (9)ExceptionTransactionFilter,用於處理在FilterChain範圍內拋出的AccessDeniedException和AuthenticationException,並把它們轉換爲對應的Http錯誤碼返回或者對應的頁面。

       (10)FilterSecurityInterceptor,保護Web URI,而且在訪問被拒絕時拋出異常。

 

 與數據庫管理不一樣的是,Spring Security提供了一個實現了能夠緩存UserDetailService的實現類,這個類的名字是CachingUserDetailsService

該類的構造接收了一個用於真正加載UserDetails的UserDetailsService實現類,當須要加載UserDetails時,會首先從緩存中獲取。若是緩存中沒有對應的UserDetails,則使用UserDetailsService實現類進行加載,而後將加載後的結果存在緩存中。UserDetais與緩存的交互是經過UserCache實現的。CachingUserDetailsService默認有一個UserCache的空引用。

Spring的決策管理器,其接口爲AccessDecisionManager,抽象類爲AbstractAccessDecisionManager。而咱們要自定義決策管理器的話通常是繼承抽象類而不去直接實現接口。

在Spring中引入了投票器(AccessDecisionVoter)的概念,有無權限訪問的最終以爲權是由投票器來決定的,最多見的投票器爲RoleVoter,在RoleVoter中定義了權限的前綴,先看下Spring在RoleVoter中是怎麼處理受權的。

public int vote(Authentication authentication, Object object, Collection<ConfigAttribute> attributes) {  
    int result = ACCESS_ABSTAIN;  
    Collection<? extends GrantedAuthority> authorities = extractAuthorities(authentication);  
  
    for (ConfigAttribute attribute : attributes) {  
        if (this.supports(attribute)) {  
            result = ACCESS_DENIED;  
  
            // Attempt to find a matching granted authority  
            for (GrantedAuthority authority : authorities) {  
                if (attribute.getAttribute().equals(authority.getAuthority())) {  
                    return ACCESS_GRANTED;  
                }  
            }  
        }  
    }  
  
    return result;  
}  
  
Collection<? extends GrantedAuthority> extractAuthorities(Authentication authentication) {  
    return authentication.getAuthorities();  
}  

Authentication中是用戶及用戶權限信息,attributes是訪問資源須要的權限,而後循環判斷用戶是否有訪問資源須要的權限,若是有就返回ACCESS_GRANTED,通俗的說就是有權限。

Spring提供了3個決策管理器,至於這三個管理器是如何工做的請查看SpringSecurity源碼

AffirmativeBased 一票經過,只要有一個投票器經過就容許訪問

ConsensusBased 有一半以上投票器經過才容許訪問資源

UnanimousBased 全部投票器都經過才容許訪問

下面來實現一個簡單的自定義決策管理器,這個決策管理器並無使用投票器

public class DefaultAccessDecisionManager extends AbstractAccessDecisionManager {  
      
    public void decide( Authentication authentication, Object object,   
            Collection<ConfigAttribute> configAttributes)   
        throws AccessDeniedException, InsufficientAuthenticationException{  
          
        SysUser user = (SysUser)authentication.getPrincipal();  
        logger.info("訪問資源的用戶爲"+user.getUsername());  
          
        //若是訪問資源不須要任何權限則直接經過  
        if( configAttributes == null ) {  
            return ;  
        }  
          
        Iterator<ConfigAttribute> ite = configAttributes.iterator();  
        //遍歷configAttributes看用戶是否有訪問資源的權限  
        while( ite.hasNext()){  
              
            ConfigAttribute ca = ite.next();  
            String needRole = ((SecurityConfig)ca).getAttribute();  
              
            //ga 爲用戶所被賦予的權限。 needRole 爲訪問相應的資源應該具備的權限。  
            for( GrantedAuthority ga: authentication.getAuthorities()){  
                  
                if(needRole.trim().equals(ga.getAuthority().trim())){  
  
                    return;  
                }  
            }  
        }  
          
        throw new AccessDeniedException("");  
          
    }  
}  

 

能夠直接在Spring官網生成帶Web和Security的基於Maven管理的Spring Boot項目,

下載項目,建立 SpringSecurityConfig 類繼承 WebSecurityConfigurerAdapter 類,

SpringSecurityConfig類中設置放開靜態資源,設置Http請求的攔截,

@Configuration
@EnableWebSecurity//打開web支持
public class SpringSecurityConfig extends WebSecurityConfigurerAdapter {
/**
     * 設置http請求放開與攔截
     * @param http
     * @throws Exception
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .antMatchers("/").permitAll()//根目錄能夠直接訪問
                .anyRequest().authenticated()//其餘路徑不能直接訪問
                .and()
                .logout().permitAll()//註銷任何權限均可以訪問
                .and()
                .formLogin();//容許表單登陸
        http.csrf().disable();//關閉默認的csrf的認證
    }

    /**
     * 設置靜態資源放開
     * @param web
     * @throws Exception
     */
    @Override
    public void configure(WebSecurity web) throws Exception {
        web.ignoring().antMatchers("/js/**", "/css/**", "/images/**");
    }
}

 

簡單的登陸功能:

在  SpringSecurityConfig  類中

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
//        設置能夠登陸的用戶名和密碼
        auth.inMemoryAuthentication().withUser("admin").password("123456").roles("ADMIN");
    }

這樣在須要登陸的,沒有放行的功能中就需輸入以上用戶名和密碼才能夠進入。

這樣能夠設置多個用戶和不一樣的權限。

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
//        設置能夠登陸的用戶名和密碼
        auth.inMemoryAuthentication().withUser("admin").password("123456").roles("ADMIN");
        auth.inMemoryAuthentication().withUser("zhangsan").password("zhangsan").roles("ADMIN");
        auth.inMemoryAuthentication().withUser("demo").password("demo").roles("USER");

    }

給方法設置權限:

   @PreAuthorize("hasRole('ROLE_ADMIN')")
    @RequestMapping("/roleAuth")
    public String role() {
        return "admin auth";
    }

這樣User權限的用戶demo就不能訪問該方法,只有admin角色的用戶能夠訪問。

也能夠從數據庫中獲取用戶和權限信息:

定義MyUserService類,實現UserDetailsService接口,使用去提供的loadUserByUsername方法:

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        return null;
    }
    @Autowired
    private MyUserService myUserService;

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
//        設置能夠登陸的用戶名和密碼
//        auth.inMemoryAuthentication().withUser("admin").password("123456").roles("ADMIN");
//        auth.inMemoryAuthentication().withUser("zhangsan").password("zhangsan").roles("ADMIN");
//        auth.inMemoryAuthentication().withUser("demo").password("demo").roles("USER");
//
        auth.userDetailsService(myUserService)
                .passwordEncoder(new MyPasswordEncoder());//使用本身定義的驗證器

        //默認的Security數據庫驗證,若是使用,須要使用給定的數據庫表結構
        auth.jdbcAuthentication().usersByUsernameQuery("").authoritiesByUsernameQuery("").passwordEncoder(new MyPasswordEncoder());
    }

 

 

定義本身的密碼驗證規則:

public class MyPasswordEncoder implements PasswordEncoder {

    private final static String SALT = "123456";

    /**
     * 密碼加密
     * @param rawPassword
     * @return
     */
    @Override
    public String encode(CharSequence rawPassword) {
        Md5PasswordEncoder encoder = new Md5PasswordEncoder();
        return encoder.encodePassword(rawPassword.toString(), SALT);
    }

    /**
     * 密碼匹配
     * @param rawPassword
     * @param encodedPassword
     * @return
     */
    @Override
    public boolean matches(CharSequence rawPassword, String encodedPassword) {
        Md5PasswordEncoder encoder = new Md5PasswordEncoder();
        return encoder.isPasswordValid(encodedPassword, rawPassword.toString(), SALT);
    }
}

 

基於表達式的權限控制

    @PreAuthorize("#id<10 and principal.username.equals(#username) and #user.username.equals('abc')")
    @PostAuthorize("returnObject%2==0")//返回的值是偶數,對2取餘爲0,此註解用於對返回值進行過濾,在方法完成後進行權限檢查
    @RequestMapping("/test")
    public Integer test(Integer id, String username, User user) {
        // ...
        return id;
    }

    @PreFilter("filterObject%2==0")//傳入的過濾
    @PostFilter("filterObject%4==0")//返回的過濾
    @RequestMapping("/test2")
    public List<Integer> test2(List<Integer> idList) {
        // ...
        return idList;
    }

 

總結:

優勢:

1.提供了一套可用的安全框架

2.提供了不少用戶認證功能,實現相關接口便可,節約了大量工做

3.基於Spring,易於集成到Spring項目中去,封裝了許多方法

缺點:

1.配置文件過多,角色被「編碼」到配置文件和源文件中,RBAC不明顯

2.對於系統中的用戶、角色、權限沒有可操做的界面

3.大數據量的狀況下幾乎不可用

相關文章
相關標籤/搜索