什么是Spring Security驗證?
提示用戶輸入用戶名和密碼進行登錄。
該系統 (成功) 驗證該用戶名的密碼正確。
獲取該用戶的環境信息 (他們的角色列表等).
為用戶建立安全的環境。
用戶進行,可能執行一些操作,這是潛在的保護的訪問控制機制,檢查所需權限,對當前的安全的環境信息的操作。
前三個項目構成的驗證過程,所以我們將看看這些是如何發生在Spring Security中的。
用戶名和密碼進行組合成一個實例UsernamePasswordAuthenticationToken (一個Authentication接口的實例, 我們之前看到的).
令牌傳遞到AuthenticationManager實例進行驗證。
該AuthenticationManager完全填充Authentication實例返回成功驗證。
安全環境是通過調用 SecurityContextHolder.getContext().setAuthentication(…?), 傳遞到返回的驗證對象建立的。
從這一點上來看,用戶被認為是被驗證的。spring security 驗證的經典例子
import org.springframework.security.authentication.*;
import org.springframework.security.core.*;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
public class AuthenticationExample {
private static AuthenticationManager am = 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);
Authentication result = am.authenticate(request);
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 {
static final List<GrantedAuthority> AUTHORITIES = new ArrayList<GrantedAuthority>();
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");
}
}
直接設置SecurityContextHolder的內容
事實上,Spring Security不介意你如何把Authentication對象包含在SecurityContextHolder內。唯一的關鍵要求是SecurityContextHolder包含Authentication在AbstractSecurityInterceptor之前(我們會看到更多的版本)需要用戶授權操作。
你可以(很多用戶都這樣做)寫一個自己的過濾器或MVC控制器來提供驗證系統的交互,這些都不是基于Spring Security的。比如,你也許使用容器管理認證,從ThreadLocal或JNDI里獲得當前用戶信息。或者,你的公司可能有一個遺留系統,它是一個企業標準,你不能控制它。這種情況下,很容易讓Spring Security工作,也能提供驗證能力。你所需要的就是寫一個過濾器(或等價物)從指定位置讀取第三方用戶信息,把它放到SecurityContextHolder里。在這種情況下,你還需要考慮的事情通常是由內置的認證基礎設施自動照顧。
spring security 支持很多種的認證模式,這些驗證絕大多數都是由第三方提供,或由相關的標準組織,如互聯網工程任務組開發。并且spring security 也提供自己的一組認證功能。
從這些大量的認證模式中抽象封裝就有了spring security的認證模塊
常見的身份驗證有:
- HTTP BASIC 認證頭 (基于 IETF RFC-based 標準)
- HTTP Digest 認證頭 ( IETF RFC-based 標準)
- HTTP X.509 客戶端證書交換 ( IETF RFC-based 標準)
- LDAP (一個非常常見的方法來跨平臺認證需要, 尤其是在大型環境)
- Form-based authentication (用于簡單的用戶界面)
- OpenID 認證
- Authentication based on pre-established request headers (such as Computer Associates Siteminder) 根據預先建立的請求有進行驗證
- JA-SIG Central Authentication Service (CAS,一個開源的SSO系統 )
- Transparent authentication context propagation for Remote Method Invocation (RMI) and HttpInvoker (Spring遠程協議)
- Automatic "remember-me" authentication (你可以勾選一個框以避免預定的時間段再認證)
- Anonymous authentication (讓每一個未經驗證的訪問自動假設為一個特定的安全標識)
- Run-as authentication (在一個訪問應該使用不同的安全標識時非常有用)
- Java Authentication and Authorization Service (JAAS)
- JEE container autentication (所以如果愿你以可以任然使用容器管理的認證)
身份驗證的一些理解
首先,http basic 和http digest ,http 的基本和摘要兩種認證模式,這兩種模式是http 協議規范里面的兩種認證機制,瀏覽器對這兩種機制都會有一個很好的支持。
基本認證模式
基本認證模式
客戶向服務器發送請求,服務器返回401(未授權),要求認證。401消息的頭里面帶了挑戰信息。realm用以區分要不同認證的部分。客戶端收到401后,將用戶名密碼和挑戰信息用BASE64加密形成證書,發送回服務器認證。語法如下:
challenge = "Basic" realm
credentials = "Basic" basic-credentials
示例:
認證頭: WWW-Authenticate: Basic realm="zhouhh@mydomain.com"
證書:Authorization: Basic QsdfgWGHffuIcaNlc2FtZQ== 【虎.無名,格式如Authorization:Basic base64(username:password)。。。但是沒定義如何處理realm信息,簡單處理,可以針對每個realm分別有一組user:pass信息。進一步,可以走md5摘要,但這些已經超出標準,估計不被瀏覽器支持。
摘要模式和基本模式差不多,這兩個模式的核心都是認證頭和證書,只是摘要要復雜一些,并且摘要模式是一個md5 摘要,而basic 只是用base64 編碼了一下,basic 的使用需要配合https 協議,要不然基本就是明文傳輸。
為了防止重放攻擊,采用摘要訪問認證。在客戶發送請求后,收到一個401(未授權)消息,包含一個Challenge。消息里面有一個唯一的字符串:nonce,每次請求都不一樣。客戶將用戶名密碼和401消息返回的挑戰一起加密后傳給服務器。這樣即使有竊聽,他也無法通過每次認證,不能重放攻擊。Http并不是一個安全的協議。其內容都是明文傳輸。因此不要指望Http有多安全。語法如下:
challenge = "Digest" digest-challenge
digest-challenge = 1#( realm | [ domain ] | nonce | [opaque] |[stale] | [algorithm] | [qop-options] | [auth-param] )
domain = "domain" "=" <"> URI ( 1*SP URI ) <">
URI = absoluteURI | abs_path
nonce = "nonce" "=" nonce-value
nonce-value = quoted-string
opaque = "opaque" "=" quoted-string
stale = "stale" "=" ( "true" | "false" )
algorithm = "algorithm" "=" ( "MD5" | "MD5-sess" | token )
qop-options = "qop" "=" <"> 1#qop-value <">
qop-value = "auth" | "auth-int" | token
realm:讓客戶知道使用哪個用戶名和密碼的字符串。不同的領域可能密碼不一樣。至少告訴用戶是什么主機做認證,他可能會提示用哪個用戶名登錄,類似一個Email。
domain:一個URI列表,指示要保護的域。可能是一個列表。提示用戶這些URI采用一樣的認證。如果為空或忽略則為整個服務器。
nonce:隨機字符串,每次401都不一樣。跟算法有關。算法類似Base64加密:time-stamp H(time-stamp ":" ETag ":" private-key) 。time-stamp為服務器時鐘,ETag為請求的Etag頭。private-key為服務器知道的一個值。
opaque:服務器產生的由客戶下去請求時原樣返回。最好是Base64串或十六進制字符串。
auth-param:為擴展用的,現階段忽略。
其他域請參考RFC2617。授權頭語法:
credentials = "Digest" digest-response
digest-response = 1#( username | realm | nonce | digest-uri | response | [ algorithm ] | [cnonce] |
[opaque] | [message-qop] | [nonce-count] | [auth-param] )
username = "username" "=" username-value
username-value = quoted-string
digest-uri = "uri" "=" digest-uri-value
digest-uri-value = request-uri ; As specified by HTTP/1.1
message-qop = "qop" "=" qop-value
cnonce = "cnonce" "=" cnonce-value
cnonce-value = nonce-value
nonce-count = "nc" "=" nc-value
nc-value = 8LHEX
response = "response" "=" request-digest
request-digest = <"> 32LHEX <">
LHEX = "0" | "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9" | "a" | "b" | "c" | "d" | "e" | "f"
response:加密后的密碼
digest-uri:拷貝Request-Line,用于Proxy
cnonce:如果qop設置,才設置,用于雙向認證,防止攻擊。
nonce-count:如果服務器看到同樣的計數,就是一次重放。
示例:
401響應: HTTP/1.1 401 Unauthorized
WWW-Authenticate: Digest
realm="testrealm@host.com",
qop="auth,auth-int",
nonce="dcd98b7102dd2f0e8b11d0f600bfb0c093",
opaque="5ccc069c403ebaf9f0171e9517f40e41"
再次請求:
Authorization: Digest username="Mufasa",
realm="testrealm@host.com",
nonce="dcd98b7102dd2f0e8b11d0f600bfb0c093",
uri="/dir/index.html",
qop=auth,
nc=00000001,
cnonce="0a4f113b",
response="6629fae49393a05397450978507c4ef1",
opaque="5ccc069c403ebaf9f0171e9517f40e41"
下面是一個http basic 的事例,是轉載別的博客上的:
在瀏覽網頁時候,瀏覽器會彈出一個登錄驗證的對話框,如下圖,這就是使用HTTP基本認證。
1、 客戶端發送http request 給服務器,服務器驗證該用戶是否已經登錄驗證過了,如果沒有的話,
服務器會返回一個401 Unauthozied給客戶端,并且在Response 的 header “WWW-Authenticate” 中添加信息。 如下
2、:瀏覽器在接受到401 Unauthozied后,會彈出登錄驗證的對話框。用戶輸入用戶名和密碼后,
瀏覽器用BASE64編碼后,放在Authorization header中發送給服務器。如下圖:
openId和 Oauth 很像都是用于提供第三方登錄。
SecurityContextHolder, SecurityContext和Authentication 對象
最根本的對象是SecurityContextHolder
。我們把當前應用程序的當前安全環境的細節存儲到它里邊了, 它也包含了應用當前使用的主體細節。默認情況下SecurityContextHolder
使用ThreadLocal
存儲這些信息, 這意味著,安全環境在同一個線程執行的方法一直是有效的, 即使這個安全環境沒有作為一個方法參數傳遞到那些方法里。這種情況下使用ThreadLocal
是非常安全的,只要記得在處理完當前主體的請求以后,把這個線程清除就行了。當然,Spring Security自動幫你管理這一切了, 你就不用擔心什么了。
有些程序并不適合使用ThreadLocal
,因為它們處理線程的特殊方法。比如Swing客戶端也許希望Java Virtual Machine里所有的線程 都使用同一個安全環境。SecurityContextHolder
可以配置啟動策略來指定你希望上下文怎么被存儲。對于一個獨立的應用程序,你會使用SecurityContextHolder.MODE_GLOBAL
策略。其他程序可能也想由安全線程產生的線程也承擔同樣的安全標識。這是通過使用SecurityContextHolder.MODE_INHERITABLETHREADLOCAL
實現。你可以通過兩種方式更改默認的SecurityContextHolder.MODE_THREADLOCAL
模式。第一個是設置系統屬性,第二個是調用SecurityContextHolder
的靜態方法。大多數應用程序不需要修改默認值,但是如果你想要修改,可以看一下SecurityContextHolder
的JavaDocs中的詳細信息了解更多。
當前用戶獲取信息
我們在SecurityContextHolder
內存儲目前與應用程序交互的主要細節。Spring Security使用一個Authentication
對象來表示這些信息。 你通常不需要創建一個自我認證的對象,但它是很常見的用戶查詢的Authentication
對象。你可以使用以下代碼塊-從你的應用程序的任何部分-獲得當前身份驗證的用戶的名稱,例如:
Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal();
if (principal instanceof UserDetails) {
String username = ((UserDetails)principal).getUsername();
} else {
String username = principal.toString();
}
通過調用getContext()返回的對象是SecurityContext接口的實例。這是保存在線程本地存儲中的對象。我們將在下面看到,大多數的認證機制以Spring Security返回UserDetails實例為主。
The UserDetailsService
從上面的代碼片段中還可以看出一件事,就是你可以從Authentication對象中獲得安全主體。這個安全主體就是一個Object。大多數情況下,可以強制轉換成UserDetails對象 。 UserDetails是一個Spring Security的核心接口。它代表一個主體,是擴展的,而且是為特定程序服務的。 想一下UserDetails章節,在你自己的用戶數據庫和如何把Spring Security需要的數據放到SecurityContextHolder里。為了讓你自己的用戶數據庫起作用,我們常常把UserDetails轉換成你系統提供的類,這樣你就可以直接調用業務相關的方法了(比如 getEmail(), getEmployeeNumber()等等)。
現在,你可能想知道,我應該什么時候提供這個UserDetails對象呢?我怎么做呢?我想你說這個東西是聲明式的,我不需要寫任何代碼,怎么辦?簡單的回答是,這里有一個特殊的接口叫UserDetailsService。這個接口里的唯一的一個方法,接收String類型的用戶名參數,返回UserDetails:
UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
這是Spring Security用戶加載信息的最常用的方法并且每當需對用戶的信息時你會看到它使用的整個框架。
成功認證后,UserDetails
用于構建存儲在SecurityContextHolder
(詳見 以下)的Authentication
對象。好消息是,我們提供了一些UserDetailsService
的實現,包括一個使用內存映射(InMemoryDaoImpl
)而另一個使用JDBC(JdbcDaoImpl
)。大多數用戶傾向于寫自己的,常常放到已有的數據訪問對象(DAO)上使用這些實現,表示他們的雇員,客戶或其他企業應用中的用戶。記住這個優勢,無論你用UserDetailsService
返回的什么數據都可以通過SecurityContextHolder
獲得,就像上面的代碼片段講的一樣。
GrantedAuthority
除了主體,另一個Authentication提供的重要方法是getAuthorities()。這個方法提供了GrantedAuthority對象數組。毫無疑問,GrantedAuthority是賦予到主體的權限。這些權限通常使用角色表示,比如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特定的主體。
GrantedAuthority, 反應,在應用程序范圍你,賦予主體的權限。
UserDetails,通過你的應用DAO,提供必要的信息,構建Authentication對象。
UserDetailsService, 創建一個UserDetails,傳遞一個 String類型的用戶名(或者證書ID或其他).
The AuthenticationManager, ProviderManager and AuthenticationProvider
public interface AuthenticationManager {
Authentication authenticate(Authentication authentication)
throws AuthenticationException;
}
public class ProviderManager implements AuthenticationManager, MessageSourceAware,
InitializingBean {
...
private List<AuthenticationProvider> providers = Collections.emptyList();
private AuthenticationManager parent;
private boolean eraseCredentialsAfterAuthentication = true;
public ProviderManager(List<AuthenticationProvider> providers) {
this(providers, null);
}
...
// ~ Methods
// ========================================================================================================
...
public Authentication authenticate(Authentication authentication)
throws AuthenticationException {
Class<? extends Authentication> toTest = authentication.getClass();
AuthenticationException lastException = null;
Authentication result = null;
boolean debug = logger.isDebugEnabled();
for (AuthenticationProvider provider : getProviders()) {
if (!provider.supports(toTest)) {
continue;
}
if (debug) {
logger.debug("Authentication attempt using "
+ provider.getClass().getName());
}
try {
result = provider.authenticate(authentication);
if (result != null) {
copyDetails(authentication, result);
break;
}
}
catch (AccountStatusException e) {
prepareException(e, authentication);
// SEC-546: Avoid polling additional providers if auth failure is due to
// invalid account status
throw e;
}
catch (InternalAuthenticationServiceException e) {
prepareException(e, authentication);
throw e;
}
catch (AuthenticationException e) {
lastException = e;
}
}
if (result == null && parent != null) {
// Allow the parent to try.
try {
result = parent.authenticate(authentication);
}
catch (ProviderNotFoundException e) {
// ignore as we will throw below if no other exception occurred prior to
// calling parent and the parent
// may throw ProviderNotFound even though a provider in the child already
// handled the request
}
catch (AuthenticationException e) {
lastException = e;
}
}
if (result != null) {
if (eraseCredentialsAfterAuthentication
&& (result instanceof CredentialsContainer)) {
// Authentication is complete. Remove credentials and other secret data
// from authentication
((CredentialsContainer) result).eraseCredentials();
}
eventPublisher.publishAuthenticationSuccess(result);
return result;
}
// Parent was null, or didn't authenticate (or throw an exception).
if (lastException == null) {
lastException = new ProviderNotFoundException(messages.getMessage(
"ProviderManager.providerNotFound",
new Object[] { toTest.getName() },
"No AuthenticationProvider found for {0}"));
}
prepareException(lastException, authentication);
throw lastException;
}
...
/**
* Copies the authentication details from a source Authentication object to a
* destination one, provided the latter does not already have one set.
*
* @param source source authentication
* @param dest the destination authentication object
*/
private void copyDetails(Authentication source, Authentication dest) {
if ((dest instanceof AbstractAuthenticationToken) && (dest.getDetails() == null)) {
AbstractAuthenticationToken token = (AbstractAuthenticationToken) dest;
token.setDetails(source.getDetails());
}
}
...
}
public interface AuthenticationProvider {
// ~ Methods
// ========================================================================================================
Authentication authenticate(Authentication authentication)
throws AuthenticationException;
boolean supports(Class<?> authentication);
}