本文基于《Spring實(shí)戰(zhàn)(第4版)》所寫镜盯。
Spring Security簡介
Spring Security是為基于Spring的應(yīng)用程序提供聲明式安全保護(hù)的安全性框架。Spring Security提供了完整的安全性解決方案傻咖,它能夠在Web請求級別和方法調(diào)用級別處理身份認(rèn)證和授權(quán)。因?yàn)榛赟pring框架仑氛,所以Spring Security充分利用了依賴注入和面向切面的技術(shù)翠拣。
Spring Security從兩個(gè)角度來解決安全性問題。它使用Servlet規(guī)范中的Filter保護(hù)Web請求并限制URL級別的訪問虑灰。Spring Security還能夠使用Spring AOP保護(hù)方法調(diào)用—借助于對象代理和使用通知吨瞎,能夠確保只有具備適當(dāng)權(quán)限的用戶才能訪問安全保護(hù)的方法。
理解Spring Security的模塊
第一件需要做的事就是將Spring Security模塊添加到應(yīng)用程序的類路徑下穆咐。Spring Security 3.2分為11個(gè)模塊颤诀,如下表
模塊 | 描述 |
---|---|
ACL | 支持通過訪問控制列表(access control list, ACL)為域?qū)ο筇峁┌踩?/td> |
切面(Aspects) | 一個(gè)很小的模塊,當(dāng)使用Spring Security注解時(shí)对湃,會使用基于AspectJ的切面着绊,而不是使用標(biāo)準(zhǔn)的Spring AOP |
CAS客戶端(CAS Client) | 提供與Jasig的中心認(rèn)證服務(wù)(Central Authentication Service, CAS)進(jìn)行集成的功能 |
配置(Configuration) | 包含通過XML和Java配置Spring Security的功能支持 |
核心(Core) | 提供Spring Security基本庫 |
加密(Cryptography) | 提供了加密和密碼編碼的功能 |
LDAP | 支持基于LDAP進(jìn)行認(rèn)證 |
OpenID | 支持使用OpenID進(jìn)行集中式認(rèn)證 |
Remoting | 提供了對Spring Remoting的支持 |
標(biāo)簽庫(Tag Library) | Spring Security的JSP標(biāo)簽庫 |
Web | 提供了Spring Security基于Filter的Web安全性支持 |
應(yīng)用程序的類路徑下至少要包含Core和Configuration這兩個(gè)模塊。Spring Security經(jīng)常被用于保護(hù)Web應(yīng)用熟尉,這顯然也是Spittr應(yīng)用的場景,所以我們還需要添加Web模塊洲脂。同時(shí)我們還會用到Spring Security的JSP標(biāo)簽庫斤儿,所以我們需要將這個(gè)模塊也添加進(jìn)來。
過濾Web請求
Spring Security借助一系列Servlet Filter來提供各種安全性功能恐锦。借助與Spring的小技巧往果,我們只需配置一個(gè)Filter就可以了。
DelegatingFilterProxy是一個(gè)特殊的Servlet Filter一铅,它本身所做的工作并不多陕贮。只是將工作委托給一個(gè)javax.servlet.Filter實(shí)現(xiàn)類,這個(gè)實(shí)現(xiàn)類作為一個(gè)<bean>注冊在Spring應(yīng)用的上下文中潘飘,如下圖:
在傳統(tǒng)的web.xml中配置Servlet和Filter和話肮之,可以使用<filter>元素,如下所示:
<filter>
<filter-name>springSecurityFilterChain</filter-name>
<filter-class>
org.springframework.web.filter.DelegatingFilterProxy
</filter-class>
</filter>
在這里卜录,最重要的是<filter-name>設(shè)置成了springSecurityFilterChain戈擒。這是因?yàn)槲覀凂R上就會將Spring Security配置在Web安全性之中,這里會有一個(gè)名為springSecurityFilterChain的Filter bean艰毒,DelegatingFilterProxy會將過濾邏輯委托給它筐高。
如果你希望借助WebApplicationInitializer以Java的方式來配置DelegatingFilterProxy的話,那么我們所需要做的就是創(chuàng)建一個(gè)擴(kuò)展的新類:
package spittr.config;
import org.springframework.security.web.context.AbstractSecurityWebApplicationInitializer;
public class SecurityWebInitializer extends AbstractSecurityWebApplicationInitializer {
}
AbstractSecurityWebApplicationInitializer實(shí)現(xiàn)了WebApplicationInitializer,因此Spring會發(fā)現(xiàn)它柑土,并用它在Web容器中注冊DelegatingFilterProxy蜀肘。盡管我們可以重載它的appendFilters()或insertFilters()方法來注冊自己選擇的Filter,但是要注冊DelegatingFilterProxy的話稽屏,我們并不需要重載任何方法扮宠。
不管我們通過web.xml還是通過AbstractSecurityWebApplicationInitializer的子類來配置DelegatingFilterProxy,它都會攔截發(fā)往應(yīng)用中的請求诫欠,并將請求委托給ID為springSecurityFilterChain bean涵卵。
springSecurityFilterChain本身是另一個(gè)特殊的Filter,它也被稱為FilterChainProxy荒叼。它可以鏈接任意一個(gè)或多個(gè)其他的Filter轿偎。Spring Security依賴一系列Servlet Filter來提供不同的安全特性。但是被廓,你幾乎不需要知道這些細(xì)節(jié)坏晦,因?yàn)槟悴恍枰@式聲明springSecurityFilterChain以及它所鏈接在一起的其他Filter。當(dāng)我們啟動(dòng)Web安全性的時(shí)候嫁乘,會自動(dòng)創(chuàng)建這些Filter昆婿。
編寫簡單的安全性配置
Spring 3.2后,如下的程序清單展現(xiàn)了Spring Security最簡單的Java配置蜓斧。
package spittr.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
@Configuration
@EnableWebSecurity // 啟動(dòng)Web安全性
public class SecurityConfig extends WebSecurityConfigurerAdapter {
}
顧名思義仓蛆,@EnableWebSecurity注解將會啟用Web安全功能。但它本身并沒有什么用處挎春,Spring Security必須配置在一個(gè)實(shí)現(xiàn)了WebSecurityConfigurer的bean中看疙,或者(簡單起見)擴(kuò)展WebSecurityConfigurerAdapter。在Spring應(yīng)用上下文中直奋,任何實(shí)現(xiàn)了WebSecurityConfigurer的bean都可以用來配置Spring Security能庆,但是最為簡單的方式還是像上面程序清單那樣擴(kuò)展WebSecurityConfigurerAdapter類。
@EnableWebSecurity可以啟用任意Web應(yīng)用的安全性功能脚线,看起來似乎并沒有做太多的事情搁胆,但安全配置類會給應(yīng)用產(chǎn)生很大的影響,將應(yīng)用嚴(yán)格鎖定邮绿,導(dǎo)致沒有人能夠進(jìn)入該系統(tǒng)了渠旁!
盡管不是嚴(yán)格要求的,但我們可能希望指定Web安全的細(xì)節(jié)斯碌,這要通過重載WebSecurityConfigurerAdapter中的一個(gè)或多個(gè)方法來實(shí)現(xiàn)一死。我們可以通過重載WebSecurityConfigurerAdapter的三個(gè)configure()方法來配置Web安全性,這個(gè)過程中會使用傳遞進(jìn)來的參數(shù)設(shè)置行為傻唾。下表描述了這三個(gè)方法投慈。
方法 | 描述 |
---|---|
configure(WebSecurity) | 通過重載承耿,配置Spring Security的Filter鏈 |
configure(HttpSecurity) | 通過重載,配置如何通過攔截器保護(hù)請求 |
configure(AuthenticationManagerBuilder) | 通過重載伪煤,配置user-detail服務(wù) |
我們之前的Java配置的Spring Security沒有重寫上述三個(gè)configure()方法中的任何一個(gè)加袋,這就說明了為什么應(yīng)用現(xiàn)在是被鎖定的。盡管對于我們的需求來講默認(rèn)的Filter鏈?zhǔn)遣诲e(cuò)的抱既,但是默認(rèn)的configure(HttpSecurity)實(shí)際上等同于如下所示:
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.anyRequest().authenticated()
.and()
.formLogin().and()
.httpBasic();
}
這個(gè)簡單的默認(rèn)配置指定了該如何保護(hù)HTTP請求职烧,以及客戶端認(rèn)證用戶的方案。通過調(diào)用authorizeRequests()和anyRequest().authenticated()就會要求所有進(jìn)入應(yīng)用的HTTP請求都要進(jìn)行認(rèn)證防泵。它也配置Spring Security支持基于表單的登錄以及HTTP Basic方式的認(rèn)證蚀之。
同時(shí),因?yàn)槲覀儧]有重載configure(AuthenticationManagerBuilder)方法捷泞,所以沒有用戶存儲支撐認(rèn)證過程足删。沒有用戶存儲,實(shí)際上就等于沒有用戶锁右。所以在這里所有的請求都需要認(rèn)證失受,但是沒有人能夠登錄成功。
為了讓Spring Security滿足我們應(yīng)用的需要咏瑟,還需要再添加一點(diǎn)配置拂到。具體來講,我們需要:
- 配置用戶儲存码泞;
- 指定哪些請求需要認(rèn)證兄旬,哪些請求不需要認(rèn)證,以及所需要的權(quán)限余寥;
- 提供一個(gè)自定義的登錄頁面辖试,替代原來簡單的默認(rèn)登錄頁。
除了Spring Security的這些功能劈狐,我們可能還希望基于安全限制,有選擇性在Web視圖上顯示特定的內(nèi)容呐馆。
選擇查詢用戶詳細(xì)信息的服務(wù)
我們所需要的是用戶存儲肥缔,也就是用戶名、密碼以及其他信息存儲的地方汹来,在進(jìn)行認(rèn)證決策的時(shí)候會對其進(jìn)行檢索续膳。
好消息是,Spring Security非常靈活收班,能夠基于各種數(shù)據(jù)存儲來認(rèn)證用戶坟岔。它內(nèi)置了多種常見的用戶存儲場景,如內(nèi)存摔桦、關(guān)系型數(shù)據(jù)庫以及LDAP社付。但我們也可以編寫并插入自定義的用戶存儲實(shí)現(xiàn)承疲。
借助Spring Security的Java配置,我們能夠很容易地配置一個(gè)或多個(gè)數(shù)據(jù)存儲方案鸥咖。
使用基于內(nèi)存的用戶存儲
因?yàn)槲覀兊陌踩渲妙悢U(kuò)展了WebSecurityConfigurerAdapter燕鸽,因此配置用戶存儲的最簡單方式就是重載configure()方法,并以AuthenticationManagerBuilder作為傳入?yún)?shù)啼辣。AuthenticationManagerBuilder有多個(gè)方法可以用來配置Spring Security對認(rèn)證的支持啊研。通過inMemoryAuthentication()方法,我們可以啟動(dòng)鸥拧、配置并任意填充基于內(nèi)存的用戶存儲党远。
例如,以下程序中富弦,SecurityConfig重載了configure()方法沟娱,并使用兩個(gè)用戶來配置內(nèi)存用戶存儲。
package spittr.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication() // 啟動(dòng)內(nèi)存用戶存儲
.withUser("user").password("password").roles("USER").and()
.withUser("admin").password("password").roles("USER","ADMIN");
}
}
我們可以看到舆声,configure() 方法中的AuthenticationManagerBuilder使用構(gòu)造者風(fēng)格的接口來構(gòu)建認(rèn)證配置花沉。通過簡單地調(diào)用inMemoryAuthentication()就能啟動(dòng)內(nèi)存用戶存儲。但是我們還需要有一些用戶媳握,否則的話碱屁,這和沒有用戶并沒有什么區(qū)別。
因此蛾找,我們需要調(diào)用withUser()方法為內(nèi)存用戶存儲添加新的用戶娩脾,這個(gè)方法的參數(shù)是username。withUser()方法返回的是UserDetailsManagerConfigurer.UserDetailsBuilder打毛,這個(gè)對象提供了多個(gè)進(jìn)一步配置用戶的方法柿赊,包括設(shè)置用戶密碼的password()方法以及為給定用戶授予一個(gè)或多個(gè)角色權(quán)限的roles()方法。
在以上程序中幻枉,我們添加了兩個(gè)用戶碰声,“user”和“admin”,密碼均為“password”熬甫∫忍簦“user”用戶具有USER角色,而“admin”用戶具有ADMIN和USER兩個(gè)角色椿肩。我們可以看到瞻颂,and()方法能夠?qū)⒍鄠€(gè)用戶的配置連接起來。
除了password()郑象、roles()和and()方法以外贡这,還有其他的幾個(gè)方法可以用來配置內(nèi)存用戶存儲中的用戶信息。下表描述了UserDetailsManagerConfigurer.UserDetailsBuilder對象所有可用的方法厂榛。
需要注意的是盖矫,roles()方法是authorities()方法的簡寫形式丽惭。roles()方法所給定的值都會添加一個(gè)“ROLE ”前綴,并將其作為權(quán)限授予給用戶炼彪。實(shí)際上同木,如下的用戶配置與以上程序是等價(jià)的
auth
.inMemoryAuthentication()
.withUser("user").password("password").authorities("ROLE_USER")
.and()
.withUser("admin").password("password")
.authorities("ROLE_USER", "ROLE_ADMIN");
方法 | 描述 |
---|---|
accountExpired(boolean) | 定義賬號是否已經(jīng)過期 |
accountLocked(boolean) | 定義賬號是否已經(jīng)鎖定 |
and() | 用來連接配置 |
authorities(GrantedAuthority...) | 授予某個(gè)用戶一項(xiàng)或多項(xiàng)權(quán)限 |
authorities(List<? extends GrantedAuthority>) | 授予某個(gè)用戶一項(xiàng)或多項(xiàng)權(quán)限 |
authorities(String...) | 授予某個(gè)用戶一項(xiàng)或多項(xiàng)權(quán)限 |
credentialsExpired(boolean) | 定義憑證是否已經(jīng)過期 |
disabled(boolean) | 定義賬號是否已經(jīng)被禁用 |
password(String) | 定義用戶的密碼 |
roles(String...) | 授予某個(gè)用戶一項(xiàng)或多項(xiàng)角色 |
基于數(shù)據(jù)庫表進(jìn)行認(rèn)證
用戶數(shù)據(jù)通常會存儲在關(guān)系型數(shù)據(jù)庫中甘萧,并通過JDBC進(jìn)行訪問。為了配置Spring Security使用以JDBC為支撐的用戶存儲,我們可以使用jdbcAuthentication()方法途戒,所需的最少配置如下所示:
@Autowired
private DataSource dataSource;
/**
* 配置Spring Security的Filter鏈
* @param auth
* @throws Exception
*/
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.jdbcAuthentication()
.dataSource(dataSource);
}
我們必須要配置的只是一個(gè)DataSource哀卫,這樣的話母市,就能訪問關(guān)系型數(shù)據(jù)庫了够坐。在這里,DataSource是通過自動(dòng)裝配的技巧得到的檩帐。
重寫默認(rèn)的用戶查詢功能
盡管默認(rèn)的最少配置能夠讓一切運(yùn)轉(zhuǎn)起來术幔,但是它對我們的數(shù)據(jù)庫模式有一些要求。它預(yù)期存在某些存儲用戶數(shù)據(jù)的表湃密。更具體來說诅挑,下面的代碼片段來源于Spring Security內(nèi)部,這塊代碼展現(xiàn)了當(dāng)查找用戶信息時(shí)所執(zhí)行的SQL查詢語句:
public static final String DEF_AUTHORITIES_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, ga.authorities ga " +
"where gm.username = ?"
"and g.id = ga.group_id " +
"and g.id = gm.group_id";
第一個(gè)查詢中泛源,我們獲取了用戶的用戶名拔妥、密碼以及是否啟用的信息,這些信息會用來進(jìn)行用戶認(rèn)證达箍。接下來的查詢查找了用戶所授予的權(quán)限没龙,用來進(jìn)行鑒權(quán),最后一個(gè)查詢中缎玫,查找了用戶作為群組的成員所授予的權(quán)限硬纤。
如果你能夠在數(shù)據(jù)庫中定義和填充滿足這些查詢的表,那么基本上就不需要你再做什么額外的事情了赃磨。但是筝家,也有可能你的數(shù)據(jù)庫與默認(rèn)所屬并不一致,那么你就會希望在查詢上有更多的控制權(quán)邻辉。如果是這樣的話肛鹏,我們可以按照如下的方式配置自己的查詢:
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.jdbcAuthentication()
.dataSource(dataSource)
.usersByUsernameQuery(
"select username, password, true " +
"from Spitter where username=?")
.authoritiesByUsernameQuery(
"select username, 'ROLE_USER' from Spitter where username=?");
}
在本例中,我們只重寫了認(rèn)證和基本權(quán)限的查詢語句恩沛,但是通過調(diào)用groupAuthoritiesByUsername()方法,我們也能夠?qū)⑷航M權(quán)限重寫為自定義的查詢語句缕减。
將默認(rèn)的SQL查詢替換為自定義的設(shè)計(jì)時(shí)雷客,很重要的一點(diǎn)就是要遵循查詢的基本協(xié)議。所有查詢都將用戶名作為唯一的參數(shù)桥狡。認(rèn)證查詢會選取用戶名搅裙、密碼以及啟用狀態(tài)信息皱卓。權(quán)限查詢會選取零行或多行包含該用戶名以及其權(quán)限信息的數(shù)據(jù)。群組權(quán)限查詢會選取零行或多行數(shù)據(jù)部逮,每行數(shù)據(jù)中都會包含群組ID娜汁、群組名稱以及權(quán)限。
使用轉(zhuǎn)碼后的密碼
看一下上面的認(rèn)賬查詢兄朋,它會預(yù)期用戶密碼存儲在了數(shù)據(jù)庫之中掐禁。這里唯一的問題在于如果密碼明文存儲的話,會很容易收到黑客的竊取颅和。但是傅事,如果數(shù)據(jù)庫中的密碼進(jìn)行了轉(zhuǎn)碼的話,那么認(rèn)證就會失敗峡扩,因?yàn)樗c用戶提交的明文密碼并不匹配蹭越。
為了解決這個(gè)問題,我們需要借助passwordEncoder() 方法指定一個(gè)密碼轉(zhuǎn)碼器(encoder):
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.jdbcAuthentication()
.dataSource(dataSource)
.usersByUsernameQuery(
"select username, password, true " +
"from Spitter where username=?")
.authoritiesByUsernameQuery(
"select username, 'ROLE_USER' from Spitter where username=?")
.passwordEncoder(new StandardPasswordEncoder("53cr3t"));
}
passwordEncoder()方法可以接受Spring Security中PasswordEncoder接口的任意實(shí)現(xiàn)教届。Spring Security的加密模塊包括了三個(gè)這樣的實(shí)現(xiàn):BCryptPasswordEncoder响鹃、NoOpPasswordEncoder和StandardPasswordEncoder.
上述的代碼中使用了StandardPasswordEncoder,但是如果內(nèi)置的實(shí)現(xiàn)無法滿足需求時(shí)案训,你可以提供自定義的實(shí)現(xiàn)买置。PasswordEncoder接口非常簡單:
public interface PasswordEncoder {
String encode(CharSequence rawPassword);
boolean matches(CharSequence rawPassword, String encodedPassword);
}
不管你使用哪一個(gè)密碼轉(zhuǎn)碼器,都需要理解的一點(diǎn)是萤衰,數(shù)據(jù)庫中的密碼是永遠(yuǎn)不會解碼的堕义。所采取的策略與之相反,用戶在登錄時(shí)輸入的密碼會按照相同的算法進(jìn)行轉(zhuǎn)碼脆栋,然后再在數(shù)據(jù)庫中已經(jīng)轉(zhuǎn)碼過的密碼進(jìn)行對比倦卖。這個(gè)對比是在PasswordEncoder的matches()方法中進(jìn)行的。
配置自定義的用戶服務(wù)
假設(shè)我們需要認(rèn)證的用戶存儲在非關(guān)系型數(shù)據(jù)庫中椿争,如Mongo或Neo4j怕膛,在這種情況下,我們需要提供一個(gè)自定義的UserDetailsService接口實(shí)現(xiàn)秦踪。
UserDetailsService接口非常簡單:
public interface UserDetailsService {
UserDetails loadUserByUsername(String username)
throws UsernameNotFoundException;
}
我們所需要做的就是實(shí)現(xiàn)loadUserByUsername() 方法褐捻,根據(jù)給定的用戶名來查找用戶。loadUserByUsername() 方法會返回代表給定用戶的UserDetails對象椅邓。如下的程序清單展現(xiàn)了一個(gè)UserDetailsService的實(shí)現(xiàn)柠逞,它會從給定的SpitterRepository實(shí)現(xiàn)中查找用戶。
package spittr.config;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import spittr.data.SpitterRepository;
import spittr.model.Spitter;
import java.util.ArrayList;
import java.util.List;
public class SpitterUserService implements UserDetailsService {
private final SpitterRepository spitterRepository;
//注入SpitterRepository
public SpitterUserService(SpitterRepository spitterRepository){
this.spitterRepository = spitterRepository;
}
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// 查找Spitter
Spitter spitter = spitterRepository.findByUsername(username);
if (spitter != null) {
List<GrantedAuthority> authorities = new ArrayList<GrantedAuthority>();
// 創(chuàng)建權(quán)限列表
authorities.add(new SimpleGrantedAuthority("ROLE_SPITTER"));
return new User( // 返回User
spitter.getUsername(),
spitter.getPassword(),
authorities);
}
throw new UsernameNotFoundException("User '" + username + "' not found.");
}
}
SpitterUserService不知道也不會關(guān)心底層所使用的數(shù)據(jù)存儲景馁。它只是獲得Spitter對象板壮,并使用它來創(chuàng)建User對象。(User是UserDetails的具體實(shí)現(xiàn)合住。)
為了使用SpitterUserService來認(rèn)證用戶绰精,我們可以通過userDetailsService()方法將其設(shè)置到安全配置中:
@Autowired
SpitterRepository spitterRepository;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(new SpitterUserService(spitterRepository));
}
userDetailsService()方法(類似于jdbcAuthentication()撒璧、ladpAuthentication()以及inMemoryAuthentication())會配置一個(gè)用戶存儲。不過笨使,這里所使用的不是Spring所提供的用戶存儲卿樱,而是使用
UserDetailsService的實(shí)現(xiàn)。
另一種值得考慮的方法就是修改Spitter硫椰,讓其實(shí)現(xiàn)UserDetails繁调。這樣的話,loadUserByUsername() 就能直接返回Spitter對象了最爬,而不必再將它的值復(fù)制到User對象中涉馁。
攔截請求
在任何應(yīng)用中,并不是所有的請求都需要同等程度地保護(hù)爱致。有些請求需要認(rèn)證烤送,而另一些可能并不需要。有些請求可能只有具備特定權(quán)限的用戶才能訪問糠悯,沒有這些權(quán)限的用戶會無法訪問帮坚。
例如,考慮Spittr應(yīng)用的請求互艾。首頁當(dāng)然是公開的试和,不需要進(jìn)行保護(hù)。類似地纫普,因?yàn)樗械腟pittle都是公開的阅悍,所以展現(xiàn)Spittle的頁面不需要安全性。但是昨稼,創(chuàng)建Spittle的請求只有認(rèn)證用戶才能執(zhí)行节视。同樣,盡管用戶基本信息頁面是公開的假栓,不需要認(rèn)證寻行,但是如果要處理“/spitters/me”請求,并展現(xiàn)當(dāng)前用戶的基本信息時(shí)匾荆,那么就需要進(jìn)行認(rèn)證拌蜘,從而確定要展現(xiàn)誰的信息。
對每個(gè)請求進(jìn)行細(xì)粒度安全性控制的關(guān)鍵在于重載configure(HttpSecurity)方法牙丽。如下的代碼片段展現(xiàn)了重載的configure(HttpSecurity)方法简卧,它為不同的URL路徑有選擇地應(yīng)用安全性:
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/spitter/me").authenticated()
.antMatchers(HttpMethod.POST, "/spittles").authenticated()
.anyRequest().permitAll();
}
configure()方法中得到的HttpSecurity對象可以在多個(gè)方法配置HTTP的安全性。在這里烤芦,我們首先調(diào)用authorizeRequests()举娩,然后調(diào)用該方法所返回的對象的方法來配置請求級別的安全性細(xì)節(jié)。其中,第一次調(diào)用antMatchers()指定了對“/spitters/me”路徑的請求需要進(jìn)行認(rèn)證晓铆。第二次調(diào)用antMatchers()更為具體,說明對“/spittles”路徑的HTTP POST請求必須要經(jīng)過認(rèn)證绰播。最后對anyRequests() 的調(diào)用中骄噪,說明其他所有的請求都是允許的,不需要認(rèn)證和任何的權(quán)限蠢箩。
antMatchers() 方法中設(shè)定的路徑支持Ant風(fēng)格的通配符链蕊。在這里我們并沒有這樣使用,但是也可以使用通配符來指定路徑谬泌,如下所示:
.antMatchers("/spitters/**").authenticated();
我們也可以在一個(gè)對antMatchers()方法的調(diào)用中指定多個(gè)路徑:
.antMatchers("/spitters/**", "spittles/mine").authenticated();
authenticated()方法所使用的路徑可能會包括Ant風(fēng)格的通配符滔韵,而regexMatchers()方法則能夠接受正則表達(dá)式來定義請求路徑。例如掌实,如下代碼片段所使用的正則表達(dá)式與“/spitters/**”(Ant風(fēng)格)功能是相同的:
.regexMatchers("/spitters/.*").authenticated();
除了路徑選擇陪蜻,我們還通過authenticated()和permitAll()來定義該如何保護(hù)路徑。authenticated()要求在執(zhí)行該請求時(shí)贱鼻,必須已經(jīng)登錄了應(yīng)用宴卖。如果用戶沒有認(rèn)證的話,Spring Security的Filter將會捕獲該請求邻悬,并將用戶重定向到應(yīng)用的登錄頁面症昏。同時(shí),permitAll()方法允許請求沒有任何的安全限制父丰。
除了authenticated()和permitAll()方法以外肝谭,還有其他的一些方法能夠用來定義該如何保護(hù)請求。下表描述了所有可用的方案蛾扇。
方法 | 能夠做什么 |
---|---|
access(String) | 如果給定的SpEL表達(dá)式計(jì)算結(jié)果為true攘烛,就允許訪問 |
anonymous() | 允許匿名用戶訪問 |
authenticated() | 允許認(rèn)證過的用戶訪問 |
denyAll() | 無條件拒絕所有訪問 |
fullyAuthenticated() | 如果用戶是完整認(rèn)證的話(不是通過Remember-me功能認(rèn)證的),就允許訪問 |
hasAnyAuthority(String...) | 如果用戶具備給定權(quán)限中的某一個(gè)的話屁桑,就允許訪問 |
hasAnyRole(String...) | 如果用戶具備給定角色中的某一個(gè)的話医寿,就允許訪問 |
hasAuthority(String) | 如果用戶具備給定權(quán)限的話,就允許訪問 |
hasIpAddress(String) | 如果請求來自給定IP地址的話蘑斧,就允許訪問 |
hasRole(String) | 如果用戶具備給定角色的話靖秩,就允許訪問 |
not() | 對其他訪問方法的結(jié)果求反 |
permitAll() | 無條件允許訪問 |
rememberMe() | 如果用戶是通過Remember-me功能認(rèn)證的,就允許訪問 |
通過上表的方法竖瘾,我們所配置的安全性能夠不僅僅限于認(rèn)證用戶沟突。例如,我們可以修改之前的configure()方法捕传,要求用戶不僅需要認(rèn)證惠拭,還要具備ROLE_SPITTER權(quán)限:
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/spitter/me").hasAuthority("ROLE_SPITTER")
.antMatchers(HttpMethod.POST, "/spittles").hasAuthority("ROLE_SPITTER")
.anyRequest().permitAll();
}
作為替代方案,我們還可以使用hasRole()方法,它會自動(dòng)使用“ROLE_”前綴:
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/spitter/me").hasRole("SPITTER")
.antMatchers(HttpMethod.POST, "/spittles").hasRole("SPITTER")
.anyRequest().permitAll();
}
我們可以將任意數(shù)量的antMatchers()职辅、regexMatchers()和anyRequest()連接起來棒呛,以滿足Web應(yīng)用安全規(guī)則的需要。但是域携,我們需要知道簇秒,這些規(guī)則會按照給定的順序發(fā)揮作用。所以秀鞭,很重要的一點(diǎn)就是將最為具體的請求路徑放在前面趋观,而最不具體的路徑(如anyRequest())放在最后面。如果不這樣做的話锋边,那不具體的路徑配置將會覆蓋掉更為具體的路徑配置皱坛。
使用Spring表達(dá)式進(jìn)行安全保護(hù)
定義保護(hù)路徑配置的方法表的大多數(shù)方法都是一維的,也就是說我們可以使用hasRole() 限制某個(gè)特定的角色豆巨,但是我們不能在相同的路徑上同時(shí)通過hasIpAddress() 限制特定的IP地址剩辟。
借助access()方法,我們也可以將SpEL作為聲明訪問限制的一種方式搀矫。例如抹沪,如下就是使用SpEL表達(dá)式來聲明具有“ROLE_SPITTER”角色才能訪問“/spitter/me”URL:
.antMatchers("/spitter/me").access("hasRole('ROLE_SPITTER')")
這個(gè)對“/spitter/me”的安全限制與開始時(shí)的效果是等價(jià)的,只不過這里使用了SpEL來描述安全規(guī)則瓤球。如果當(dāng)前用戶被授予了給定角色的話融欧,那hasRole()表達(dá)式的計(jì)算結(jié)果就為true。
讓SpEL更強(qiáng)大的原因在于卦羡,hasRole() 僅是Spring支持的安全相關(guān)表達(dá)式中的一種噪馏,下表列出了Spring Security支持的所有SpEL表達(dá)式。
安全表達(dá)式 | 計(jì)算結(jié)果 |
---|---|
authentication | 用戶的認(rèn)證對象 |
denyAll | 結(jié)果始終為false |
hasAnyRole(list of roles) | 如果用戶被授予了列表中任意的指定角色绿饵,結(jié)果為true |
hasRole(role) | 如果用戶被授予了指定的角色欠肾,結(jié)果為true |
hasIpAddress(IPAddress) | 如果請求來自指定的IP的話,結(jié)果為true |
isAnonymous() | 如果當(dāng)前用戶為匿名用戶拟赊,結(jié)果為true |
isAuthenticated() | 如果當(dāng)前用戶進(jìn)行了認(rèn)證的話刺桃,結(jié)果為true |
isFullAuthenticated() | 如果當(dāng)前用戶進(jìn)行了完整認(rèn)證的話(不是用過Remember-me功能進(jìn)行的認(rèn)證),結(jié)果為true |
isRememberMe() | 如果當(dāng)前用戶是通過Remember-me自動(dòng)認(rèn)證的話吸祟,結(jié)果為true |
permitAll | 結(jié)果始終為true |
principal | 用戶的principal對象 |
在掌握了Spring Security的SpEL表達(dá)式后瑟慈,我們就能夠不再局限于基于用戶的權(quán)限進(jìn)行訪問限制了。例如屋匕,如果你想限制“/spitter/me”URL的訪問葛碧,不僅需要ROLE_SPITTER,還需要來自指定的IP地址过吻,那么我們可以按照如下的方式調(diào)用access()方法:
.antMatchers("/spitter/me")
.access("hasRole('ROLE_SPITTER') and hasIpAddress('192.168.1.2')")
我們可以使用SpEL實(shí)現(xiàn)各種各樣的安全性限制进泼。
還有一種Spring Security攔截請求的方式:強(qiáng)制通道的安全性。
強(qiáng)制通道的安全性
使用HTTP提交數(shù)據(jù)是一件具有風(fēng)險(xiǎn)的事情。通過HTTP發(fā)送的數(shù)據(jù)沒有經(jīng)過加密乳绕,黑客就有機(jī)會攔截請求并且能夠看到他們想看的數(shù)據(jù)绞惦。這就是為什么敏感信息要通過HTTPS來加密發(fā)送的原因。
傳遞到configure() 方法中的HttpSecurity對象洋措,除了具有authorizeRequests() 方法以外翩隧,還有一個(gè)requiresChannel() 方法,借助這個(gè)方法能夠?yàn)楦鞣N為各種URL模式聲明所要求的通道呻纹。
作為示例,可以參考Spittr應(yīng)用的注冊表單专缠。盡管Spittr應(yīng)用不需要信用卡號等敏感的信息雷酪,但用戶有可能仍然希望信息是私密的。為了保證注冊表單的數(shù)據(jù)通過HTTPS傳送涝婉,我們可以在配置中添加requiresChannel() 方法哥力,如下所示:
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/spitter/me").hasRole("SPITTER")
.antMatchers(HttpMethod.POST, "/spittles").hasRole("SPITTER")
.anyRequest().permitAll()
.and()
.requiresChannel()
.antMatchers("/spitter/form").requiresSecure(); // 需要 HTTPS
}
不論何時(shí),只要是對“/spitter/form”的請求墩弯,Spring Security都視為需要安全通道(通過調(diào)用requiresChannel()確定)并自動(dòng)將請求重定向到HTTPS上吩跋。
與之相反,有些頁面并不需要通過HTTPS傳送渔工。例如锌钮,首頁不包含任何敏感信息,因此并不需要通過HTTPS傳送引矩。我們可以使用requiresInsecure()代替requiresSecure()方法梁丘,將首頁聲明為始終通過HTTP傳送:
.antMatchers("/").requiresInsecure();
如果通過HTTPS發(fā)送了對“/”的請求,Spring Security將會把請求重定向到不安全的HTTP通道上旺韭。
在強(qiáng)制要求通道時(shí)氛谜,路徑的選取方案與authorizeRequests()是相同的。程序中区端,除了可以使用antMatchers()值漫,我們也可以使用regexMatchers()方法,通過正則表達(dá)式選取路徑模式织盼。
防止跨站請求偽造
當(dāng)一個(gè)POST請求提交到“/spittles”上杨何,SpittleController將會為用戶創(chuàng)建一個(gè)新的Spittle對象。但是如果這個(gè)POST請求來源于其他站的話悔政,這就是跨站請求偽造(cross-site request forgery晚吞,CSRF)的一個(gè)簡單樣例。簡單來講谋国,如果一個(gè)站點(diǎn)欺騙用戶提交請求到其他服務(wù)器的話槽地,就會發(fā)生CSRF攻擊,這可能會帶來消極的后果,比如可能會對你的銀行賬號執(zhí)行難以預(yù)期的操作捌蚊。
從Spring Security 3.2開始集畅,默認(rèn)就會啟用CSRF防護(hù)。實(shí)際上缅糟,除非你采取行為處理CSRF防護(hù)或者將這個(gè)功能禁用挺智,否則的話,在應(yīng)用中提交表單時(shí)窗宦,可能會遇到問題赦颇。
Spring Security通過一個(gè)同步token的方式來實(shí)現(xiàn)CSRF防護(hù)的功能。它將會攔截狀態(tài)變化的請求(例如赴涵,非GET媒怯、HEAD、OPTIONS和TRACE的請求)并檢查CSRF token髓窜。如果請求中不包含CSRF token的話扇苞,或者token不能與服務(wù)器端的token相匹配,請求將會失敗寄纵,并拋出CsrfException異常鳖敷。
這意味著在你的應(yīng)用中,所有的表單必須在一個(gè)“_csrf ”域中提交token程拭,而且這個(gè)token必須要與服務(wù)器端計(jì)算并存儲的token一致定踱,這樣的話當(dāng)表單提交的時(shí)候,才能進(jìn)行匹配恃鞋。
好消息是屋吨,Spring Security已經(jīng)簡化了將token放到請求的屬性中這一任務(wù)。如果使用JSP作為頁面模板的話山宾,我們要做的事情:
<input type="hidden" name="${_csrf.parameterName}" value="${_csrf.token}" />
更好的功能是至扰,如果使用Spring的表單綁定標(biāo)簽的話,<sf:form>便簽會自動(dòng)為我們添加隱藏的CSRF token標(biāo)簽资锰。
處理CSRF的另一種方式就是根本不去處理它敢课。我們可以在配置中通過調(diào)用csrf().disable()禁用Spring Security的CSRF防護(hù)功能,如下所示:
@Override
protected void configure(HttpSecurity http) throws Exception {
http.
...
.csrf()
.disable(); //禁用CSRF防護(hù)功能
}
需要提醒的是绷杜,禁用CSRF防護(hù)功能通常來講并不是一個(gè)好主意直秆。如果這樣做的話,那么應(yīng)用就會面臨CSRF攻擊的風(fēng)險(xiǎn)鞭盟。只要在深思熟慮之后圾结,才能禁用CSRF防護(hù)功能。
認(rèn)證用戶
如果你使用程序中最簡單的Spring Security配置的話齿诉,那么就能無償?shù)氐玫揭粋€(gè)登錄頁筝野。實(shí)際上晌姚,在重寫configure(HttpSecurity)之前,我們都能使用一個(gè)簡單卻功能完備的登錄頁歇竟。但是挥唠,一旦重寫了configure(HttpSecurity)方法,就失去了這個(gè)簡單的登錄頁面焕议。
不過宝磨,把這個(gè)功能找回來也很容易。我們所需要做的就是在configure(HttpSecurity)方法中盅安,調(diào)用formLogin()唤锉,如下面的程序清單所示。
請注意别瞭,和前面一樣腌紧,這里調(diào)用and()方法來將不同的配置指令連接在一起。
如果我們訪問應(yīng)用的“/login”鏈接或者導(dǎo)航到需要認(rèn)證的頁面畜隶,那么將會在瀏覽器中展現(xiàn)登錄頁面。如下圖所示号胚,在審美上它沒有什么令人興奮的籽慢,但是它卻能實(shí)現(xiàn)所需的功能。
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.formLogin() // 啟用默認(rèn)的登錄頁
.and()
.authorizeRequests()
.antMatchers("/spitter/me").hasRole("SPITTER")
.antMatchers(HttpMethod.POST, "/spittles").hasRole("SPITTER")
.anyRequest().permitAll()
.and()
.requiresChannel()
.antMatchers("/spitter/form").requiresSecure();
}
添加自定義的登錄頁
如下程序清單所展現(xiàn)的JSP模板提供了一個(gè)與Spittr應(yīng)用風(fēng)格一致的登錄頁箱亿。
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>
<%@ page session="false" language="java" pageEncoding="UTF-8" contentType="text/html; charset=utf-8"%>
<%@ taglib prefix="sf" uri="http://www.springframework.org/tags/form" %>
<%@ page isELIgnored="false"%>
<!DOCTYPE HTML>
<html>
<head>
<title>Spitter Login</title>
<link rel="stylesheet" type="text/css"
href="<c:url value="/resources/style.css" />" >
</head>
<body onload="document.f.username.focus();">
<h3>Login</h3>
<form name='f' action='/login' method='POST'>
<table>
<tr><td>User:</td><td>
<input type="text" name="username" /></td></tr>
<tr><td>Password:</td>
<td><input type="password" name="password" /></td></tr>
<tr><td colspan='2'>
<input name="submit" type="submit" value="Login"/></td></tr>
</table>
<input type="hidden" name="${_csrf.parameterName}" value="${_csrf.token}" />
</form>
<%--以下是使用了Spring Form的標(biāo)簽--%>
<%--<sf:form name="f" method="POST" action="/login">--%>
<%--<sf:errors path="*" element="div" cssClass="errors"/>--%>
<%--Username: <sf:input path="username"/><br/>--%>
<%--Password: <sf:password path="password"/><br/>--%>
<%--<input type="submit" value="Login" />--%>
<%--</sf:form>--%>
</body>
</html>
啟用HTTP Basic認(rèn)證
除了表單提交,我們還會經(jīng)称眩看到Web應(yīng)用的頁面轉(zhuǎn)化為RESTful API届惋。
HTTP Basic認(rèn)證(HTTP Basic Authentication)會直接通過 HTTP請求本身,對要訪問應(yīng)用程序的用戶進(jìn)行認(rèn)證菠赚。你可能在以前見過HTTP Basic認(rèn)證脑豹。當(dāng)在Web瀏覽其中使用,它將想用戶彈出一個(gè)簡單的模態(tài)對話框衡查。
本質(zhì)上瘩欺,這是一個(gè)HTTP 401響應(yīng),表明必須要在請求中包含一個(gè)用戶名和密碼拌牲。在REST客戶端它使用的服務(wù)進(jìn)行認(rèn)證的場景中俱饿,這種方式比較適合。
如果要啟用HTTP Basic認(rèn)證的話塌忽,只需在configure()方法所傳入的HttpSecurity對象上調(diào)用httpBasic()即可拍埠。另外糯崎,還可以通過調(diào)用realmName()方法指定域强饮。如下是在Spring Security中啟用HTTP Basic認(rèn)證的典型配置:
@Override
protected void configure(HttpSecurity http) throws Exception {
http.formLogin()
.loginPage("/login")// 如果不設(shè)置,則默認(rèn)為/login
.and()
.httpBasic()
.realmName("Spittr")
.and()
...
}
注意免姿,和前面一樣,在configure()方法中坷虑,通過調(diào)用and()方法來將不同的配置指令連接在一起甲馋。
在httpBasic()方法中,并沒有太多的可配置項(xiàng)迄损,甚至不需要什么額外配置定躏。HTTP Basic認(rèn)證要么開啟要么關(guān)閉。
啟用Remember-me功能
許多站點(diǎn)提供了Remember-me功能芹敌,你只要登錄過一次痊远,應(yīng)用就會記住你,當(dāng)再次回到應(yīng)用的時(shí)候你就不需要登錄了氏捞。
Spring Security使得為應(yīng)用添加Remember-me功能變得非常容易碧聪。為了啟用這項(xiàng)功能,只需在configure()方法所傳入的HttpSecurity對象上調(diào)用rememberMe()即可液茎。
@Override
protected void configure(HttpSecurity http) throws Exception {
http.formLogin()
.loginPage("/login")// 如果不設(shè)置逞姿,則默認(rèn)為/login
.and()
.rememberMe()
.tokenValiditySeconds(2419200)
.key("spittrKey"); // cookie中的私鑰
}
在這里,我們通過一點(diǎn)特殊的配置就可以啟用Remember-me功能捆等。默認(rèn)情況下滞造,這個(gè)功能是通過在cookie中存儲一個(gè)token完成的,這個(gè)token最多兩周內(nèi)有效栋烤。但是谒养,在這里,我們指定這個(gè)token最多四周內(nèi)有效(2,419,200秒)明郭。
存儲在cookie中的token包含用戶名买窟、密碼、過期時(shí)間和一個(gè)私鑰—在寫入cookie前都進(jìn)行了MD5哈希薯定。默認(rèn)情況下始绍,私鑰的名為SpringSecured,但在這里我們將其設(shè)置為spitterKey话侄,使它專門用戶Spittr應(yīng)用疆虚。
如此簡單。既然Remember-me功能已經(jīng)啟用满葛,我們需要有一種方式來讓用戶表明他們希望應(yīng)用程序能夠記住他們径簿。為了實(shí)現(xiàn)這一點(diǎn),登錄請求必須包含一個(gè)名為remember-me的參數(shù)嘀韧。在登錄表單中篇亭,增加一個(gè)簡單復(fù)選框就可以完成這件事情:
<input id="remember_me" name="remember-me" type="checkbox"/>
<label for="remember_me" class="inline">Remember me</label>
退出
在應(yīng)用中,與登錄同等主要的功能就是退出锄贷。其實(shí)译蒂,按照我們的配置曼月,退出功能已經(jīng)啟用了,不需要再做其他的配置了柔昼。我們需要的只是一個(gè)使用該功能的鏈接哑芹。
退出功能通過Servlet容器中Filter實(shí)現(xiàn)的(默認(rèn)情況下),這個(gè)Filter會攔截對“/logout”的請求捕透。因此聪姿,為應(yīng)用添加退出功能只需要添加如下的鏈接即可:
<a href="<c:url value="/logout" />">Logout</a>
當(dāng)用戶點(diǎn)擊這個(gè)鏈接的時(shí)候,會發(fā)起對“/logout”的請求乙嘀,這個(gè)請求會被Spring Security的LogoutFilter所處理末购。用戶會退出應(yīng)用,所有Remember-me token都會被清除掉虎谢。在退出完成后盟榴,用戶瀏覽器將會重定向“/login?logout”,從而允許用戶進(jìn)行再次登錄婴噩。
如果你希望用戶被重定向到其他的頁面擎场,如應(yīng)用的首頁,那么可以在configure()中進(jìn)行如下的配置:
@Override
protected void configure(HttpSecurity http) throws Exception {
http.formLogin()
.loginPage("/login")// 如果不設(shè)置几莽,則默認(rèn)為/login
.and()
.logout()
.logoutSuccessUrl("/")
...
}
在這里迅办,和前面一樣,通過andI()連接起了對logout()的調(diào)用银觅。logout()提供了配置退出行為的方法。在本例中坏为,調(diào)用logoutSuccessUrl()表明在退出成功之后究驴,瀏覽器需要重定向到“/”。
除了logoutSuccessUrl()方法以外匀伏,你可能還希望重寫默認(rèn)的LogoutFilter攔截路徑洒忧。我們可以通過調(diào)用logoutUrl()方法實(shí)現(xiàn)這一功能:
.logout()
.logoutUrl("/signout") // 自定義訪問退出地址
.logoutSuccessUrl("/"); // 自定義退出后的重定向地址
保護(hù)視圖
為了在瀏覽器渲染HTML內(nèi)容時(shí),在視圖中能夠反映安全限制和相關(guān)的信息够颠,我們可以通過Spring Security本身提供的JSP標(biāo)簽庫或者Thymeleaf實(shí)現(xiàn)與Spring Security的集成熙侍。
使用Spring Security的JSP標(biāo)簽庫
Spring Security的JSP標(biāo)簽庫很小,只包含三個(gè)標(biāo)簽履磨,如下表蛉抓。
JSP標(biāo)簽 | 作用 |
---|---|
<security:accesscontrollist> | 如果用戶通過訪問控制列表授予了指定的權(quán)限,那么渲染該標(biāo)簽體中的內(nèi)容 |
<security:authentication> | 渲染當(dāng)前用戶認(rèn)證對象的詳細(xì)信息 |
<security:authorize> | 如果用戶被授予了特定的權(quán)限或者SpEL表達(dá)式的計(jì)算結(jié)果為true剃诅,那么渲染該標(biāo)簽體中的內(nèi)容 |
為了使用JSP標(biāo)簽庫巷送,我們需要在對應(yīng)的JSP中聲明它:
<%@ taglib prefix="security"
uri="http://www.springframework.org/security/tags" %>
只要標(biāo)簽庫在JSP文件中進(jìn)行了聲明,我們就可以使用它了矛辕。
訪問認(rèn)證信息的細(xì)節(jié)
借助Spring Security JSP標(biāo)簽庫笑跛,所能做到的最簡單的一件事情就是便利地訪問用戶的認(rèn)證信息付魔。例如,對于Web站點(diǎn)來講飞蹂,在頁面頂部以用戶名標(biāo)示顯示“歡迎”或“您好”信息是很常見的几苍。這恰恰是<security:authentication>能為我們所做的事情。例如:
Hello <security:authentication property="principal.username" />!
其中陈哑,property用來標(biāo)示用戶認(rèn)證對象的一個(gè)屬性妻坝。可用的屬性取決于用戶認(rèn)證的方式芥颈。但是惠勒,我們可以依賴幾個(gè)通用的屬性,在不同的認(rèn)證方式下爬坑,他們都是可用的纠屋,如下表所示
認(rèn)證屬性 | 描述 |
---|---|
authorities | 一組用于表示用戶所授予權(quán)限的GrantedAuthority對象 |
Credentials | 用于核實(shí)用戶的憑證(通常,這會是用戶的密碼) |
details | 認(rèn)證的附加信息(IP地址盾计、證件序列號售担、會話ID等) |
principal | 用戶的基本信息對象 |
在我們的示例中,實(shí)際上渲染的是principal屬性中嵌套的username屬性署辉。
當(dāng)像前面示例那樣使用時(shí)族铆,<security:authentication>將在視圖中渲染屬性的值。但是如果你愿意將其賦值給一個(gè)變量哭尝,那只需要在var屬性中指明變量的名字即可哥攘。例如,如下展現(xiàn)了如何將其設(shè)置給名為loginId的屬性:
<security:authentication property="principal.username" var="loginId" />
這個(gè)變量默認(rèn)是定義在頁面作用域內(nèi)的材鹦。但是如果你愿意在其他作用域內(nèi)創(chuàng)建它逝淹,例如請求或會話作用域(或者是能夠在javax.servlet.jsp.PageContext中獲取的其他作用域),那么可以通過scope屬性來聲明桶唐。例如栅葡,要在請求作用域內(nèi)創(chuàng)建這個(gè)變量,那可以使用<security:authentication>按照如下的方式來設(shè)置:
<security:authentication property="principal.username"
var="loginId" scope="request" />
條件性的渲染內(nèi)容
有時(shí)候視圖上的一部分內(nèi)容需要根據(jù)用戶被授予了什么權(quán)限來確定是否渲染尤泽。對于已經(jīng)登錄的用戶顯示登錄表單欣簇,或者對還未登錄的用戶顯示個(gè)性化的問候信息都是毫無意義的。
Spring Security的<security:authorize>JSP標(biāo)簽?zāi)軌蚋鶕?jù)用戶被授予的權(quán)限有條件地渲染頁面的部分內(nèi)容坯约。例如熊咽,在Spittr應(yīng)用中,對于沒有ROLE_SPITTER角色的用戶闹丐,我們不會為其顯示添加新Spitter記錄的表單网棍。以下程序展現(xiàn)了如何使用<security:authorize>標(biāo)簽來為具有ROLE_SPITTER角色的用戶顯示Spitter表單。
<sec:authorize access="hasRole('ROLE_SPITTER')">
<s:url value="/spittles" var="spittle_url" />
<sf:form modelAttribute="spittle" action="${spittle_url}">
<sf:label path="text">
<s:message code="label.spittle" text="Enter spittle:"/>
</sf:label>
<sf:textarea path="text" rows="2" cols="40" />
<sf:errors path="text" />
<br/>
<div class="spitIsSubmitIt">
<input type="submit" value="Spit it!"
class="status-btn round-btn disabled" />
</div>
</sf:form>
</sec:authorize>
access屬性被賦值為一個(gè)SpEL表達(dá)式妇智,這個(gè)表達(dá)式的值將確定<security:authorize>標(biāo)簽主體內(nèi)的內(nèi)容是否渲染滥玷。這里我們使用了hasRole('ROLE_SPITTER')表達(dá)式來確保用戶具有ROLE_SPITTER角色氏身。但是當(dāng)你設(shè)置access屬性時(shí),可以任意發(fā)揮SpEL的強(qiáng)大威力惑畴。
借助與這些可以的表達(dá)式蛋欣,可以構(gòu)造出非常有意思的安全性約束。例如如贷,假設(shè)應(yīng)用中有一些管理功能只能對用戶名為habuma的用戶可用陷虎。也許你會像這樣使用isAuthenticated()和principal表達(dá)式:
<security:authorize
access="isAuthenticated() and principal.username=='habuma' ">
<a href="/admin">Administration</a>
</security:authorize>
盡管<security:authorize>能在視圖上阻止鏈接的渲染。但是沒能阻止別人在瀏覽器的地址欄手動(dòng)輸入“/admin”這個(gè)URL杠袱。
只需要在安全配置中尚猿,添加一個(gè)對antMatchers()方法的調(diào)用將會嚴(yán)格限制對“/admin”這個(gè)URL的訪問。
.antMatchers("/admin")
.access("isAuthenticated() and principal.user=='habuma' ");
如果想要不像access屬性那樣需要在兩個(gè)地方聲明SpEL表達(dá)式楣富,可以使用<security:authorize>的url屬性凿掂,它所要做的事情是對一個(gè)給定的URL模式會間接引用其安全性約束。鑒于我們已經(jīng)在Spring Security配置中為“/admin”聲明了安全性約束纹蝴,所以我們可以這樣使用url屬性:
<security:authorize url="/admin">
<spring:url value="/admin" var="admin_url" />
<br/><a href="${admin_url}">Admin</a>
</security:authorize>
因?yàn)橹挥谢拘畔⒅杏脩裘麨椤癶abuma”的已認(rèn)證用戶才能訪問“/admin” URL庄萎,所以只有滿足以上條件,<security:authorize>標(biāo)簽主體中的內(nèi)容才會被渲染塘安。我們只在一個(gè)地方配置了表達(dá)式(安全配置中)糠涛,但是在兩個(gè)地方進(jìn)行了應(yīng)用。