Spring Security_1

  • Spring Security介紹
  • 使用Servlet規(guī)范中的Filter保護Web應(yīng)用

9.1 Spring Security簡介

Spring Security

  • 基于Spring的應(yīng)用程序提供聲明式安全保護的安全性框架

  • 提供了完整的安全性解決方案,它能夠在Web請求級別和方法調(diào)用級別處理身份認證和授權(quán)。

  • 充分利用了依賴注入(dependency injection,DI)和面向切面的技術(shù)

最初,Spring Security被稱為Acegi Security。Acegi是一個強大的安全框架,但是它存在一個嚴 重的問題:那就是需要大量的XML配置。

9.1.1 Spring Security的模塊

表9.1 Spring Security被分成了11個模塊

模 塊 描 述
ACL 支持通過訪問控制列表(access control list,ACL)為域?qū)ο筇峁┌踩?/td>
切面(Aspects) 一個很小的模塊,當使用Spring Security注解時,會使用基于AspectJ的切面,而不是使用標準的Spring AOP
CAS客戶端 (CASClient) 提供與Jasig的中心認證服務(wù)(Central Authentication Service,CAS)進行集成的功能
配置(Configuration) 包含通過XML和Java配置Spring Security的功能支持
核心(Core) 提供Spring Security基本庫
加密(Cryptography) 提供了加密和密碼編碼的功能
LDAP 支持基于LDAP進行認證
OpenID 支持使用OpenID進行集中式認證
Remoting 提供了對Spring Remoting的支持
標簽庫(Tag Library) Spring Security的JSP標簽庫
Web 提供了Spring Security基于Filter的Web安全性支持

應(yīng)用程序的類路徑下至少要包含Core和Configuration這兩個模塊。Spring Security經(jīng)常被用于保 護Web應(yīng)用

9.1.2 過濾Web請求

Spring Security借助一系列Servlet Filter來提供各種安全性功能。只需配置一個Filter就可以了

DelegatingFilterProxy是一個特殊的Servlet Filter,它本身所做的工作并不多。只是將 工作委托給一個javax.servlet.Filter實現(xiàn)類,這個實現(xiàn)類作為一個<bean>注冊在 Spring應(yīng)用的上下文中,如

<!--spring security配置-->
    <filter>
        <filter-name>springSecurityFilterChain</filter-name>
        <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
        <async-supported>true</async-supported>
    </filter>
import org.springframework.web.servlet.support.AbstractAnnotationConfigDispatcherServletInitializer;

public class SpittrWebAppInitializer
        extends AbstractAnnotationConfigDispatcherServletInitializer {
}

AbstractSecurityWebApplicationInitializer實現(xiàn)了WebApplication?Initializer,因此Spring會發(fā)現(xiàn)它,并用它在Web容器中注冊DelegatingFilterProxy。

盡管我們可以重載它的appendFilters()或insertFilters()方法來注冊自己選擇的Filter,但是要注冊 DelegatingFilterProxy的話,我們并不需要重載任何方法。

不管我們通過web.xml還是通過AbstractSecurityWebApplicationInitializer的子類來配置DelegatingFilterProxy,它都會攔截發(fā)往應(yīng)用中的請求,并將請求委托給ID為springSecurityFilterChain bean。

springSecurityFilterChain本身是另一個特殊的Filter,它也被稱為FilterChainProxy。它可以鏈接任意一個或多個其他的Filter。Spring Security依賴一系列Servlet Filter來提供不同的安全特性。但是,你幾乎不需要知道這些細節(jié),因為你不需要顯式聲明springSecurityFilterChain以及它所鏈接在一起的其他Filter。當我們啟用Web 安全性的時候,會自動創(chuàng)建這些Filter。

9.1.3 編寫簡單的安全性配置

啟用Web安全性功能的最簡單配置

import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.annotation.web.servlet.configuration.EnableWebMvcSecurity;

@Configuration
@EnableWebMvcSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
}

@EnableWebMvcSecurity注解還配置了一個Spring MVC參數(shù)解析解析器(argument resolver),這樣的話處理器方法就能夠通過帶有@AuthenticationPrincipal注解的參數(shù)獲得認證用戶的principal(或username)。

