if語句中條件判斷就是檢查當前的url請求是不是logout-url的配置值,接下來,獲取用戶的authentication,並循環調用處理器鏈中各個處理器的logout()函數,前面在parse階段說過,處理器鏈中有兩個實例,處理會話的SecurityContextLogoutHandler及remember-me服務,咱們來一一看看它們的logout函數實現:css
2.1.0 SecurityContextLogoutHandlerhtml
public void logout(HttpServletRequest request, HttpServletResponse response, Authentication authentication) { Assert.notNull(request, "HttpServletRequest required"); if (this.invalidateHttpSession) { HttpSession session = request.getSession(false); if (session != null) { session.invalidate(); //使當前會話失效 } } SecurityContextHolder.clearContext(); //清空安全上下文 }
很簡單,若是配置了登出之後使會話失效,則它調用session的invalidate()讓會話過時,另外,清空安全上下文。java
Spring Security
Spring Security是Spring社區的一個頂級項目,也是Spring Boot官方推薦使用的Security框架。除了常規的Authentication和Authorization以外,Spring Security還提供了諸如ACLs,LDAP,JAAS,CAS等高級特性以知足複雜場景下的安全需求。雖然功能強大,Spring Security的配置並不算複雜(得益於官方詳盡的文檔),尤爲在3.2版本加入Java Configuration的支持以後,能夠完全告別令很多初學者望而卻步的XML Configuration。在使用層面,Spring Security提供了多種方式進行業務集成,包括註解,Servlet API,JSP Tag,系統API等。下面就結合一些示例代碼介紹Boot應用中集成Spring Security的幾個關鍵點。web
1核心概念
Principle(User), Authority(Role)和Permission是Spring Security的3個核心概念。
跟一般理解上Role和Permission之間一對多的關係不一樣,在Spring Security中,Authority和Permission是兩個徹底獨立的概念,二者並無必然的聯繫,但能夠經過配置進行關聯。
正則表達式
應用級別的安全主要分爲「驗證( authentication) 」和「(受權) authorization 」兩個部分。
這也是Spring Security主要須要處理的兩個部分:
在Spring Security中,認證過程稱之爲Authentication(驗證),指的是創建系統使用者信息( principal )的過程。使用者能夠是一個用戶、設備、或者其餘能夠在咱們的應用中執行某種操做的其餘系統。
" Authorization "指的是判斷某個 principal 在咱們的應用是否容許執行某個操做。在 進行受權判斷以前,要求其所要使用到的規則必須在驗證過程當中已經創建好了。
這些概念是通用的,並非只針對"Spring Security"。
關於Authentication:算法
Spring Security中的驗證authentication 究竟是什麼?spring
讓咱們考慮一個每一個人都熟悉的標準驗證場景:
一、一個用戶被提示使用用戶名和密碼登陸
二、系統成功的驗證了用戶名與密碼是匹配的
三、獲取到用戶的上下文信息(角色列表等)
四、創建這個用戶的安全上下文(security context )
五、用戶可能繼續進行一些受到訪問控制機制保護的操做,訪問控制機制會依據當前安全上下文信息檢查這個操做所需的權限。數據庫
前三條組成了驗證過程,所以咱們要看一下在Spring Security中這是如何發生的:
一、用戶名和密碼被獲取到,並放入一個 UsernamePasswordAuthenticationToken 實例中( Authentication接口的一個實例,咱們以前已經看到過)。
二、這個token被傳遞到一個 AuthenticationManager 實例中進行驗證
三、在成功驗證後, AuthenticationManager返回一個全部字段都被賦值的 Authentication 對象實例
四、經過調用 SecurityContextHolder.getContext().setAuthentication(…)建立安全上下文,經過返回的驗證對象進行傳遞。
從這個角度來講,用戶被認爲已經成功驗證。讓咱們來看一段樣例代碼:apache
import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.BadCredentialsException; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; import org.springframework.security.core.AuthenticationException; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.context.SecurityContextHolder; import java.io.BufferedReader; import java.io.InputStreamReader; import java.util.ArrayList; import java.util.List; public class AuthenticationExample { private static AuthenticationManager authenticationManager = new SampleAuthenticationManager(); public static void main(String[] args) throws Exception { BufferedReader in = new BufferedReader(new InputStreamReader(System.in)); while (true) { System.out.println("Please enter your username:"); String name = in.readLine(); System.out.println("Please enter your password:"); String password = in.readLine(); try { Authentication request = new UsernamePasswordAuthenticationToken(name, password); System.out.println("before:" + request); Authentication result = authenticationManager.authenticate(request); System.out.println("after:" + result); SecurityContextHolder.getContext().setAuthentication(result); break; } catch (AuthenticationException e) { System.out.println("Authentication failed: " + e.getMessage()); } } System.out.println("Successfully authenticated. Security context contains: " + SecurityContextHolder.getContext().getAuthentication()); } } class SampleAuthenticationManager implements AuthenticationManager { private static final List<GrantedAuthority> AUTHORITIES = new ArrayList<>(); static { AUTHORITIES.add(new SimpleGrantedAuthority("ROLE_USER")); } public Authentication authenticate(Authentication auth) throws AuthenticationException { if (auth.getName().equals(auth.getCredentials())) { return new UsernamePasswordAuthenticationToken(auth.getName(), auth.getCredentials(), AUTHORITIES); } throw new BadCredentialsException("Bad Credentials"); } }
這裏咱們編寫了一個小程序,要求用戶輸入用戶名和密碼並執行以上的驗證流程。咱們這裏實現的 AuthenticationManager 將會任何用戶輸入的用戶名和密碼是否相同。爲了每一個用戶分配一個單獨的角色。上面代碼輸出將會相似如下:小程序
Please enter your username: 123 Please enter your password: 345 before:org.springframework.security.authentication.UsernamePasswordAuthenticationToken@7bd9: Principal: 123; Credentials: [PROTECTED]; Authenticated: false; Details: null; Not granted any authorities Authentication failed: Bad Credentials Please enter your username: 123 Please enter your password: 123 before:org.springframework.security.authentication.UsernamePasswordAuthenticationToken@1f: Principal: 123; Credentials: [PROTECTED]; Authenticated: false; Details: null; Not granted any authorities after:org.springframework.security.authentication.UsernamePasswordAuthenticationToken@441d0230: Principal: 123; Credentials: [PROTECTED]; Authenticated: true; Details: null; Granted Authorities: ROLE_USER Successfully authenticated. Security context contains: org.springframework.security.authentication.UsernamePasswordAuthenticationToken@441d0230: Principal: 123; Credentials: [PROTECTED]; Authenticated: true; Details: null; Granted Authorities: ROLE_USER
注意你不須要編寫這樣的代碼。這個過程一般狀況下會在內部發生,例如在一個web驗證的過濾器中。咱們在這裏介紹這段代碼僅僅是爲了展現在Spring Security中構建驗證過程是很是簡單的。用戶在 SecurityContextHolder 包含了一個徹底賦值的 Authentication d的時候被驗證。
直接設置SecurityContextHolder中的內容
事實上,Spring Security並不關心你如何將 Authentication 對象放入 SecurityContextHolder中。
惟一的關鍵要求是在 AbstractSecurityInterceptor 驗證一個用戶請求以前確保 SecurityContextHolder 包含一個用於表示principal的 Authentication 對象。
你能夠(許多用戶都這樣作)編寫本身的Filter或者MVC controller,來提供與那些不是基於Spring Security的驗證系統的互操做能力。
例如,你可能會使用容器管理的驗證機制,經過ThreadLocal或者JNDI地址來使當前用戶可用。或者你可能在爲一個有着遺留驗證系統的公司工做。
在這類場景下,很容易可讓Spring Security工做,而且仍然提供驗證能力。
全部你須要作的是編寫一個過濾器,從某個位置讀取第三方用戶信息,構建一個特定的Spring Security Authentication 對象,並將其放入 SecurityContextHolder中。在這種狀況下,你須要考慮在內置的驗證基礎結構上自動應用這些。
例如,你可能須要在返回給客戶端響應以前,預先建立一個Http Session對象來爲不一樣線程緩存安全上下文。
5.3 在web應用中進行驗證
如今讓咱們來探索當你在一個web應用中使用Spring Security的情形(不使用web.xml配置安全)。這時一個用戶如何被驗證以及安全上下文如何被創建?
考慮一個傳統的web應用中的驗證過程:
一、你訪問主頁,而且點擊一個連接
二、一個請求被髮送給服務器,服務器判斷你是否在請求一個受保護的資源
三、若是你當前沒有通過驗證,服務器返回一個響應代表你必需要進行驗證。響應能夠是經過HTTP響應碼或者直接重定向到一個特定的網頁。
四、根據驗證機制,你的瀏覽器可能會重定向到一個特定的網頁以致於你能夠填寫表單,或者瀏覽器會檢索你的身份(經過一個基礎驗證對話框,一個cookie或者X.509證書,等等)。
五、瀏覽器給服務器回覆一個響應。這多是一個包含你填充好的表單內容的HTTP POST請求,或者一個包含你的驗證信息的HTTP請求頭。
六、下一步服務器會判斷當前的驗證信息是不是正確的。若是是,能夠繼續下一步。若是不是,一般你的瀏覽器會被要求重試(所以你又回到了上兩步)。
七、你的原始的驗證過程的請求將會被重試。但願你驗證後能被賦予足夠的權限來訪問受保護的資源。若是是,請求將會成功,不然,你將會得到一個403 HTTP響應碼,表示"禁止"。
對於以上提到的大部分步驟,Spring Security都有不一樣的類來負責。
主要的參與者(按照使用的順序)是 ExceptionTranslationFilter, AuthenticationEntryPoint 和驗證機制,負責調用咱們以前提到的 AuthenticationManager。
ExceptionTranslationFilter
ExceptionTranslationFilter是一個Spring Security的過濾器,負責檢測任何Spring Security拋出的異常。這些異常一般是經過一個 AbstractSecurityInterceptor拋出,這是驗證服務的一個主要提供者。咱們將會在下一節討論 AbstractSecurityInterceptor ,可是如今咱們僅僅須要知道其是用於產生Java異常,並不知道HTTP或者如何驗證一個principal。
取而代之的是 ExceptionTranslationFilter 來提供這個服務,負責返回403錯誤碼(若是principal已經被驗證而且缺少足夠的訪問權限-上面的第七步)
或者
啓動一個 AuthenticationEntryPoint (若是principal沒有被驗證,咱們須要進行上面的第三步)。
AuthenticationEntryPoint
AuthenticationEntryPoint 負責上述列表的第三步。你能夠想象,每個web應用會有一個默認的驗證策略(在Spring Security中,這能夠像其餘內容同樣進行配置,可是如今咱們須要繼續保持簡單)。
每一個驗證系統會有本身的 AuthenticationEntryPoint 實現,這將會執行第三步中描述的步驟。
驗證機制
一旦你的瀏覽器提交了你的驗證憑證(不管是HTTP表單提交或者HTTP請求頭),服務器端就須要一些機制來收集這些驗證信息。到目前咱們已經到了第六步。
在Spring Security中對於從用戶代理(一般是一個web瀏覽器)收集驗證信息的功能,咱們有一個特殊的名字來描述,稱之爲" authentication mechanism"(驗證機制)。
一旦從用戶代理(瀏覽器)中收集到驗證信息,一個驗證請求對象會被建立,以後傳遞給 AuthenticationManager。
在驗證機制接收到被徹底賦值後的 Authentication 對象後,若是肯定請求能夠執行,會將 Authentication 放到 SecurityContextHolder中,並引發原始的請求被重試(上述第七步)。若是 AuthenticationManager 拒絕了請求,驗證機制將會要求用戶代理(瀏覽器)重試(上述第二步)。
在不一樣的請求中存儲SecurityContext
根據應用的類型,可能須要有一個策略在不一樣的用戶請求中來存儲安全上下文。在一個傳統的web應用中,一個用戶被記錄並在接下來的過程當中經過session id進行區分。服務器在session中緩存principal的信息。在Spring Security中,存儲 SecurityContext 的責任交由SecurityContextPersistenceFilter負責,默認狀況下會將安全上下文當作 HttpSession的一個屬性存儲。對於每一次請求,會從新存儲安全上下文到 SecurityContextHolder 中,在請求完成時清空 SecurityContextHolder 。你不該該出於安全的目的而直接與 HttpSession 交互,而是老是應該使用 SecurityContextHolder 代替。
不少其餘類型的應用(例如無狀態的RESTFul web服務)並不使用Http Session,而且會從新驗證每個請求。可是,在過濾器鏈中包含 SecurityContextPersistenceFilter 依然很重要,這能夠確保每次請求後 SecurityContextHolder 被清空。
提示:在一個單獨的session中接受併發請求的應用中,同一個 SecurityContext 實例將會在線程間共享。即便一個 ThreadLocal 被使用,也是爲每個線程從 HttpSession 中獲取同一個 SecurityContext 實例。若是你但願一個線程在運行時臨時改變安全上下文,這是一個提示。若是你僅僅使用 SecurityContextHolder.getContext(),而且在返回的安全上下文對象上調用 setAuthentication(anAuthentication),全部共享同一個 SecurityContext 實例的多個併發線程中的 Authentication 對象都會改變。你能夠自定義 SecurityContextPersistenceFilter 的行爲來爲每一個請求建立一個全新的 SecurityContext,從而阻止在一個線程中的改變影響到其餘線程。做爲替選方案,你也能夠在須要臨時改變安全上下的時候來建立一個新的實例。
SecurityContextHolder.createEmptyContext()方法老是返回一個新的安全上下文實例。
5.4 Spring Security中的訪問控制(受權)
Spring Security中負責作出訪問控制決定的主要接口是 AccessDecisionManager,其有一個 decide 方法,接受一個描述principal的 Authentication 對象,一個「security object」(見下文)和一個應用於這個對象的安全元數據屬性列表(例如訪問一個資源必須被授予的角色列表)。
Secuirty和AOP Advice
若是你對AOP的概念熟悉,你可能知道有不一樣類型的advice可使用:before、after、throws和around。around advice很是有用,由於一個advisor能夠選擇是否執行一個方法調用,是否修改返回結果,以及是否拋出一個異常。
Spring Security對於web請求和方法調用都提供了一個around advice。咱們可使用Spring 標準的AOP支持來獲取一個方法的調用的around advice,也能夠經過一個標準的過濾器來得到web請求的around advice。
對於那些不熟悉AOP的用戶,理解這些的核心在於知道Spring Security能夠幫助你像保護web請求同樣來保護方法調用。
大部分用戶對於保護業務層的方法調用感興趣。這是由於在當前的JAVA EE開發中,服務層包含了大部分的業務邏輯代碼。
若是你須要保護服務層的方法調用,Spring 標準的AOP是合適的選擇。若是你想直接保護域對象,你會發現AspectJ是值得考慮的。
你能夠選擇使用AspectJ或者Spring AOP來執行方法受權,或者你能夠選擇使用過濾器來進行web請求受權。
你能夠聯合使用0、一、2或者3種方式。
主流的用法是執行一些web請求受權,而且在服務層聯合Spring AOP方法調用受權。
6 核心服務(Core Services)
如今咱們已經在較高的層面對SpringSecuirty的架構和核心類有所瞭解,如今咱們來仔細的查看一到兩個其核心接口的實現,特別是AuthenticationManager、UserDetailsService 和AccessDecisionManager。這三個接口會貫穿咱們後面文檔中的全部內容,所以掌握他們是如何配置以及使用是很是重要的。
6.1 AuthenticationManager, ProviderManager 與AuthenticationProvider
AuthenticationManager 僅僅是一個接口,因此咱們能夠任意的選擇器實現,可是在實際過程當中其是如何工做的呢?若是咱們的驗證信息位於多個數據庫實例或者咱們想聯合多個使用多個驗證服務,例如同時使用JDBC認證和LDAP認證,咱們須要作什麼呢?
在Spring Secuirty中,AuthenticationManager 默認的實現類是ProviderManager ,ProviderManager 並非本身直接對請求進行驗證,而是將其委派給一個AuthenticationProvider 列表。列表中的每個AuthenticationProvider 將會被依次查詢其是否須要經過其進行驗證。每一個provider的驗證結果只有兩個狀況:拋出一個異常或者徹底填充一個Authentication 對象的全部屬性。你是否還記得咱們的好朋友UserDetails和UserDetailsService?若是已經不記得了,請返回以前的章節從新閱讀來刷新的你的記憶。驗證一個請求最多見的方式是加載對應的UserDetails 來檢查用戶輸入的密碼與其是否匹配。DaoAuthenticationProvider 使用的就是這種方式(見下文)。被加載的UserDetails 對象(包含了GrantedAuthority s),在認證成功後,將會被用於填充的Authentication 對象,而且存儲在SecurityContext中。
若是你使用基於命名空間的配置,將會框架內部自動建立並維護一個ProviderManager 對象,你能夠在命名空間的authentication provider元素中往ProviderManager對象中添加provider。在這種情下,你沒必要在application context中聲明一個ProviderManager Bean。可是若是你沒有使用命名空間,你應該使用相似如下的方式進行聲明:
<bean id="authenticationManager" class="org.springframework.security.authentication.ProviderManager"> <constructor-arg> <list> <ref local="daoAuthenticationProvider"/> <ref local="anonymousAuthenticationProvider"/> <ref local="ldapAuthenticationProvider"/> </list> </constructor-arg> </bean>
在上面這個案例中,咱們有三個provider,他們將會被按順序進行嘗試(內部使用了一個List),每一個provider均可以嘗試進行認證,或者簡單的經過返回null來跳過認證。若是全部的provider都返回了null,ProviderManager 將會拋出一個ProviderNotFoundException異常。若是你對聯合使用多個provider感興趣,請參考ProviderManager 的javaDoc。
相似於表單登陸的處理過濾器這樣的驗證機制,會以引用的形式注入到ProviderManager 中,從而調用其來對請求進行驗證。使用這樣的驗證機制,咱們的provider有時是能夠相互替代的。例如DaoAuthenticationProvider 和LdapAuthenticationProvider 對於任何經過用戶名/密碼形式提交驗證請求的這樣的驗證機制都是可使用的(如基於表單的登陸和Http基礎驗證)。另外一方面,某些驗證機制建立的驗證請求只能由特定的AuthenticationProvider處理,例如JA-SIG CAS,其使用了服務ticket的概念,所以只能經過CasAuthenticationProvider驗證。你不須要要過多的考慮這些內容,由於若是你忘記了註冊一個合適的provider,你僅僅是會收到一個驗證失敗的時的ProviderNotFoundException 。
成功認證後刪除驗證信息
從Spring Security 3.1以後,在請求認證成功後ProviderManager 將會刪除Authentication 中的認證信息,這能夠阻止一些敏感信息例如密碼在不須要的時候還長時間保留。
這可能會致使一些問題,例如你但願經過緩存user對象來提升一個無狀態應用的性能。若是Authentication對象包含了位於緩存中的而且移除了認證信息的對象(例如UserDetails )的引用,那麼就沒法再繼續使用緩存的對象來進行驗證。在你使用緩存的時候,你須要考慮這個因素。一個顯而易見的解決方案是,不管是在AuthenticationProvider 仍是在緩存的實現中,在建立Authentication 對象的時候,將這個對象拷貝一份。或者你也能夠禁用ProviderManager的eraseCredentialsAfterAuthentication屬性。
DaoAuthenticationProvider
Spring Security提供了一個AuthenticationProvider 的簡單實現DaoAuthenticationProvider,這也是框架最先支持的provider。它使用了一個UserDetailsService 來查詢用戶名、密碼和GrantedAuthority 。其簡單的經過比較封裝了用戶的密碼信息的UsernamePasswordAuthenticationToken和經過UserDetailsService查詢出來的用戶的密碼是否相同來驗證用戶。對於這個provider的配置很是簡單。
<bean id="daoAuthenticationProvider" class="org.springframework.security.authentication.dao.DaoAuthenticationProvider"> <property name="userDetailsService" ref="inMemoryDaoImpl"/> <property name="passwordEncoder" ref="passwordEncoder"/> </bean>
PasswordEncoder 是可選的。PasswordEncoder 提供了對經過UserDetailsService查詢返回的UserDetails 對象中的密碼進行編碼和解碼的功能,更多的細節將在後面講解。
像本教程以前提到的同樣,大部分驗證provider都利用了UserDetails 和UserDetailsService 接口。回顧一下UserDetailsService 中只定義了一個方法:
UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
返回的UserDetails 也是一個接口,其只提供了getters方法來保證用戶的驗證信息例如用戶名、密碼和被授予的權利等不會由於用戶帳號的禁用或者啓用而被置爲空。大部分驗證提供者都會使用UserDetailsService,即便username和password在驗證過程當中不須要使用到。他們也可能只使用UserDetails 對象中GrantedAuthority 信息。由於一些其餘的系統(例如 LDAP or X.509 or CAS)接管了驗證用戶信息的責任。
在驗證(Authentication )層面, Spring Security 提供了不一樣的驗證模型。
大部分的authentication模型來自於第三方或者權威機構或者由一些相關的標準制定組織(如IETF)開發。
此外,Spring Security也提供了一些驗證特性。
特別的,Spring Security目前支持對如下全部驗證方式的整合:
HTTP BASIC authentication headers (an IETF RFC-based standard) HTTP Digest authentication headers (an IETF RFC-based standard) HTTP X.509 client certificate exchange (an IETF RFC-based standard) LDAP (a very common approach to cross-platform authentication needs, especially in large environments) Form-based authentication (for simple user interface needs) OpenID authentication Authentication based on pre-established request headers (such as Computer Associates Siteminder) JA-SIG Central Authentication Service (otherwise known as CAS, which is a popular open source single sign-on system) Transparent authentication context propagation for Remote Method Invocation (RMI) and HttpInvoker (a Spring remoting protocol) Automatic "remember-me" authentication (so you can tick a box to avoid re-authentication for a predetermined period of time) Anonymous authentication (allowing every unauthenticated call to automatically assume a particular security identity) Run-as authentication (which is useful if one call should proceed with a different security identity) Java Authentication and Authorization Service (JAAS) JEE container autentication (so you can still use Container Managed Authentication if desired) Kerberos Java Open Source Single Sign On (JOSSO) * OpenNMS Network Management Platform * AppFuse * AndroMDA * Mule ESB * Direct Web Request (DWR) * Grails * Tapestry * JTrac * Jasypt * Roller * Elastic Path * Atlassian Crowd * Your own authentication systems (see below)
注:*號標記的部分由第三方提供。
不少獨立軟件廠家(ISV)接受 Spring Security 的緣由就是能夠靈活的選擇驗證模型。這樣不管客戶的需求是什麼,他們均可以快速的整合進本身的解決方案,不須要進行太多額外的研究或者要求客戶改變他們的運行環境。若是上述驗證模型都不能知足咱們的需求,因爲Spring Security是一個開放的平臺,咱們也能夠很容易的就編寫出本身的驗證機制。
許多Spring Security的 企業用戶須要將一些並不遵循任何安全標準的"遺留"系統中整合進安全特性,Spring Security很適合用來處理這類系統。
除了驗證機制, Spring Security 也提供了一系列的受權能力。主要感興趣的是如下三個方面:
一、對web請求進行受權
二、受權某個方法是否能夠被調用
三、受權訪問單個領域對象實例
爲了理解其中的不一樣,考慮一下Servlet規範web模式安全中的受權能力、EJB容器管理的安全、文件系統安全。SpringSecurity對於這些重要的方面提供了深刻的支持
將以上代碼部署到Tomcat中運行,經過瀏覽器訪問任何頁面都會被從新定位到一個登錄頁面:
這個頁面是Spring Security自動幫咱們生成的。咱們可使用以前配置的用戶名和密碼進行登陸。
自動生成這個頁面的代碼位於類DefaultLoginPageGeneratingFilter的generateLoginPageHtml方法中。
Spring Security如何知道咱們但願全部的用戶都被驗證呢?
Spring Security是怎麼知道咱們但願支持表單形式的驗證呢?
緣由是咱們的SecurityConfig類繼承了WebSecurityConfigurerAdapter,而其在 configure(HttpSecurity http)中提供了一些默認的配置。
默認的配置以下:
protected void configure(HttpSecurity http) throws Exception { logger.debug("Using default configure(HttpSecurity). If subclassed this will potentially override subclass configure(HttpSecurity)."); http .authorizeRequests() .anyRequest().authenticated() .and() .formLogin().and() .httpBasic(); }
上面的默認配置的做用:
* 要求訪問應用的全部用戶都要被驗證
* 容許全部用戶能夠經過表單進行驗證
* 容許全部請求經過Http Basic 驗證(譯者注:關於什麼是Http Basic驗證,讀者能夠查看維基百科。)
你會發現,這個配置與如下xml格式的配置是相似的:
<http> <intercept-url pattern="/**" access="authenticated"/> <form-login /> <http-basic /> </http>
Java配置中的and()方法相似於xml配置中的結束標籤,and()方法返回的對象仍是HttpSecurity,方便咱們繼續對HttpSecurity進行配置。
HTTP基本認證
在HTTP中,基本認證是一種用來容許Web瀏覽器或其餘客戶端程序在請求時提供用戶名和口令形式的身份憑證的一種登陸驗證方式。
在發送以前是以用戶名追加一個冒號而後串接上口令,並將得出的結果字符串再用Base64算法編碼。
例如,提供的用戶名是Aladdin、口令是open sesame,則拼接後的結果就是Aladdin:open sesame,而後再將其用Base64編碼,獲得QWxhZGRpbjpvcGVuIHNlc2FtZQ==。
最終將Base64編碼的字符串發送出去,由接收者解碼獲得一個由冒號分隔的用戶名和口令的字符串。
雖然對用戶名和口令的Base64算法編碼結果很難用肉眼識別解碼,但它仍能夠極爲輕鬆地被計算機所解碼,就像其容易編碼同樣。
編碼這一步驟的目的並不是安全與隱私,而是爲將用戶名和口令中的不兼容的字符轉換爲均與HTTP協議兼容的字符集。
總的來講:
HttpSecurity是SecurityBuilder接口的一個實現類,從名字上咱們就能夠看出這是一個HTTP安全相關的構建器。固然咱們在構建的時候可能須要一些配置,當咱們調用HttpSecurity對象的方法時,實際上就是在進行配置。
例如在默認的安全配置中authorizeRequests(),formLogin()、httpBasic()這三個方法返回的分別是ExpressionUrlAuthorizationConfigurer、FormLoginConfigurer、HttpBasicConfigurer,他們都是SecurityConfigurer接口的實現類,分別表明的是不一樣類型的安全配置器。
所以,從總的流程上來講,當咱們在進行配置的時候,須要一個安全構建器SecurityBuilder(例如咱們這裏的HttpSecurity),SecurityBuilder實例的建立須要有若干安全配置器SecurityConfigurer實例的配合。
配置的最終結果是什麼?啓用指定的Filter並完成相關配置
基本上每一個SecurityConfigurer子類都對應一個或多個過濾器。
咱們分別查看ExpressionUrlAuthorizationConfigurer、FormLoginConfigurer、HttpBasicConfigurer三個類的JavaDoc:
可見ExpressionUrlAuthorizationConfigurer、FormLoginConfigurer、HttpBasicConfigurer三個配置器對應的Filter分別是FilterSecurityInterceptor、UsernamePasswordAuthenticationFilter、BasicAuthenticationFilter。
而HttpSecuirty內部維護了一個Filter的List集合,咱們添加的各類安全配置器對應的Filter最終都會被加入到這個List集合中。
從更高的層面來講,SecurityBuilder和SecurityConfigurer實現類都有不少。
SecurityBuilder的類圖
SecurityConfigurer的類圖
因爲SecurityConfigurer的類圖過於複雜,如下只列目前咱們已經接觸到的幾個(紅色圈標記)
雖然SecurityBuilder是須要使用到SecurityConfigurer,不過根據功能劃分,一個SecurityBuilder只能支持部分的SecurityConfigurer,而不是全部。
例如對於HttpSecurity來講,其支持的SecurityConfigurer定義在HttpSecurity類的源碼中:
能夠看到這些方法的返回類型都是XXXConfigurer,表示的是HttpSecurity這個SecurityBuilder支持的SecurityConfiguer。
對於其餘的SecurityBuilder實現類也是相似,其支持的SecurityConfiguer都定義在本身的源碼中。
前面咱們已經提到,當咱們在一個類上添加@EnableWebSecurity註解後,Spring Security會自動幫助咱們建立一個名字爲的springSecurityFilterChain過濾器。
這個過濾器實際上只是Spring Security框架驗證請求的一個入口,到底如何驗證請求其實是要依賴於咱們如何配置Spring Security。
咱們以上面提到的WebSecurityConfigurerAdapter默認的configuer(HttpSecurity)方法進行說明。
配置的核心代碼以下:
http.authorizeRequests() .anyRequest().authenticated() .and() .formLogin().and() .httpBasic();
以and()方法做爲切分,能夠劃分爲3個部分,你能夠認爲每一個部分實際上都是配置了一個過濾器。
首先咱們來觀察authorizeRequests(),formLogin()、httpBasic()三個方法的實現:
public final class HttpSecurity extends AbstractConfiguredSecurityBuilder<DefaultSecurityFilterChain, HttpSecurity> implements SecurityBuilder<DefaultSecurityFilterChain>, HttpSecurityBuilder<HttpSecurity>{ ... public ExpressionUrlAuthorizationConfigurer<HttpSecurity>.ExpressionInterceptUrlRegistry authorizeRequests() throws Exception { return getOrApply(new ExpressionUrlAuthorizationConfigurer<HttpSecurity>()) .getRegistry(); } public FormLoginConfigurer<HttpSecurity> formLogin() throws Exception { return getOrApply(new FormLoginConfigurer<HttpSecurity>()); } public HttpBasicConfigurer<HttpSecurity> httpBasic() throws Exception { return getOrApply(new HttpBasicConfigurer<HttpSecurity>()); } ... }
這三個方法最終返回的分別是ExpressionUrlAuthorizationConfigurer、FormLoginConfigurer、HttpBasicConfigurer。事實上,這都是SecurityConfigurerAdapter的子類,SecurityConfigurerAdapter是的SecurityConfigurer接口抽象子類。
而這3個方法內部又都調用了getOrApply方法:
private <C extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity>> C getOrApply( C configurer) throws Exception { C existingConfig = (C) getConfigurer(configurer.getClass()); if (existingConfig != null) { return existingConfig; } return apply(configurer); }
能夠看到這段代碼的主要做用就是在HttpSecurity對象中,添加一個基礎驗證過濾器BasicAuthenticationFilter。
這也驗證咱們咱們以前的說法,能夠認爲經過and()分割後的每段配置,實際上都是在HttpSecuirty中添加一個過濾器。
固然並非每一個SecurityConfigurer都是經過這種方式來建立過濾器的,
例如FormLoginConfigurer就直接在構造方法中來建立一個類型爲UsernamePasswordAuthenticationFilter的過濾器,
源碼以下:
public FormLoginConfigurer() { super(new UsernamePasswordAuthenticationFilter(), null); usernameParameter("username"); passwordParameter("password"); }
大多數狀況下,咱們可能會但願使用本身的登陸頁面。此時咱們能夠按照相似如下的方式配置:
protected void configure(HttpSecurity http) throws Exception { http .authorizeRequests() .anyRequest().authenticated() .and() .formLogin() .loginPage( "/login")// 1 若是沒有此行指定,則會使用內置的登錄頁面 .permitAll(); // 2 由於是自定義的URL,則須要指定認證要求 }
一、更新後的配置,指定了登陸頁面的位置
二、咱們必須容許全部的用戶,無論是否登陸,均可以訪問這個頁面。 formLogin().permitAll()容許全部用戶訪問這個頁面。
如下是一個JSP形式登陸頁面的實現:
<c:url value="/login" var="loginUrl"/> <form action="${loginUrl}" method="post"> 1 <c:if test="${param.error != null}" > 2 <p> Invalid username and password. </p> </c:if> <c:if test="${param.logout != null}" > 3 <p> You have been logged out. </p> </c:if> <p> <label for="username" >Username</ label> <input type="text" id="username" name="username" /> 4 </p> <p> <label for="password" >Password</ label> <input type="password" id="password" name="password" /> 5 </p> <input type="hidden" 6 name="${_csrf.parameterName}" value="${_csrf.token}" /> <button type="submit" class="btn"> Log in</button > </form>
一、向/login URL發送post請求嘗試驗證用戶
二、若是參數有錯,驗證失敗
三、若是註銷參數存在,用戶成功註銷
四、HTTP用戶名參數必須以username命名
五、http密碼參數必須以password命名
六、咱們能夠經過16.4節的"引入 csrf token"和跨站請求僞造章節查看更多的參考信息。
源碼解讀
在這裏,SpringSecurity強制要求咱們的表單登陸頁面必須是以POST方式向/login URL提交請求,並且要求用戶名和密碼的參數名必須是username和password。若是不符合,則不能正常工做。
緣由在於,當咱們調用了HttpSecurity對象的formLogin方法時,其最終會給咱們註冊一個過濾器UsernamePasswordAuthenticationFilter。
咱們來看一下這個過濾器的源碼:
public class UsernamePasswordAuthenticationFilter extends AbstractAuthenticationProcessingFilter { // ~ Static fields/initializers // ===================================================================================== public static final String SPRING_SECURITY_FORM_USERNAME_KEY = "username";//默認的用戶名參數名 public static final String SPRING_SECURITY_FORM_PASSWORD_KEY = "password";//默認的密碼參數名 private String usernameParameter = SPRING_SECURITY_FORM_USERNAME_KEY; private String passwordParameter = SPRING_SECURITY_FORM_PASSWORD_KEY; private boolean postOnly = true; // ~ Constructors // =================================================================================================== public UsernamePasswordAuthenticationFilter() { super(new AntPathRequestMatcher("/login", "POST"));//只對POST請求方式的/login進行攔截 } ... }
此時一切顯得很明瞭,由於這個過濾器中強制指定了必須使用這些值。
當咱們也能夠有方式進行修改用戶名和密碼的參數名,以下:
... .formLogin() .loginPage( "/login") .usernameParameter("uname")//自定義用戶名參數名稱 .passwordParameter("pwd")//自定義密碼參數名稱
不過因爲POST方法請求/login這個URL,由於已經代碼中寫死,所以沒法修改。
請求受權(Authorize Requests)
一、http.authorizeRequests()方法有不少子方法,每一個子匹配器將會按照聲明的順序起做用。
二、指定用戶能夠訪問的多個url模式。特別的,任何用戶能夠訪問以"/resources"開頭的url資源,或者等於"/signup"或about
三、任何以"/admin"開頭的請求限制用戶具備 "ROLE_ADMIN"角色。你可能已經注意的,儘管咱們調用的hasRole方法,可是不用傳入"ROLE_"前綴
四、任何以"/db"開頭的請求同時要求用戶具備"ROLE_ADMIN"和"ROLE_DBA"角色。
五、任何沒有匹配上的其餘的url請求,只須要用戶被驗證。
源碼解讀
在這個案例中咱們調用了antMatchers方法來定義什麼樣的請求能夠放過,什麼樣的請求須要驗證。antMatchers使用的是Ant風格的路徑匹配模式(在下一節咱們會詳細講解)。
這個方法中定以在AbstractRequestMatcherRegistry中,咱們查看一下這個方法的源碼:
public C antMatchers(String... antPatterns) { return chainRequestMatchers(RequestMatchers.antMatchers(antPatterns)); }
這個方法內部又調用了RequestMatchers對象的靜態方法antMatchers方法,源碼以下
public static List<RequestMatcher> antMatchers(HttpMethod httpMethod, String... antPatterns) { String method = httpMethod == null ? null : httpMethod.toString(); List<RequestMatcher> matchers = new ArrayList<RequestMatcher>(); for (String pattern : antPatterns) { matchers.add(new AntPathRequestMatcher(pattern, method)); } return matchers; }
可見最終返回的是一個RequestMatcher列表,事實上,SpringSecurity在工做過程當中,就能夠利用RequestMatcher對象來進行路徑匹配了。
除了ANT風格的路徑匹配模式,咱們還可使用基於正則表達式的路徑匹配模式,對應的方法是regexMatchers(..)。
3.6 Ant Parttern語法
antMatcher使用的是ant風格的路徑匹配模式。
Apache Ant樣式的路徑有三種通配符匹配方法(在下面的表格中列出)這些能夠組合出不少種靈活的路徑模式:
Ant風格路徑匹配的通配符
Wildcard Description
? 匹配任何單字符
* 匹配0或者任意數量的字符,不包含"/"
** 匹配0或者更多的目錄,包含"/"
ANT風格路徑匹配案例
Path Description
/app/*.x 匹配(Matches)全部在app路徑下的.x文件
/app/p?ttern 匹配(Matches) /app/pattern 和 /app/pXttern,可是不包括/app/pttern
/**/example 匹配(Matches) /app/example, /app/foo/example, 和 /example
/app/**/dir/file. 匹配(Matches) /app/dir/file.jsp, /app/foo/dir/file.html,/app/foo/bar/dir/file.pdf, 和 /app/dir/file.java
/**/*.jsp 匹配(Matches)任何的.jsp 文件
3.7 註銷處理
當使用 WebSecurityConfigurerAdapter,註銷功能將會被自動應用,也就是說,就算不寫也有用。
默認狀況下訪問/logout將會將用戶註銷,包含的內容有:
一、使HttpSession失效
二、清空已配置的RememberMe驗證
三、清空 SecurityContextHolder
四、重定向到 /login?success
相似於配置登陸功能,你一樣能夠自定義本身的註銷需求
protected void configure(HttpSecurity http) throws Exception { http .logout() 1 .logoutUrl("/my/logout") 2 .logoutSuccessUrl("/my/index") 3 .logoutSuccessHandler(logoutSuccessHandler) 4 .invalidateHttpSession(true) 5 .addLogoutHandler(logoutHandler) 6 .deleteCookies(cookieNamesToClear) 7 .and() ... }
一、提供註銷支持,當使用 WebSecurityConfigurerAdapter時這將會被自動應用
二、觸發註銷操做的url,默認是/logout。若是開啓了CSRF保護(默認開啓),那麼請求必須是POST方式。
三、註銷操做發生後重定向到的url,默認爲 /login?logout。
四、讓你指定自定義的 LogoutSuccessHandler。若是指定了, logoutSuccessUrl() 將會被忽略。
五、指定在註銷的時候是否銷燬 HttpSession 。默認爲True。
六、添加一個 LogoutHandler。默認狀況下, SecurityContextLogoutHandler 被做爲最後一個 LogoutHandler 。
七、容許指定當註銷成功時要移除的cookie的名稱。這是顯式添加 CookieClearingLogoutHandler 的一種快捷處理方式。
通常狀況下,爲了自定義註銷功能,你能夠添加 LogoutHandler 或者 LogoutSuccessHandler 的實現。
對於不少場景,這些handler經過流式API的方式進行使用。
LogoutHandler
通常而言, LogoutHandler
的實現表示那些能夠參與處理註銷操做的類。它們被指望用於執行一些必要的清理操做。這些實現不該該拋出異常。默認提供瞭如下這些實現:
除了直接提供的 LogoutHandler
的實現,流式API一樣爲每一個 LogoutHandler
提供了快捷方式的實現,
如 deleteCookies()容許指定一個或多個當註銷成功時須要移除的cookie的名稱,它是 CookieClearingLogoutHandler的快捷方式。
LogoutSuccessHandler
LogoutSuccessHandler
在 LogoutFilter成功執行以後被調用,來重定向或者轉發到合適的目的地上,注意這個接口和 LogoutHandler
幾乎同樣,可是能夠拋出異常。默認提供瞭如下實現:
-
HttpStatusReturningLogoutSuccessHandler
正如上面提到的,你不須要直接的指定 SimpleUrlLogoutSuccessHandler
。流式API提供了一個快捷方法來 logoutSuccessUrl()代替。底層仍是使用 SimpleUrlLogoutSuccessHandler
來實現。
當註銷發生以後,會重定向到提供的URL上,默認是 /login?logout
.。
在REST API的場景下,可能會對 HttpStatusReturningLogoutSuccessHandler
感興趣。
當註銷成功後,不是重定向到一個URL,這個 LogoutSuccessHandler
容許你提供一個純文本的 HTTP 狀態碼來返回。若是沒有配置,默認狀況下返回的是200。
更多註銷相關的文檔參考
-
Logging Out in section CSRF Caveats
-
Section Single Logout (CAS protocol)
-
Documentation for the logout element in the Spring Security XML Namespace section
3.9 多個HttpSecurity
咱們能夠配置多個 HttpSecurity 實例,就像咱們能夠在xml文件中配置多個 <http>同樣。關鍵在於屢次擴展 WebSecurityConfigurationAdapter。
例如,如下是一個對於以/api/開頭的url的不一樣配置
@EnableWebSecurity public class MultiHttpSecurityConfig { @Autowired public void configureGlobal(AuthenticationManagerBuilder auth) { auth .inMemoryAuthentication() .withUser("user").password("password").roles("USER").and() .withUser("admin").password("password").roles("USER", "ADMIN"); } @Configuration @Order(1) public static class ApiWebSecurityConfigurationAdapter extends WebSecurityConfigurerAdapter { protected void configure(HttpSecurity http) throws Exception { http .antMatcher("/api/**") .authorizeRequests() .anyRequest().hasRole("ADMIN") .and() .httpBasic(); } } @Configuration public static class FormLoginWebSecurityConfigurerAdapter extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http .authorizeRequests() .anyRequest().authenticated() .and() .formLogin(); } } }
一、按照正常的方式配置驗證
二、建立一個包含 @Order的 WebSecurityConfigurerAdapter實例來指定哪個 WebSecurityConfigurerAdapter應該被首先考慮。
三、http.antMatcher代表這個 HttpSecurity 只適用於以 /api/開頭的URL。
四、建立另外一個 WebSecurityConfigurerAdapter實例。若是URL沒有以 /api/開頭,這個配置將會被使用。
這個配置在 ApiWebSecurityConfigurationAdapter 以後生效,由於其含有一個 @Order值爲1.沒有 @Order默認是最後一個生效。
譯者注:對於一些小的應用,可能咱們的應用的後臺管理界面和前臺用戶界面都集成在一個應用中,這個時候使用這種方式的配置就很是有用。
3.10 方法安全
從2.0版本開始,Spring Security開始對服務層的方法提供事實上的安全支持。其提供對 JSR-250 中註解的支持,同時自身提供了 @Secured註解。
從3.0開始,你可使用基於表達式的註解。你能夠針對單獨的某個Bean應用安全策略,使用攔截方法來裝飾Bean的聲明,或者你能夠Aspect風格的切點來對整個service層中多個Bean進行安全保護。
EnableGlobalMethodSecurity
咱們能夠經過在任何 @Configuration實例上使用 @EnableGlobalMethodSecurity開啓基於註解的安全驗證。
例如,以下配置將會使SpringSecurity的 @Secured註解生效。
@EnableGlobalMethodSecurity(securedEnabled = true) public class MethodSecurityConfig { // ... }
添加一個註解到一個方法(或者類、接口)將會限制對這個方法的訪問。
Spring Secuirty自帶的註解支持爲方法定義一些列屬性。這將會被傳遞給 AccessDecisionManager 來作出事實上的決定。
public interface BankService { @Secured("IS_AUTHENTICATED_ANONYMOUSLY") public Account readAccount(Long id); @Secured("IS_AUTHENTICATED_ANONYMOUSLY") public Account[] findAccounts(); @Secured("ROLE_TELLER") public Account post(Account account, double amount); }
添加一個密碼編碼器
密碼應該始終使用一個安全的,其設計目的就是用來進行密碼編碼的哈希算法進行編碼(而不是標準的算法如SHA和MD5), <password-encoder>元素提供了這種支持。
要想 使用bcrypt來編碼密碼,原來的 authentication provider配置看起來應該像這樣:
<beans:bean name="bcryptEncoder" class="org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder"/> <authentication-manager> <authentication-provider> <password-encoder ref="bcryptEncoder"/> <user-service> <user name="jimi" password="d7e6351eaa13189a5a3641bab846c8e8c69ba39f" authorities="ROLE_USER, ROLE_ADMIN" /> <user name="bob" password="4e7421b1b8765d8f9406d87e7cc6aa784c4ab97f" authorities="ROLE_USER" /> </user-service> </authentication-provider> </authentication-manager>
在大多數狀況下, Bcrypt 是一個好的選擇,除非你有一個遺留系統強制你使用其餘的算法。
若是你使用一個簡單的哈希算法,若是更壞的狀況,存儲純文本形式的密碼,那麼你應該考慮遷移到一個更安全的選擇,例如 Bcrypt 。
添加HTTP/HTTPS通道安全
若是你的應用同時支持HTTP和HTTPS,而且你須要特定的URL只能經過HTTPS進行訪問, <intercept-url>上的屬性 requires-channel對此提供了直接的支持。
<http> <intercept-url pattern="/secure/**" access="ROLE_USER" requires-channel="https"/> <intercept-url pattern="/**" access="ROLE_USER" requires-channel="any"/> ... </http>
進行了這樣的配置後,若是一個用戶嘗試經過HTTP訪問匹配"/secure/**"模式的任何資源,首先將會被重定向到一個HTTPS URL。可用的選項有"http"、"https"和"any",使用any表示HTTP和HTTPS均可以使用。
若是你的應用使用非標準的HTTP、HTTPS端口,你能夠按照以下方式指定一個端口映射列表:
<http> ... <port-mappings> <port-mapping http="9080" https="9443"/> </port-mappings> </http>
注意爲了能夠真正的受到保護,一個應用根本不該該使用HTTP或者在HTTP與HTTPS之間進行切換。應該經過HTTPS,使用安全的連接來避免可能的"中間人"攻擊。
Session管理
檢測超時
你能夠配置spring security來支持檢測一個過時的session ID並將用戶重定向到一個合適的URL。這經過session管理元素實現。
<http> ... <session-management invalid-session-url="/invalidSession.htm" /> </http>
注意,若是你使用這種機制來檢測session超時,若是用戶註銷後,沒有關閉瀏覽器又從新登陸了,可能會錯誤的報告一個異常。
這是由於當你讓session失效時沒有清空session的cookie。你能夠在註銷的時候顯式的指定刪除名爲JSESSIONID的cookie。
例如你能夠在註銷處理器中使用如下的語法:
<http> <logout delete-cookies="JSESSIONID" /> </http>
不幸的是並不能保證在每個servlet容器中這都功能起到做用,因此你須要在本身的環境中進行測試。
提示:若是你在一個代理以後運行本身的應用,你一樣能夠經過配置代理服務器來刪除session cookie。
例如使用 apache HTTPD的mod_headers,如下的指令將會刪除 JSESSIONID cookie,原理是在一個註銷請求的響應中設置cookie過時。(假設應用的部署路徑是 /tutorial)
<LocationMatch "/tutorial/logout">Header always set Set-Cookie "JSESSIONID=;Path=/tutorial;Expires=Thu, 01 Jan 1970 00:00:00 GMT" </LocationMatch>
當前session控制
若是你想限制只有一個用戶能夠登陸你的應用,Spring Security經過如下的簡單附加配置對此提供支持。
首先你須要在你的web.xml文件中添加如下的監聽器來保證session生命週期事件發生時Spring Security被更新。
<listener> <listener-class> org.springframework.security.web.session.HttpSessionEventPublisher </listener-class> </listener>
接着在你的application context中添加如下內容
<http> ... <session-management> <concurrency-control max-sessions="1" /> </session-management> </http>
這將會阻止一個用戶屢次進行登陸--第二次登陸將會致使第一個session失效。
一般狀況下你可能會阻止第二次登陸,在這種場景下你可使用:
<http> ... <session-management> <concurrency-control max-sessions="1" error-if-maximum-exceeded="true" /> </session-management> </http>
二次登陸就會被拒絕,"拒絕"的意思是用戶將會被帶到 authentication-failure-url指定的url(若是使用了基於表單登陸的話)。
若是二次驗證是經過另外的非交互的機制發生的,例如" remember-me",一個「 unauthorized」401錯誤將會被髮送給客戶端。
若是你想用一個錯誤頁面來替代。你能夠在 session-management元素中添加屬性 session-authentication-error-url。
若是你爲表單登陸使用了一個自定義的驗證過濾器,那麼你必須顯式的配置當前session控制的支持。在 Session Management chapter.有更多的細節。
Session Fixation Attack Protection
會話固定攻擊是一個潛在的風險。一個惡意攻擊者經過訪問一個站點來建立一個session。接着讓另一個用戶使用一樣的session進行登陸(例如經過發送一個包含當前session標識符做爲參數的鏈接)。Spring Security對此能夠自動進行包括,經過建立一個新的session或者當用戶登陸後改變session 的標識符。若是你不須要這種保護,或者這與其餘的需求由衝突,你能夠經過 <session-management>的屬性 session-fixation-protection來對這種行爲進行控制,其值有四個選項:
none -不作任何事,當前session仍然被保留。
newSession - 建立一個新的乾淨的session,不拷貝已經存在的session中的數據。 (Spring Security相關的屬性依然會被拷貝).
migrateSession - 建立一個新的session並其拷貝現有session的全部屬性到新的session中。在servlet 3.0或者更早的容器中,這是默認的。
changeSessionId - 不建立一個新的session. 取而代之的是使用Servelt容器提供固定會話保護 (調用HttpServletRequest#changeSessionId())方法.這個選項只有在Servlet 3.1 (Java EE 7)或者更新的容器中才是有效的。
在老版本的容器中指定這個將會產生一個異常. 在servlet 3.1或者更新的容器中,這是默認的。
當固定會話攻擊發生時,會致使application context中一個 SessionFixationProtectionEvent 事件的發佈。若是你使用 changeSessionId,這個保護將會全部 javax.servlet.http.HttpSessionIdListener被通知。因此使用的時候須要當心,若是你的代碼對兩種事件都進行了監聽。閱讀session管理章節獲取更多的細節。
4.4 默認的AccessDecisionManager
當你使用一個命名空間配置,將會爲你註冊一個默認的 AccessDecisionManager 實例,其基於 intercept-url和 protect-pointcut的聲明(或者是註解),來對web URL和方法調用作出一些訪問判斷。
默認的策略是使用帶有 RoleVoter 和 AuthenticatedVoter的 AffirmativeBased 、AccessDecisionManager 。
你能夠在 authorization章節查看更多的信息。
自定義AccessDecisionManager
若是你須要使用一個更加複雜的訪問控制策略,那麼你要知道對於方法安全和web安全設置一個替代者都很是簡單。
對於方法安全,你經過設置 global-method-securit元素的 access-decision-manager-ref屬性 來指向一個在application context中定義的合適的 AccessDecisionManager bean的id。
<global-method-security access-decision-manager-ref="myAccessDecisionManagerBean"> ... </global-method-security>
web安全的語法是相同的,可是是放在<http>元素上
<http access-decision-manager-ref="myAccessDecisionManagerBean"> ... </http>
4.5 驗證管理器和命名空間
Spring Secuirty中提供驗證服務的主要接口是 AuthenticationManager。其是Spring Security ProviderManager類的一個實例
運行時環境
Spring Security 3.0須要Java 5.0或者更高的版本。
因爲Spring Security致力於以一種自包含的方式運行,所以沒有必要在Java運行時環境中放置任何特別的配置文件。
5.1 核心組件
SecurityContextHolder, SecurityContext 和 Authentication 對象
最基礎的對象是 SecurityContextHolder,這是咱們在應用中存儲當前安全細節的地方,包含了當前在應用中使用到的principal細節。
默認狀況下, SecurityContextHolder 使用一個 ThreadLocal 對象來存儲這些細節,這表示對於在同一個線程中執行的方法,安全上下文(security context)都是可用的,即便安全上下文沒有顯式的當作方法的參數進行傳遞。
經過這種方式使用 ThreadLocal 是很安全的,若是當前使用到的規則須要在請求處理完以後被清空。
固然,Spring Security幫你自動的處理了這些,你不須要考慮。
一些應用並不徹底適用使用一個 ThreadLocal,這須要考慮使用線程完成工做的方式。
例如,對於一個Swing客戶端,可能但願在虛擬機中運行的全部線程使用同一個安全上下文(security context)。在應用啓動的時候咱們能夠配置一個策略來指定 SecurityContextHolder如何來存儲安全上下文。對於一個本地應用,你可能會使用 SecurityContextHolder.MODE_GLOBAL策略。
其餘的應用可能但願由安全線程( secure thread)啓動的其餘線程擁有一樣的安全特性。
這能夠經過 SecurityContextHolder.MODE_INHERITABLETHREADLOCAL實現。
你能夠經過2種方式來概念默認的安全策略(SecurityContextHolder.MODE_THREADLOCAL).
第一種方式是設置系統屬性,
第二種方式是調用 SecurityContextHolder的靜態方法。
大部分應用不須要改變默認的配置,若是你確實須要,查看 SecurityContextHolder對象的java doc來獲取更多的信息。
獲取當前用戶信息
在 SecurityContextHolder中咱們存儲了與系統交互的principal相關細節。Spring Security使用 Authentication 對象來描述這些信息。
一般你不須要本身建立 Authentication 對象,可是對於 Authentication對象的查詢是很是常見的。你能夠在應用中的任何地方使用如下代碼塊來獲取當前已驗證的用戶的名字,
例如:
Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal(); if (principal instanceof UserDetails) { String username = ((UserDetails)principal).getUsername(); } else { String username = principal.toString(); }
經過調用 getContext()方法返回的對象是 SecurityContext 接口的一個實例。這個對象被存儲在 thread local裏。就像咱們接下來要看到的,Spring Security大部分的驗證機制經過返回一個 UserDetails 實例做爲 principal。
UserDetailsService
在上面的代碼片斷,咱們須要注意的是能夠從一個 Authentication 對象中獲取一個principal。principal就是一個 Object而已。
大部分狀況下,能夠強制轉換爲一個 UserDetails 對象。 UserDetails 是Spring Secuirty中的一個核心接口。它表明了一個principal,其是是可擴展的而且是應用相關的。
你能夠認爲UserDetails是你本身的用戶數據與Spring Security在 SecurityContextHolder對象中須要使用到的用戶數據的適配器。
做爲你本身的用戶數據的某種表現,你會常常強制轉換 UserDetails 爲應用中原來提供的數據類型,所以你能夠調用特定的業務方法,(例如getEmail(), getEmployeeNumber()等等)。
到如今你可能想知道,何時我應該提供一個 UserDetails 對象?如何來作?我認爲你僅僅是在聲明這件事情,並不須要編寫任何java代碼。簡單的回答是有一個特定的接口 UserDetailsService。這個接口中定義的惟一的方法接受一個String類型的用戶名參數,返回 UserDetails對象。
UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
這是在Spring Secuirty中加載用戶信息最經常使用的方式,而且你將會看到在框架中任何須要獲取一個用戶的信息的時候使用的都是這種方式。
在驗證成功的狀況下, UserDetails
被用來構建 Authentication
對象,並存儲在 SecurityContextHolder
中。一個好消息是咱們已經提供了大量的 UserDetailsService
的實現,包括使用內存中的map( InMemoryDaoImpl),和使用JDBC( JdbcDaoImpl)。儘管如此,大部分用戶傾向於基於使用應用中已經存在的用於描述員工、客戶或者其餘類型用戶的數據訪問對象(Data Access Object )來編寫他們本身的實現。記住不管你的 UserDetailsService
返回類型是什麼,老是能夠在 SecurityContextHolder
中經過以上的代碼片斷來獲取。
提示:讀者常常會對 UserDetailsService產生疑惑。它是一個純粹用於獲取用戶數據的DAO,沒有任何其餘功能,除了提供框架中其餘組件須要的數據。特別的,其並不驗證用戶,驗證是經過 AuthenticationManager完成。在不少場景下,若是咱們須要自定義的驗證過程,須要直接實現 AuthenticationProvider。
GrantedAuthority
除了principal, Authentication
提供的另外一個重要的方法是 getAuthorities()。這個方法用於提供一個 GrantedAuthority
對象數組。 GrantedAuthority
表示的是授予當前的 principal權利。這些權利一般是角色("roles"),例如 ROLE_ADMINISTRATOR
或者 ROLE_HR_SUPERVISOR。這些角色的配置是爲了接下來的web受權、方法受權和域對象受權。Spring Security的其餘部分能夠解析角色中權利,執行某些操做時必須擁有這些權利。 GrantedAuthority
對象一般經過 UserDetailsService加載。
一般 GrantedAuthority
是整個應用範圍的權利許可,並非針對於某個特定的領域對象。所以,你不該該用一個 GrantedAuthority對象表示一個編號爲54的 Employee
對象的許可,由於若是有數以千計的這樣的權利,內存很快會被耗盡(或者至少會引發應用要花費很長的時間才能驗證一個用戶)。固然,Spring Security明確的設計能夠處理這種常見需求,可是做爲替代你應該使用項目的域對象的安全能力來實現這個目標。
總結
這裏僅僅是簡要歸納,目前咱們所看到的Spring Security的主要組成部分是:
SecurityContextHolder,提供對 SecurityContext的訪問
SecurityContext,維護了 Authentication 和可能的特定請求的安全信息
Authentication,以Spring Security的方式描述principal。
GrantedAuthority,表示在應用範圍內授予principal的權利許可。
UserDetailsService,用來根據傳遞的字符串形式的用戶名(或者驗證id等相似信息)來建立 UserDetails 對象。
2基礎配置
首先在項目的pom.xml中引入spring-boot-starter-security依賴。
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency>
和其他Spring框架同樣,XML Configuration和Java Configuration是Spring Security的兩種經常使用配置方式。Spring 3.2版本以後,Java Configuration因其流式API支持,強類型校驗等特性,逐漸替代XML Configuration成爲更普遍的配置方式。下面是一個示例Java Configuration。
@Configuration @EnableWebSecurity @EnableGlobalMethodSecurity(prePostEnabled = true) public class SecurityConfig extends WebSecurityConfigurerAdapter { @Autowired MyUserDetailsService detailsService; @Override protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests() .and().formLogin().loginPage("/login").permitAll().defaultSuccessUrl("/", true) .and().logout().logoutUrl("/logout") .and().sessionManagement().maximumSessions(1).expiredUrl("/expired") .and() .and().exceptionHandling().accessDeniedPage("/accessDenied"); } @Override public void configure(WebSecurity web) throws Exception { web.ignoring().antMatchers("/js/**", "/css/**", "/images/**", "/**/favicon.ico"); } @Override public void configure(AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(detailsService).passwordEncoder(new BCryptPasswordEncoder()); } }
- @EnableWebSecurity: 禁用Boot的默認Security配置,配合@Configuration啓用自定義配置(須要擴展WebSecurityConfigurerAdapter)
- @EnableGlobalMethodSecurity(prePostEnabled = true): 啓用Security註解,例如最經常使用的@PreAuthorize
- configure(HttpSecurity): Request層面的配置,對應XML Configuration中的
<http>
元素 - configure(WebSecurity): Web層面的配置,通常用來配置無需安全檢查的路徑
- configure(AuthenticationManagerBuilder): 身份驗證配置,用於注入自定義身份驗證Bean和密碼校驗規則
3 擴展配置
完成基礎配置以後,下一步就是實現本身的UserDetailsService和PermissionEvaluator,分別用於自定義Principle, Authority和Permission。
@Component public class MyUserDetailsService implements UserDetailsService { @Autowired private LoginService loginService; @Autowired private RoleService roleService; @Override public UserDetails loadUserByUsername(String username) { if (StringUtils.isBlank(username)) { throw new UsernameNotFoundException("用戶名爲空"); } Login login = loginService.findByUsername(username).orElseThrow(() -> new UsernameNotFoundException("用戶不存在")); Set<GrantedAuthority> authorities = new HashSet<>(); roleService.getRoles(login.getId()).forEach(r -> authorities.add(new SimpleGrantedAuthority(r.getName()))); return new org.springframework.security.core.userdetails.User( username, login.getPassword(), true,//是否可用 true,//是否過時 true,//證書不過時爲true true,//帳戶未鎖定爲true authorities); } }
建立GrantedAuthority對象時,通常名稱加上ROLE_前綴。
@Component public class MyPermissionEvaluator implements PermissionEvaluator { @Autowired private LoginService loginService; @Autowired private RoleService roleService; @Override public boolean hasPermission(Authentication authentication, Object targetDomainObject, Object permission) { String username = authentication.getName(); Login login = loginService.findByUsername(username).get(); return roleService.authorized(login.getId(), targetDomainObject.toString(), permission.toString()); } @Override public boolean hasPermission(Authentication authentication, Serializable targetId, String targetType, Object permission) { // not supported return false; } }
- hasPermission(Authentication, Object, Object)和hasPermission(Authentication, Serializable, String, Object)兩個方法分別對應Spring Security中兩個同名的表達式。
4 業務集成
Spring Security提供了註解,Servlet API,JSP Tag,系統API等多種方式進行集成,最經常使用的是第一種方式,包含@Secured, @PreAuthorize, @PreFilter, @PostAuthorize和@PostFilter五個註解。@Secure是最第一版本中的一個註解,自3.0版本引入了支持Spring EL表達式的其他四個註解以後,就不多使用了。
@RequestMapping(value = "/hello", method = RequestMethod.GET) @PreAuthorize("authenticated and hasPermission('hello', 'view')") public String hello(Model model) { String username = SecurityContextHolder.getContext().getAuthentication().getName(); model.addAttribute("message", username); return "hello"; }
- @PreAuthorize("authenticated and hasPermission('hello', 'view')"): 表示只有當前已登陸的而且擁有("hello", "view")權限的用戶才能訪問此頁面
- SecurityContextHolder.getContext().getAuthentication().getName(): 獲取當前登陸的用戶,也能夠經過HttpServletRequest.getRemoteUser()獲取
總結
以上就是Spring Security的通常集成步驟,更多細節和高級特性可參考官方文檔。
參考文章:
http://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/
http://docs.spring.io/spring-security/site/docs/current/reference/htmlsingle/