- 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防護功能通常來講并不是一個好主意