它同時還配置了一個bean,在使用Spring表單綁定標簽庫來定義表單時,這個bean會自動添加一個隱藏的跨站請求偽造(cross-site request forgery,CSRF)token輸入域。

看起來似乎并沒有做太多的事情,但程序清單9.1和9.2中的配置類會給應(yīng)用產(chǎn)生很大的影響。 其中任何一種配置都會將應(yīng)用嚴格鎖定,導致沒有人能夠進入該系統(tǒng)了! 盡管不是嚴格要求的,但我們可能希望指定Web安全的細節(jié),這要通過重 載WebSecurityConfigurerAdapter中的一個或多個方法來實現(xiàn)。我們可以通過重 載WebSecurityConfigurerAdapter的三個configure()方法來配置Web安全性,這個 過程中會使用傳遞進來的參數(shù)設(shè)置行為。表9.2描述了這三個方法。

表9.2 重載WebSecurityConfigurerAdapter的configure()方法

方 法 描 述
configure(WebSecurity) 通過重載,配置Spring Security的Filter鏈
configure(HttpSecurity) 通過重載,配置如何通過攔截器保護請求
configure(AuthenticationManagerBuilder) 通過重載,配置user-detail服務(wù)

默認的configure(HttpSecurity)實際上等同于如下所示:

protected void configure(HttpSecurity httpSecurity) throws Exception {
    httpSecurity.authorizeRequests()
            .anyRequest()
            .authenticated()
            .and()
            .formLogin()
            .and()
            .httpBasic();
}

通過調(diào)用authorizeRequests()和anyRequest().authenticated()就會要求所有進入應(yīng)用的HTTP請求都要進行認證。

它也配置Spring Security支持基于表單的登錄以及HTTP Basic方式的認證。

同時,因為我們沒有重載configure(AuthenticationManagerBuilder)方法,所以沒有用戶存儲支撐認證過程。沒有用戶存儲,實際上就等于沒有用戶。所以,在這里所有的請求都需要認證,但是沒有人能夠登錄成功。

為了讓Spring Security滿足我們應(yīng)用的需求,還需要再添加一點配置。具體來講,我們需要:

  • 配置用戶存儲;

  • 指定哪些請求需要認證,哪些請求不需要認證,以及所需要的權(quán)限;

  • 提供一個自定義的登錄頁面,替代原來簡單的默認登錄頁。

除了Spring Security的這些功能,我們可能還希望基于安全限制,有選擇性地在Web視圖上顯示特定的內(nèi)容。

9.2 選擇查詢用戶詳細信息的服務(wù)

我們所需要的是用戶存儲,也就是用戶名、密碼以及其他信息存儲的地方,在進行認證決策的時候,會對其進行檢索。

Spring Security非常靈活,能夠基于各種數(shù)據(jù)存儲來認證用戶。它內(nèi)置了多種常見的用戶存儲場景,如內(nèi)存、關(guān)系型數(shù)據(jù)庫以及LDAP。但我們也可以編寫并插入自定義的用戶存儲實現(xiàn)。

借助Spring Security的Java配置,我們能夠很容易地配置一個或多個數(shù)據(jù)存儲方案。

那我們就從最簡單的開始:在內(nèi)存中維護用戶存儲。

9.2.1 使用基于內(nèi)存的用戶存儲

因為我們的安全配置類擴展了WebSecurityConfigurerAdapter,因此配置用戶存儲的最簡單方式就是重載configure()方法,并以AuthenticationManagerBuilder作為傳入?yún)?shù)。AuthenticationManagerBuilder有多個方法可以用來配置Spring Security對認證的支持。

通過inMemoryAuthentication()方法,我們可以啟用、配置并任意填充基于內(nèi)存的用戶存儲。

例如,在如程序清單9.3中,SecurityConfig重載了configure()方法,并使用兩個用戶 來配置內(nèi)存用戶存儲。

程序清單9.3 配置Spring Security使用內(nèi)存用戶存儲

import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.annotation.web.servlet.configuration.EnableWebMvcSecurity;

@Configuration
@EnableWebMvcSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        // 啟用內(nèi)存用戶儲存
        auth.inMemoryAuthentication()
                .withUser("user").password("password").roles("USER").and()
                .withUser("admin").password("password").roles("USER","ADMIN");
    }
    
  }

