Spring實(shí)戰(zhàn)(九)-保護(hù)Web應(yīng)用

本文基于《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)用的上下文中潘飘,如下圖:

DelegatingFilterProxy把Filter的處理邏輯委托給Spring應(yīng)用上下文中所定義的一個(gè)代理Filter bean

在傳統(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();  
    }
默認(rèn)的登錄頁在審美上過于簡陋猫胁,但是功能完備

添加自定義的登錄頁

如下程序清單所展現(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)用。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末兼犯,一起剝皮案震驚了整個(gè)濱河市忍捡,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌切黔,老刑警劉巖砸脊,帶你破解...
    沈念sama閱讀 221,198評論 6 514
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異绕娘,居然都是意外死亡脓规,警方通過查閱死者的電腦和手機(jī)栽连,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,334評論 3 398
  • 文/潘曉璐 我一進(jìn)店門险领,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人秒紧,你說我怎么就攤上這事绢陌。” “怎么了熔恢?”我有些...
    開封第一講書人閱讀 167,643評論 0 360
  • 文/不壞的土叔 我叫張陵脐湾,是天一觀的道長。 經(jīng)常有香客問我叙淌,道長秤掌,這世上最難降的妖魔是什么愁铺? 我笑而不...
    開封第一講書人閱讀 59,495評論 1 296
  • 正文 為了忘掉前任,我火速辦了婚禮闻鉴,結(jié)果婚禮上茵乱,老公的妹妹穿的比我還像新娘。我一直安慰自己孟岛,他們只是感情好瓶竭,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,502評論 6 397
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著渠羞,像睡著了一般斤贰。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上次询,一...
    開封第一講書人閱讀 52,156評論 1 308
  • 那天荧恍,我揣著相機(jī)與錄音,去河邊找鬼渗蟹。 笑死块饺,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的雌芽。 我是一名探鬼主播授艰,決...
    沈念sama閱讀 40,743評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼世落!你這毒婦竟也來了淮腾?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,659評論 0 276
  • 序言:老撾萬榮一對情侶失蹤屉佳,失蹤者是張志新(化名)和其女友劉穎谷朝,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體武花,經(jīng)...
    沈念sama閱讀 46,200評論 1 319
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡圆凰,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,282評論 3 340
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了体箕。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片专钉。...
    茶點(diǎn)故事閱讀 40,424評論 1 352
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖累铅,靈堂內(nèi)的尸體忽然破棺而出跃须,到底是詐尸還是另有隱情,我是刑警寧澤娃兽,帶...
    沈念sama閱讀 36,107評論 5 349
  • 正文 年R本政府宣布菇民,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏第练。R本人自食惡果不足惜阔馋,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,789評論 3 333
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望娇掏。 院中可真熱鬧垦缅,春花似錦、人聲如沸驹碍。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,264評論 0 23
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽志秃。三九已至怔球,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間浮还,已是汗流浹背竟坛。 一陣腳步聲響...
    開封第一講書人閱讀 33,390評論 1 271
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留钧舌,地道東北人担汤。 一個(gè)月前我還...
    沈念sama閱讀 48,798評論 3 376
  • 正文 我出身青樓,卻偏偏與公主長得像洼冻,于是被迫代替她去往敵國和親崭歧。 傳聞我的和親對象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,435評論 2 359

推薦閱讀更多精彩內(nèi)容

  • Spring Cloud為開發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見模式的工具(例如配置管理撞牢,服務(wù)發(fā)現(xiàn)率碾,斷路器,智...
    卡卡羅2017閱讀 134,693評論 18 139
  • Spring Boot 參考指南 介紹 轉(zhuǎn)載自:https://www.gitbook.com/book/qbgb...
    毛宇鵬閱讀 46,836評論 6 342
  • Spring Web MVC Spring Web MVC 是包含在 Spring 框架中的 Web 框架屋彪,建立于...
    Hsinwong閱讀 22,430評論 1 92
  • 前言 本章內(nèi)容: ??Spring Security介紹 ??使用Servlet規(guī)范中的Filter保護(hù)Web應(yīng)用...
    Chandler_玨瑜閱讀 7,180評論 0 68
  • 1所宰、盒子模型 1.1、 box-sizing: border-box; box-sizing:border-box...
    馮威武閱讀 190評論 0 0