什么是Spring Security驗證?
提示用戶輸入用戶名和密碼進行登錄饵蒂。
該系統(tǒng) (成功) 驗證該用戶名的密碼正確。
獲取該用戶的環(huán)境信息 (他們的角色列表等).
為用戶建立安全的環(huán)境最仑。
用戶進行迹栓,可能執(zhí)行一些操作,這是潛在的保護的訪問控制機制踏枣,檢查所需權(quán)限七兜,對當(dāng)前的安全的環(huán)境信息的操作丸凭。
前三個項目構(gòu)成的驗證過程,所以我們將看看這些是如何發(fā)生在Spring Security中的腕铸。
用戶名和密碼進行組合成一個實例UsernamePasswordAuthenticationToken (一個Authentication接口的實例, 我們之前看到的).
令牌傳遞到AuthenticationManager實例進行驗證惜犀。
該AuthenticationManager完全填充Authentication實例返回成功驗證。
安全環(huán)境是通過調(diào)用 SecurityContextHolder.getContext().setAuthentication(…?), 傳遞到返回的驗證對象建立的恬惯。
從這一點上來看向拆,用戶被認(rèn)為是被驗證的。spring security 驗證的經(jīng)典例子
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");
}
}
直接設(shè)置SecurityContextHolder的內(nèi)容
事實上酪耳,Spring Security不介意你如何把Authentication對象包含在SecurityContextHolder內(nèi)浓恳。唯一的關(guān)鍵要求是SecurityContextHolder包含Authentication在AbstractSecurityInterceptor之前(我們會看到更多的版本)需要用戶授權(quán)操作刹缝。
你可以(很多用戶都這樣做)寫一個自己的過濾器或MVC控制器來提供驗證系統(tǒng)的交互,這些都不是基于Spring Security的颈将。比如梢夯,你也許使用容器管理認(rèn)證,從ThreadLocal或JNDI里獲得當(dāng)前用戶信息晴圾∷淘遥或者,你的公司可能有一個遺留系統(tǒng)死姚,它是一個企業(yè)標(biāo)準(zhǔn)人乓,你不能控制它。這種情況下都毒,很容易讓Spring Security工作色罚,也能提供驗證能力。你所需要的就是寫一個過濾器(或等價物)從指定位置讀取第三方用戶信息账劲,把它放到SecurityContextHolder里戳护。在這種情況下,你還需要考慮的事情通常是由內(nèi)置的認(rèn)證基礎(chǔ)設(shè)施自動照顧瀑焦。
spring security 支持很多種的認(rèn)證模式腌且,這些驗證絕大多數(shù)都是由第三方提供,或由相關(guān)的標(biāo)準(zhǔn)組織榛瓮,如互聯(lián)網(wǎng)工程任務(wù)組開發(fā)铺董。并且spring security 也提供自己的一組認(rèn)證功能。
從這些大量的認(rèn)證模式中抽象封裝就有了spring security的認(rèn)證模塊
常見的身份驗證有:
- HTTP BASIC 認(rèn)證頭 (基于 IETF RFC-based 標(biāo)準(zhǔn))
- HTTP Digest 認(rèn)證頭 ( IETF RFC-based 標(biāo)準(zhǔn))
- HTTP X.509 客戶端證書交換 ( IETF RFC-based 標(biāo)準(zhǔn))
- LDAP (一個非常常見的方法來跨平臺認(rèn)證需要, 尤其是在大型環(huán)境)
- Form-based authentication (用于簡單的用戶界面)
- OpenID 認(rèn)證
- Authentication based on pre-established request headers (such as Computer Associates Siteminder) 根據(jù)預(yù)先建立的請求有進行驗證
- JA-SIG Central Authentication Service (CAS禀晓,一個開源的SSO系統(tǒng) )
- Transparent authentication context propagation for Remote Method Invocation (RMI) and HttpInvoker (Spring遠(yuǎn)程協(xié)議)
- Automatic "remember-me" authentication (你可以勾選一個框以避免預(yù)定的時間段再認(rèn)證)
- Anonymous authentication (讓每一個未經(jīng)驗證的訪問自動假設(shè)為一個特定的安全標(biāo)識)
- Run-as authentication (在一個訪問應(yīng)該使用不同的安全標(biāo)識時非常有用)
- Java Authentication and Authorization Service (JAAS)
- JEE container autentication (所以如果愿你以可以任然使用容器管理的認(rèn)證)
身份驗證的一些理解
首先柄粹,http basic 和http digest ,http 的基本和摘要兩種認(rèn)證模式,這兩種模式是http 協(xié)議規(guī)范里面的兩種認(rèn)證機制匆绣,瀏覽器對這兩種機制都會有一個很好的支持。
基本認(rèn)證模式
基本認(rèn)證模式
客戶向服務(wù)器發(fā)送請求什黑,服務(wù)器返回401(未授權(quán))崎淳,要求認(rèn)證。401消息的頭里面帶了挑戰(zhàn)信息愕把。realm用以區(qū)分要不同認(rèn)證的部分拣凹。客戶端收到401后恨豁,將用戶名密碼和挑戰(zhàn)信息用BASE64加密形成證書嚣镜,發(fā)送回服務(wù)器認(rèn)證。語法如下:
challenge = "Basic" realm
credentials = "Basic" basic-credentials
示例:
認(rèn)證頭: WWW-Authenticate: Basic realm="zhouhh@mydomain.com"
證書:Authorization: Basic QsdfgWGHffuIcaNlc2FtZQ== 【虎.無名橘蜜,格式如Authorization:Basic base64(username:password)菊匿。付呕。。但是沒定義如何處理realm信息跌捆,簡單處理徽职,可以針對每個realm分別有一組user:pass信息。進一步佩厚,可以走md5摘要姆钉,但這些已經(jīng)超出標(biāo)準(zhǔn),估計不被瀏覽器支持抄瓦。
摘要模式和基本模式差不多潮瓶,這兩個模式的核心都是認(rèn)證頭和證書,只是摘要要復(fù)雜一些钙姊,并且摘要模式是一個md5 摘要毯辅,而basic 只是用base64 編碼了一下,basic 的使用需要配合https 協(xié)議摸恍,要不然基本就是明文傳輸悉罕。
為了防止重放攻擊,采用摘要訪問認(rèn)證立镶。在客戶發(fā)送請求后壁袄,收到一個401(未授權(quán))消息,包含一個Challenge媚媒。消息里面有一個唯一的字符串:nonce嗜逻,每次請求都不一樣$哉伲客戶將用戶名密碼和401消息返回的挑戰(zhàn)一起加密后傳給服務(wù)器栈顷。這樣即使有竊聽,他也無法通過每次認(rèn)證嵌巷,不能重放攻擊萄凤。Http并不是一個安全的協(xié)議。其內(nèi)容都是明文傳輸搪哪。因此不要指望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:讓客戶知道使用哪個用戶名和密碼的字符串。不同的領(lǐng)域可能密碼不一樣晓折。至少告訴用戶是什么主機做認(rèn)證惑朦,他可能會提示用哪個用戶名登錄,類似一個Email漓概。
domain:一個URI列表漾月,指示要保護的域∥刚洌可能是一個列表梁肿。提示用戶這些URI采用一樣的認(rèn)證蜓陌。如果為空或忽略則為整個服務(wù)器。
nonce:隨機字符串栈雳,每次401都不一樣护奈。跟算法有關(guān)。算法類似Base64加密:time-stamp H(time-stamp ":" ETag ":" private-key) 哥纫。time-stamp為服務(wù)器時鐘霉旗,ETag為請求的Etag頭。private-key為服務(wù)器知道的一個值蛀骇。
opaque:服務(wù)器產(chǎn)生的由客戶下去請求時原樣返回厌秒。最好是Base64串或十六進制字符串。
auth-param:為擴展用的擅憔,現(xiàn)階段忽略鸵闪。
其他域請參考RFC2617。授權(quán)頭語法:
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設(shè)置蚌讼,才設(shè)置,用于雙向認(rèn)證个榕,防止攻擊篡石。
nonce-count:如果服務(wù)器看到同樣的計數(shù),就是一次重放西采。
示例:
401響應(yīng): 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 的事例凰萨,是轉(zhuǎn)載別的博客上的:
在瀏覽網(wǎng)頁時候,瀏覽器會彈出一個登錄驗證的對話框械馆,如下圖胖眷,這就是使用HTTP基本認(rèn)證。
1霹崎、 客戶端發(fā)送http request 給服務(wù)器,服務(wù)器驗證該用戶是否已經(jīng)登錄驗證過了珊搀,如果沒有的話,
服務(wù)器會返回一個401 Unauthozied給客戶端尾菇,并且在Response 的 header “WWW-Authenticate” 中添加信息食棕。 如下
2、:瀏覽器在接受到401 Unauthozied后错沽,會彈出登錄驗證的對話框。用戶輸入用戶名和密碼后眶拉,
瀏覽器用BASE64編碼后千埃,放在Authorization header中發(fā)送給服務(wù)器。如下圖:
openId和 Oauth 很像都是用于提供第三方登錄忆植。
SecurityContextHolder, SecurityContext和Authentication 對象
最根本的對象是SecurityContextHolder
放可。我們把當(dāng)前應(yīng)用程序的當(dāng)前安全環(huán)境的細(xì)節(jié)存儲到它里邊了谒臼, 它也包含了應(yīng)用當(dāng)前使用的主體細(xì)節(jié)。默認(rèn)情況下SecurityContextHolder
使用ThreadLocal
存儲這些信息耀里, 這意味著蜈缤,安全環(huán)境在同一個線程執(zhí)行的方法一直是有效的, 即使這個安全環(huán)境沒有作為一個方法參數(shù)傳遞到那些方法里冯挎。這種情況下使用ThreadLocal
是非常安全的底哥,只要記得在處理完當(dāng)前主體的請求以后,把這個線程清除就行了房官。當(dāng)然趾徽,Spring Security自動幫你管理這一切了, 你就不用擔(dān)心什么了翰守。
有些程序并不適合使用ThreadLocal
孵奶,因為它們處理線程的特殊方法。比如Swing客戶端也許希望Java Virtual Machine里所有的線程 都使用同一個安全環(huán)境蜡峰。SecurityContextHolder
可以配置啟動策略來指定你希望上下文怎么被存儲了袁。對于一個獨立的應(yīng)用程序,你會使用SecurityContextHolder.MODE_GLOBAL
策略湿颅。其他程序可能也想由安全線程產(chǎn)生的線程也承擔(dān)同樣的安全標(biāo)識载绿。這是通過使用SecurityContextHolder.MODE_INHERITABLETHREADLOCAL
實現(xiàn)。你可以通過兩種方式更改默認(rèn)的SecurityContextHolder.MODE_THREADLOCAL
模式肖爵。第一個是設(shè)置系統(tǒng)屬性卢鹦,第二個是調(diào)用SecurityContextHolder
的靜態(tài)方法。大多數(shù)應(yīng)用程序不需要修改默認(rèn)值劝堪,但是如果你想要修改冀自,可以看一下SecurityContextHolder
的JavaDocs中的詳細(xì)信息了解更多。
當(dāng)前用戶獲取信息
我們在SecurityContextHolder
內(nèi)存儲目前與應(yīng)用程序交互的主要細(xì)節(jié)秒啦。Spring Security使用一個Authentication
對象來表示這些信息熬粗。 你通常不需要創(chuàng)建一個自我認(rèn)證的對象,但它是很常見的用戶查詢的Authentication
對象。你可以使用以下代碼塊-從你的應(yīng)用程序的任何部分-獲得當(dāng)前身份驗證的用戶的名稱余境,例如:
Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal();
if (principal instanceof UserDetails) {
String username = ((UserDetails)principal).getUsername();
} else {
String username = principal.toString();
}
通過調(diào)用getContext()返回的對象是SecurityContext接口的實例驻呐。這是保存在線程本地存儲中的對象。我們將在下面看到芳来,大多數(shù)的認(rèn)證機制以Spring Security返回UserDetails實例為主含末。
The UserDetailsService
從上面的代碼片段中還可以看出一件事,就是你可以從Authentication對象中獲得安全主體即舌。這個安全主體就是一個Object佣盒。大多數(shù)情況下,可以強制轉(zhuǎn)換成UserDetails對象 顽聂。 UserDetails是一個Spring Security的核心接口肥惭。它代表一個主體盯仪,是擴展的,而且是為特定程序服務(wù)的蜜葱。 想一下UserDetails章節(jié)全景,在你自己的用戶數(shù)據(jù)庫和如何把Spring Security需要的數(shù)據(jù)放到SecurityContextHolder里。為了讓你自己的用戶數(shù)據(jù)庫起作用牵囤,我們常常把UserDetails轉(zhuǎn)換成你系統(tǒng)提供的類爸黄,這樣你就可以直接調(diào)用業(yè)務(wù)相關(guān)的方法了(比如 getEmail(), getEmployeeNumber()等等)。
現(xiàn)在奔浅,你可能想知道馆纳,我應(yīng)該什么時候提供這個UserDetails對象呢?我怎么做呢?我想你說這個東西是聲明式的,我不需要寫任何代碼汹桦,怎么辦?簡單的回答是鲁驶,這里有一個特殊的接口叫UserDetailsService。這個接口里的唯一的一個方法舞骆,接收String類型的用戶名參數(shù)钥弯,返回UserDetails:
UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
這是Spring Security用戶加載信息的最常用的方法并且每當(dāng)需對用戶的信息時你會看到它使用的整個框架。
成功認(rèn)證后督禽,UserDetails
用于構(gòu)建存儲在SecurityContextHolder
(詳見 以下)的Authentication
對象脆霎。好消息是,我們提供了一些UserDetailsService
的實現(xiàn)狈惫,包括一個使用內(nèi)存映射(InMemoryDaoImpl
)而另一個使用JDBC(JdbcDaoImpl
)睛蛛。大多數(shù)用戶傾向于寫自己的,常常放到已有的數(shù)據(jù)訪問對象(DAO)上使用這些實現(xiàn)胧谈,表示他們的雇員忆肾,客戶或其他企業(yè)應(yīng)用中的用戶。記住這個優(yōu)勢菱肖,無論你用UserDetailsService
返回的什么數(shù)據(jù)都可以通過SecurityContextHolder
獲得客冈,就像上面的代碼片段講的一樣。
GrantedAuthority
除了主體稳强,另一個Authentication提供的重要方法是getAuthorities()场仲。這個方法提供了GrantedAuthority對象數(shù)組。毫無疑問退疫,GrantedAuthority是賦予到主體的權(quán)限渠缕。這些權(quán)限通常使用角色表示,比如ROLE_ADMINISTRATOR或ROLE_HR_SUPERVISOR褒繁。這些角色會在后面亦鳞,對web驗證,方法驗證和領(lǐng)域?qū)ο篁炞C進行配置。Spring Security的其他部分用來攔截這些權(quán)限蚜迅,期望他們被表現(xiàn)出現(xiàn)。GrantedAuthority對象通常是使用UserDetailsService讀取的俊抵。
通常情況下谁不,GrantedAuthority對象是應(yīng)用程序范圍下的授權(quán)。它們不會特意分配給一個特定的領(lǐng)域?qū)ο蠡栈濉R虼松才粒悴荒茉O(shè)置一個GrantedAuthority,讓他有權(quán)限展示編號54的Employee對象谎替,因為如果有成千上萬的這種授權(quán)偷溺,你會很快用光內(nèi)存(或者,至少钱贯,導(dǎo)致程序花費大量時間去驗證一個用戶)挫掏。當(dāng)然,Spring Security被明確設(shè)計成處理常見的需求秩命,但是你最好別因為這個目的使用項目領(lǐng)域模型安全功能尉共。
Spring Security主要由以下幾部分組成的:
SecurityContextHolder, 提供幾種訪問 SecurityContext的方式。
SecurityContext, 保存Authentication信息和請求對應(yīng)的安全信息弃锐。
Authentication, 展示Spring Security特定的主體袄友。
GrantedAuthority, 反應(yīng),在應(yīng)用程序范圍你霹菊,賦予主體的權(quán)限剧蚣。
UserDetails,通過你的應(yīng)用DAO,提供必要的信息旋廷,構(gòu)建Authentication對象鸠按。
UserDetailsService, 創(chuàng)建一個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);
}