我們可以看到,configure()方法中的AuthenticationManagerBuilder使用構(gòu)造者風格的接口來構(gòu)建認證配置。

通過簡單地調(diào)用inMemoryAuthentication()就能啟用內(nèi)存用戶存儲。但是我們還需要有一些用戶,否則的話,這和沒有用戶并沒有什么區(qū)別。

因此,我們需要調(diào)用withUser()方法為內(nèi)存用戶存儲添加新的用戶,這個方法的參數(shù)是username。withUser()方法返回的是UserDetailsManagerConfigurer.UserDetailsBuilder,這個對象提供了多個進一步配置用戶的方法,包括設(shè)置用戶密碼的password()方法以及為給定用戶授予一個或多個角色權(quán)限的roles()方法。

在程序清單9.3中,我們添加了兩個用戶,“user”和“admin”,密碼均為“password”。“user”用戶具有USER角色,而“admin”用戶具有ADMIN和USER兩個角色。我們可以看到,and()方法能夠?qū)⒍鄠€用戶的配置連接起來

除了password()、roles()和and()方法以外,還有其他的幾個方法可以用來配置內(nèi)存用 戶存儲中的用戶信息。表9.3描述了UserDetailsManagerConfigurer.UserDetailsBuilder對象所有可用的方法。 需要注意的是,roles()方法是authorities()方法的簡寫形式。roles()方法所給定的值都會添加一個“ROLE_”前綴,并將其作為權(quán)限授予給用戶。實際上,如下的用戶配置與程序清單9.3是等價的

 auth.inMemoryAuthentication()
                .withUser("user").password("password").roles("ROLE_USER").and()
                .withUser("admin").password("password").roles("ROLE_USER","ROLE_ADMIN");

表9.3 配置用戶詳細信息的方法

方 法 描 述
accountExpired(boolean) 定義賬號是否已經(jīng)過期
accountLocked(boolean) 定義賬號是否已經(jīng)鎖定
and() 用來連接配置
authorities(GrantedAuthority...) 授予某個用戶一項或多項權(quán)限
authorities(List<? extends GrantedAuthority>) 授予某個用戶一項或多項權(quán)限
authorities(String...) 授予某個用戶一項或多項權(quán)限
credentialsExpired(boolean) 定義憑證是否已經(jīng)過期
disabled(boolean) 定義賬號是否已被禁用
password(String) 定義用戶的密碼
roles(String...) 授予某個用戶一項或多項角色

對于調(diào)試和開發(fā)人員測試來講,基于內(nèi)存的用戶存儲是很有用的,但是對于生產(chǎn)級別的應(yīng)用來講,這就不是最理想的可選方案了。為了用于生產(chǎn)環(huán)境,通常最好將用戶數(shù)據(jù)保存在某種類型的數(shù)據(jù)庫之中。

9.2.2 基于數(shù)據(jù)庫表進行認證

用戶數(shù)據(jù)通常會存儲在關(guān)系型數(shù)據(jù)庫中,并通過JDBC進行訪問。為了配置Spring Security使用以JDBC為支撐的用戶存儲,我們可以使用jdbcAuthentication()方法,所需的最少配置如下所示:

@Autowired
DataSource dataSource;

/**
 *  使用jdbc進行訪問
 * @param auth
 * @throws Exception
 */
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
    // 啟用內(nèi)存用戶儲存
    auth.jdbcAuthentication()
            .dataSource(dataSource);
}

我們必須要配置的只是一個DataSource,這樣的話,就能訪問關(guān)系型數(shù)據(jù)庫了。在這里,DataSource是通過自動裝配的技巧得到的。 重寫默認的用戶查詢功能盡管默認的最少配置能夠讓一切運轉(zhuǎn)起來,但是它對我們的數(shù)據(jù)庫模式有一些要求。它預期存在某些存儲用戶數(shù)據(jù)的表。更具體來說,下面的代碼片段來源于Spring Security內(nèi)部,這塊代碼展現(xiàn)了當查找用戶信息時所執(zhí)行的SQL查詢語句:

/**
 * 在第一個查詢中,我們獲取了用戶的用戶名、密碼以及是否啟用的信息,這些信息會用來進行用戶認證。
 * 接下來的查詢查找了用戶所授予的權(quán)限,用來進行鑒權(quán),
 * 最后一個查詢中,查找了用戶作為群組的成員所授予的權(quán)限
 */
public static final String DEF_USERS_BY_USERNAME_QUERY = "select username, password, enabled from users where username = ?";
public static final String DEF_AUTHORITIES_BY_USERNAME_QUERY = "select username, authority from authorities where username = ?";
public static final String DEF_GROUP_AUTHORITIES_BY_USERNAME_QUERY = "select g.id, g.group_name, ga.authority " +
 "from group g, group_members gm, group_authorities ga " +
 "where gm.username = ? and g.id = ga.group_id and g.id = gm.group_id"; 

當數(shù)據(jù)庫與上面所述并不一致,那么你就會希望在查詢上有更多的控制權(quán)。如果是這樣的話,我們可以按照如下的方式配置自己的查詢:

/**
 *  使用jdbc進行訪問,只重寫了認證和基本權(quán)限的查詢語句
 * @param auth
 * @throws Exception
 */
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
 // 啟用內(nèi)存用戶儲存
 auth.jdbcAuthentication()
 .dataSource(dataSource)
 .usersByUsernameQuery("select username, password, true from Spitter where username = ?")
 .authoritiesByUsernameQuery("selcet username, `ROLE_USER` from Spitter where username= ?");
}

在本例中,我們只重寫了認證和基本權(quán)限的查詢語句,但是通過調(diào)用group?AuthoritiesByUsername()方法,我們也能夠?qū)⑷航M權(quán)限重寫為自定義的查詢語句。

將默認的SQL查詢替換為自定義的設(shè)計時,很重要的一點就是要遵循查詢的基本協(xié)議。所有查詢都將用戶名作為唯一的參數(shù)。認證查詢會選取用戶名、密碼以及啟用狀態(tài)信息。權(quán)限查詢會選取零行或多行包含該用戶名及其權(quán)限信息的數(shù)據(jù)。

群組權(quán)限查詢會選取零行或多行數(shù)據(jù),每行數(shù)據(jù)中都會包含群組ID、群組名稱以及權(quán)限。

使用轉(zhuǎn)碼后的密碼看一下上面的認證查詢,它會預期用戶密碼存儲在了數(shù)據(jù)庫之中。這里唯一的問題在于如果密碼明文存儲的話,會很容易受到黑客的竊取。但是,如果數(shù)據(jù)庫中的密碼進行了轉(zhuǎn)碼的話,那么認證就會失敗,因為它與用戶提交的明文密碼并不匹配。

為了解決這個問題,我們需要借助passwordEncoder()方法指定一個密碼轉(zhuǎn)碼器(encoder):

protected void configure(AuthenticationManagerBuilder auth) throws Exception {
 // 啟用內(nèi)存用戶儲存
 auth.jdbcAuthentication()
 .dataSource(dataSource)
 .usersByUsernameQuery("select username, password, true from Spitter where username = ?")
 .authoritiesByUsernameQuery("selcet username, 'ROLE_USER' from Spitter where username= ?")
 .passwordEncoder(new StandardPasswordEncoder("33"));
}

passwordEncoder()方法可以接受Spring Security中PasswordEncoder接口的任意實現(xiàn)。 Spring Security的加密模塊包括了三個這樣的實現(xiàn):BCryptPasswordEncoder、NoOpPasswordEncoder和StandardPasswordEncoder。 上述的代碼中使用了StandardPasswordEncoder,但是如果內(nèi)置的實現(xiàn)無法滿足需求時,你可以提供自定義的實現(xiàn)。PasswordEncoder接口非常簡單:

public interface PasswordEncoder {
 String encoude(CharSequence rawPassworc);
 boolean matches(CharSequence rawPassword, String encodeedPassword);
}

不管你使用哪一個密碼轉(zhuǎn)碼器,數(shù)據(jù)庫中的密碼是永遠不會解碼的。所采取的策略與之相反,用戶在登錄時輸入的密碼會按照相同的算法進行轉(zhuǎn)碼,然后再與數(shù)據(jù)庫中已經(jīng)轉(zhuǎn)碼過的密碼進行對比。這個對比是在PasswordEncoder的matches()方法中進行的。

9.2.3 基于LDAP進行認證

為了讓Spring Security使用基于LDAP的認證,我們可以使用ldapAuthentication()方法。 這個方法在功能上類似于jdbcAuthentication(),只不過是LDAP版本。如下的configure()方法展現(xiàn)了LDAP認證的簡單配置:

@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
 // 啟用內(nèi)存用戶儲存
 auth.ldapAuthentication()
 .userSearchFilter("(uid={0})")
 .groupSearchFilter("(member={0})");
}

方法userSearchFilter()和groupSearchFilter()用來為基礎(chǔ)LDAP查詢提供過濾條件,它們分別用于搜索用戶和組。默認情況下,對于用戶和組的基礎(chǔ)查詢都是空的,也就是表明搜索會在LDAP層級結(jié)構(gòu)的根開始。但是我們可以通過指定查詢基礎(chǔ)來改變這個默認行為:

@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
 // 啟用內(nèi)存用戶儲存
 auth.ldapAuthentication()
 .userSearchBase("ou=people")
 .userSearchFilter("(uid={0})")
 .groupSearchBase("ou=groups")
 .groupSearchFilter("(member={0})");
}

userSearchBase()屬性為查找用戶提供了基礎(chǔ)查詢。同樣,groupSearchBase()為查找組指定了基礎(chǔ)查詢。我們聲明用戶應(yīng)該在名為people的組織單元下搜索而不是從根開始。而組應(yīng)該在名為groups的組織單元下搜索。

配置密碼比對基于LDAP進行認證的默認策略是進行綁定操作,直接通過LDAP服務(wù)器認證用戶。另一種可選的方式是進行比對操作。這涉及將輸入的密碼發(fā)送到LDAP目錄上,并要求服務(wù)器將這個密碼和用戶的密碼進行比對。因為比對是在LDAP服務(wù)器內(nèi)完成的,實際的密碼能保持私密。 如果你希望通過密碼比對進行認證,可以通過聲明passwordCompare()方法來實現(xiàn)

默認情況下,在登錄表單中提供的密碼將會與用戶的LDAP條目中的userPassword屬性進行比對。如果密碼被保存在不同的屬性中,可以通過passwordAttribute()方法來聲明密碼屬性的名稱:

auth.ldapAuthentication()
 .userSearchBase("ou=people")
 .userSearchFilter("(uid={0})")
 .groupSearchBase("ou=groups")
 .groupSearchFilter("(member={0})")
 .passwordCompare()
 .passwordEncoder(new MD5PasswordEncode())
 .passwordAttribute("passcode");

在本例中,我們指定了要與給定密碼進行比對的是“passcode”屬性。另外,我們還可以指定密碼轉(zhuǎn)碼器。在進行服務(wù)器端密碼比對時,有一點非常好,那就是實際的密碼在服務(wù)器端是私密的。但是進行嘗試的密碼還是需要通過線路傳輸?shù)絃DAP服務(wù)器上,這可能會被黑客所攔截。為了避免這一點,我們可以通過調(diào)用passwordEncoder()方法指定加密策略。 在本示例中,密碼會進行MD5加密。這需要LDAP服務(wù)器上密碼也使用MD5進行加密。 引用遠程的LDAP服務(wù)器到目前為止,我們忽略的一件事就是LDAP和實際的數(shù)據(jù)在哪里。我們很開心地配置Spring使用LDAP服務(wù)器進行認證,但是服務(wù)器在哪里呢?默認情況下,Spring Security的LDAP認證假設(shè)LDAP服務(wù)器監(jiān)聽本機的33389端口。但是,如果你的LDAP服務(wù)器在另一臺機器上,那么可以使用contextSource()方法來配置這個地址:

auth.ldapAuthentication()
 .userSearchBase("ou=people")
 .userSearchFilter("(uid={0})")
 .groupSearchBase("ou=groups")
 .groupSearchFilter("(member={0})")
 .contextSource().root("dc=habuma,dc=com");

當LDAP服務(wù)器啟動時,它會嘗試在類路徑下尋找LDIF文件來加載數(shù)據(jù)。LDIF(LDAP Data Interchange Format,LDAP數(shù)據(jù)交換格式)是以文本文件展現(xiàn)LDAP數(shù)據(jù)的標準方式。每條記錄可以有一行或多行,每項包含一個名值對。記錄之間通過空行進行分割。 如果你不想讓Spring從整個根路徑下搜索LDIF文件的話,那么可以通過調(diào)用ldif()方法來明確指定加載哪個LDIF文件:

auth.ldapAuthentication()
 .userSearchBase("ou=people")
 .userSearchFilter("(uid={0})")
 .groupSearchBase("ou=groups")
 .groupSearchFilter("(member={0})")
 .contextSource().root("dc=habuma,dc=com").ldif("classpath:users.ldif")

http://m.itdecent.cn/p/7e4d99f6baaf LDAP入門

9.2.4 配置自定義的用戶服務(wù)

假設(shè)我們需要認證的用戶存儲在非關(guān)系型數(shù)據(jù)庫中,如Mongo或Neo4j,在這種情況下,我們需要提供一個自定義的UserDetailsService接口實現(xiàn)。

UserDetailsService接口非常簡單:


import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
?
public interface UserDetailService {
 UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}

我們所需要做的就是實現(xiàn)loadUserByUsername()方法,根據(jù)給定的用戶名來查找用戶。loadUserByUsername()方法會返回代表給定用戶的UserDetails對象。如下的程序清單展現(xiàn)了一個UserDetailsService的實現(xiàn),它會從給定的SpitterRepository實現(xiàn)中查找用戶。


import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
?
public interface UserDetailService {
 UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}
public class SpitterUserDetailService implements UserDetailService {
?
 @Autowired
 private SpittleRepository spittleRepository;
?
 @Override
 public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
 Spitter spitter = spittleRepository.findByUsername(username);
 if (spitter !=null) {
 List<GrantedAuthority> authorities = new ArrayList<>();
 authorities.add(new SimpleGrantedAuthority("ROLE_SPITER"));
 return new User(spitter.getUsername(),
 spitter.getPassword(),
 authorities);
 }
 throw new UsernameNotFoundException("User '" + username + " 'not found.");
 }
}

SpitterUserService有意思的地方在于它并不知道用戶數(shù)據(jù)存儲在什么地方。設(shè)置進來的SpitterRepository能夠從關(guān)系型數(shù)據(jù)庫、文檔數(shù)據(jù)庫或圖數(shù)據(jù)中查找Spitter對象,甚至可以偽造一個。SpitterUserService不知道也不會關(guān)心底層所使用的數(shù)據(jù)存儲。它只是獲得Spitter對象,并使用它來創(chuàng)建User對象。(User是UserDetails的具體實現(xiàn)。) 為了使用SpitterUserService來認證用戶,我們可以通過userDetailsService()方法將其設(shè)置到安全配置中:

9.3 攔截請求

一個特別簡單的Spring Security配置,在這個默認的配置中,會要求所有請求都要經(jīng)過認證。有些人可能會說,過多的安全性總比安全性太少要好。但也有一種說法就是要適量地應(yīng)用安全性。

在任何應(yīng)用中,并不是所有的請求都需要同等程度地保護。有些請求需要認證,而另一些可能并不需要。有些請求可能只有具備特定權(quán)限的用戶才能訪問,沒有這些權(quán)限的用戶會無法訪問。

對每個請求進行細粒度安全性控制的關(guān)鍵在于重載configure(HttpSecurity)方法。如下的代碼片段展現(xiàn)了重載的configure(HttpSecurity)方法,它為不同的URL路徑有選擇地應(yīng)用安全性:

@Override
protected void configure(HttpSecurity httpSecurity) throws Exception {
 httpSecurity.authorizeRequests()
 .antMatchers("/spitters/me")
 .authenticated()
 .antMatchers(HttpMethod.POST, "/spittles")
 .authenticated()
 .anyRequest()
 .permitAll();
}

configure()方法中得到的HttpSecurity對象可以在多個方面配置HTTP的安全性。在這里,我們首先調(diào)用authorizeRequests(),然后調(diào)用該方法所返回的對象的方法來配置請求級別的安全性細節(jié)。

其中,第一次調(diào)用antMatchers()指定了對“/spitters/me”路徑的請求需要進行認證。

第二次調(diào)用antMatchers()更為具體,說明對“/spittles”路徑的HTTP POST請求必須要經(jīng)過認證。

最后對anyRequests()的調(diào)用中,說明其他所有的請求都是允許的,不需要認證和任何的權(quán)限。 antMatchers()方法中設(shè)定的路徑支持Ant風格的通配符。在這里我們并沒有這樣使用,但是也可以使用通配符來指定路徑,如下所示:

.antMatchers("/spitter/**").authenticated();

我們也可以在一個對antMatchers()方法的調(diào)用中指定多個路徑:


.antMatchers("/spitter/**","/spitters/mine").authenticated();</pre>

除了路徑選擇,我們還通過authenticated()和permitAll()來定義該如何保護路徑。authenticated()要求在執(zhí)行該請求時,必須已經(jīng)登錄了應(yīng)用。如果用戶沒有認證的話,Spring Security的Filter將會捕獲該請求,并將用戶重定向到應(yīng)用的登錄頁面。同時,permitAll()方法允許請求沒有任何的安全限制。

除了authenticated()和permitAll()以外,還有其他的一些方法能夠用來定義該如何保護請求。

用來定義如何保護路徑的配置方法

方 法 能夠做什么
access(String) 如果給定的SpEL表達式計算結(jié)果為true,就允許訪問
anonymous() 允許匿名用戶訪問
authenticated() 允許認證過的用戶訪問
denyAll() 無條件拒絕所有訪問
fullyAuthenticated() 如果用戶是完整認證的話(不是通過Remember-me功能認證的),就允許訪問
hasAnyAuthority(String...) 如果用戶具備給定權(quán)限中的某一個的話,就允許訪問如果用戶具備給定角色中的某一個的話,就允許訪問
。。 。。。

我們可以將任意數(shù)量的antMatchers()、regexMatchers()和anyRequest()連接起來,以滿足Web應(yīng)用安全規(guī)則的需要。但是,我們需要知道,這些規(guī)則會按照給定的順序發(fā)揮作用。所以,很重要的一點就是將最為具體的請求路徑放在前面,而最不具體的路徑(如anyRequest())放在最后面。如果不這樣做的話,那不具體的路徑配置將會覆蓋掉更為具體的路徑配置。

9.3.1 使用Spring表達式進行安全保護

使用hasRole()限制某個特定的角色, 但是我們不能在相同的路徑上同時通過hasIpAddress()限制特定的IP地址。

望限制某個角色只能在星期二進行訪問

SpEL更強大的原因在于,hasRole()僅是Spring支持的安全相關(guān)表達式中的一種

在掌握了Spring Security的SpEL表達式后,我們就能夠不再局限于基于用戶的權(quán)限進行訪問限制了。例如,如果你想限制“/spitter/me” URL的訪問,不僅需要ROLE_SPITTER,還需要來自指定的IP地址,那么我們可以按照如下的方式調(diào)用access()方法

httpSecurity.antMatcher("/s/me").access("hasRole('ROLE_SPITTER') and hadIpAddress('192.168.1.2')");

可以使用SpEL實現(xiàn)各種各樣的安全性限制

9.3.2 強制通道的安全性

使用HTTP提交數(shù)據(jù)是一件具有風險的事情。如果使用HTTP發(fā)送無關(guān)緊要的信息,這可能不是什么大問題。但是如果你通過HTTP發(fā)送諸如密碼和信用卡號這樣的敏感信息的話,那你就是在找麻煩了。通過HTTP發(fā)送的數(shù)據(jù)沒有經(jīng)過加密,黑客就有機會攔截請求并且能夠看到他們想看的數(shù)據(jù)。這就是為什么敏感信息要通過HTTPS來加密發(fā)送的原因。

使用HTTPS似乎很簡單。你要做的事情只是在URL中的HTTP后加上一個字母“s”就可以了。是這樣嗎? 這是真的,但這是把使用HTTPS通道的責任放在了錯誤的地方。通過添加“s”我們就能很容易地實現(xiàn)頁面的安全性,但是忘記添加“s”同樣也是很容易出現(xiàn)的。如果我們的應(yīng)用中有多個鏈接需要HTTPS,估計在其中的一兩個上忘記添加“s”的概率還是很高的。

另一方面,你可能還會在原本并不需要HTTPS的地方,誤用HTTPS。

傳遞到configure()方法中的HttpSecurity對象,除了具有authorizeRequests()方法以外,還有一個requiresChannel()方法,借助這個方法能夠為各種URL模式聲明所要求的通道。

作為示例,可以參考Spittr應(yīng)用的注冊表單。盡管Spittr應(yīng)用不需要信用卡號、社會保障號或其他特別敏感的信息,但用戶有可能仍然希望信息是私密的。為了保證注冊表單的數(shù)據(jù)通過HTTPS傳送,我們可以在配置中添加requiresChannel()方法,如下所示:

程序清單9.5 requiresChannel()方法會為選定的URL強制使用HTTPS

傳遞到configure()方法中的HttpSecurity對象,除了具有authorizeRequests()方法以外,還有一個requiresChannel()方法,借助這個方法能夠為各種URL模式聲明所要求的通道。

作為示例,可以參考Spittr應(yīng)用的注冊表單。盡管Spittr應(yīng)用不需要信用卡號、社會保障號或其他特別敏感的信息,但用戶有可能仍然希望信息是私密的。為了保證注冊表單的數(shù)據(jù)通過HTTPS傳送,我們可以在配置中添加requiresChannel()方法,如下所示: 程序清單9.5 requiresChannel()方法會為選定的URL強制使用HTTPS

// 需要HTTPS
httpSecurity.authorizeRequests()
 .antMatchers("/spitter/me").hasRole("SPITTER")
 .antMatchers(HttpMethod.POST, "/spitter/me").hasRole("SPITTER")
 .anyRequest().permitAll()
 .and()
 .requiresChannel()
 .antMatchers("/spitter/from")
 .requiresSecure();</pre>

9.3.3 防止跨站請求偽造

當一個POST請求提交到“/spittles”上時,SpittleController將會為用戶創(chuàng)建一個新的Spittle對象。但是,如果這個POST請求來源于其他站點的話,會怎么樣呢?如果在其他站點提交如下表單,

<h1>跨域請求</h1>
<form method="post" action="http://www.spittr.com/spittle/register">
 <input type="hidden" name="message" value=" I`m stupid!" />
 <input type="submit" value="Click here to win a new car!" />
</form></pre>

這是跨站請求偽造(cross-site request forgery,CSRF)的一個簡單樣例。簡單來講,如果一個站點欺騙用戶提交請求到其他服務(wù)器的話,就會發(fā)生CSRF攻擊,這可能會帶來消極的后果。盡管提交“I’m stupid!”這樣的信息到微博站點算不上什么CSRF攻擊的最糟糕場景,但是你可以很容易想到更為嚴重的攻擊情景,它可能會對你的銀行賬號執(zhí)行難以預期的操作。

從Spring Security 3.2開始,默認就會啟用CSRF防護。實際上,除非你采取行為處理CSRF防護或者將這個功能禁用,否則的話,在應(yīng)用中提交表單時,你可能會遇到問題。

Spring Security通過一個同步token的方式來實現(xiàn)CSRF防護的功能。它將會攔截狀態(tài)變化的請求(例如,非GET、HEAD、OPTIONS和TRACE的請求)并檢查CSRF token。如果請求中不包含CSRF token的話,或者token不能與服務(wù)器端的token相匹配,請求將會失敗,并拋出CsrfException異常。

這意味著在你的應(yīng)用中,所有的表單必須在一個“_csrf”域中提交token,而且這個token必須要與服務(wù)器端計算并存儲的token一致,這樣的話當表單提交的時候,才能進行匹配。好消息是,Spring Security已經(jīng)簡化了將token放到請求的屬性中這一任務(wù)。如果你使用Thymeleaf作為頁面模板的話,只要<form>標簽的action屬性添加了Thymeleaf命名空間前綴,那么就會自動生成一個“csrf”隱藏域:

<form method="POST"  th:action="@{/spittle/register}*">
</form>

處理CSRF的另外一種方式就是根本不去處理它。我們可以在配置中通過調(diào)用csrf().disable()禁用Spring Security的CSRF防護功能,如 http.. .csrf( ).dsibale( ); 但禁用CSRF防護功能通常來講并不是一個好主意

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容