什么是Spring Security驗(yàn)證?
提示用戶輸入用戶名和密碼進(jìn)行登錄。
該系統(tǒng) (成功) 驗(yàn)證該用戶名的密碼正確。
獲取該用戶的環(huán)境信息 (他們的角色列表等).
為用戶建立安全的環(huán)境。
用戶進(jìn)行,可能執(zhí)行一些操作,這是潛在的保護(hù)的訪問(wèn)控制機(jī)制,檢查所需權(quán)限,對(duì)當(dāng)前的安全的環(huán)境信息的操作。
前三個(gè)項(xiàng)目構(gòu)成的驗(yàn)證過(guò)程,所以我們將看看這些是如何發(fā)生在Spring Security中的。
用戶名和密碼進(jìn)行組合成一個(gè)實(shí)例UsernamePasswordAuthenticationToken (一個(gè)Authentication接口的實(shí)例, 我們之前看到的).
令牌傳遞到AuthenticationManager實(shí)例進(jìn)行驗(yàn)證。
該AuthenticationManager完全填充Authentication實(shí)例返回成功驗(yàn)證。
安全環(huán)境是通過(guò)調(diào)用 SecurityContextHolder.getContext().setAuthentication(…?), 傳遞到返回的驗(yàn)證對(duì)象建立的。
從這一點(diǎn)上來(lái)看,用戶被認(rèn)為是被驗(yàn)證的。spring security 驗(yàn)證的經(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)容
事實(shí)上,Spring Security不介意你如何把Authentication對(duì)象包含在SecurityContextHolder內(nèi)。唯一的關(guān)鍵要求是SecurityContextHolder包含Authentication在AbstractSecurityInterceptor之前(我們會(huì)看到更多的版本)需要用戶授權(quán)操作。
你可以(很多用戶都這樣做)寫(xiě)一個(gè)自己的過(guò)濾器或MVC控制器來(lái)提供驗(yàn)證系統(tǒng)的交互,這些都不是基于Spring Security的。比如,你也許使用容器管理認(rèn)證,從ThreadLocal或JNDI里獲得當(dāng)前用戶信息。或者,你的公司可能有一個(gè)遺留系統(tǒng),它是一個(gè)企業(yè)標(biāo)準(zhǔn),你不能控制它。這種情況下,很容易讓Spring Security工作,也能提供驗(yàn)證能力。你所需要的就是寫(xiě)一個(gè)過(guò)濾器(或等價(jià)物)從指定位置讀取第三方用戶信息,把它放到SecurityContextHolder里。在這種情況下,你還需要考慮的事情通常是由內(nèi)置的認(rèn)證基礎(chǔ)設(shè)施自動(dòng)照顧。
spring security 支持很多種的認(rèn)證模式,這些驗(yàn)證絕大多數(shù)都是由第三方提供,或由相關(guān)的標(biāo)準(zhǔn)組織,如互聯(lián)網(wǎng)工程任務(wù)組開(kāi)發(fā)。并且spring security 也提供自己的一組認(rèn)證功能。
從這些大量的認(rèn)證模式中抽象封裝就有了spring security的認(rèn)證模塊
常見(jiàn)的身份驗(yà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 客戶端證書(shū)交換 ( IETF RFC-based 標(biāo)準(zhǔn))
- LDAP (一個(gè)非常常見(jiàn)的方法來(lái)跨平臺(tái)認(rèn)證需要, 尤其是在大型環(huán)境)
- Form-based authentication (用于簡(jiǎn)單的用戶界面)
- OpenID 認(rèn)證
- Authentication based on pre-established request headers (such as Computer Associates Siteminder) 根據(jù)預(yù)先建立的請(qǐng)求有進(jìn)行驗(yàn)證
- JA-SIG Central Authentication Service (CAS,一個(gè)開(kāi)源的SSO系統(tǒng) )
- Transparent authentication context propagation for Remote Method Invocation (RMI) and HttpInvoker (Spring遠(yuǎn)程協(xié)議)
- Automatic "remember-me" authentication (你可以勾選一個(gè)框以避免預(yù)定的時(shí)間段再認(rèn)證)
- Anonymous authentication (讓每一個(gè)未經(jīng)驗(yàn)證的訪問(wèn)自動(dòng)假設(shè)為一個(gè)特定的安全標(biāo)識(shí))
- Run-as authentication (在一個(gè)訪問(wèn)應(yīng)該使用不同的安全標(biāo)識(shí)時(shí)非常有用)
- Java Authentication and Authorization Service (JAAS)
- JEE container autentication (所以如果愿你以可以任然使用容器管理的認(rèn)證)
身份驗(yàn)證的一些理解
首先,http basic 和http digest ,http 的基本和摘要兩種認(rèn)證模式,這兩種模式是http 協(xié)議規(guī)范里面的兩種認(rèn)證機(jī)制,瀏覽器對(duì)這兩種機(jī)制都會(huì)有一個(gè)很好的支持。
基本認(rèn)證模式
基本認(rèn)證模式
客戶向服務(wù)器發(fā)送請(qǐng)求,服務(wù)器返回401(未授權(quán)),要求認(rèn)證。401消息的頭里面帶了挑戰(zhàn)信息。realm用以區(qū)分要不同認(rèn)證的部分??蛻舳耸盏?01后,將用戶名密碼和挑戰(zhàn)信息用BASE64加密形成證書(shū),發(fā)送回服務(wù)器認(rèn)證。語(yǔ)法如下:
challenge = "Basic" realm
credentials = "Basic" basic-credentials
示例:
認(rèn)證頭: WWW-Authenticate: Basic realm="zhouhh@mydomain.com"
證書(shū):Authorization: Basic QsdfgWGHffuIcaNlc2FtZQ== 【虎.無(wú)名,格式如Authorization:Basic base64(username:password)。。。但是沒(méi)定義如何處理realm信息,簡(jiǎn)單處理,可以針對(duì)每個(gè)realm分別有一組user:pass信息。進(jìn)一步,可以走md5摘要,但這些已經(jīng)超出標(biāo)準(zhǔn),估計(jì)不被瀏覽器支持。
摘要模式和基本模式差不多,這兩個(gè)模式的核心都是認(rèn)證頭和證書(shū),只是摘要要復(fù)雜一些,并且摘要模式是一個(gè)md5 摘要,而basic 只是用base64 編碼了一下,basic 的使用需要配合https 協(xié)議,要不然基本就是明文傳輸。
為了防止重放攻擊,采用摘要訪問(wèn)認(rèn)證。在客戶發(fā)送請(qǐng)求后,收到一個(gè)401(未授權(quán))消息,包含一個(gè)Challenge。消息里面有一個(gè)唯一的字符串:nonce,每次請(qǐng)求都不一樣??蛻魧⒂脩裘艽a和401消息返回的挑戰(zhàn)一起加密后傳給服務(wù)器。這樣即使有竊聽(tīng),他也無(wú)法通過(guò)每次認(rèn)證,不能重放攻擊。Http并不是一個(gè)安全的協(xié)議。其內(nèi)容都是明文傳輸。因此不要指望Http有多安全。語(yǔ)法如下:
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:讓客戶知道使用哪個(gè)用戶名和密碼的字符串。不同的領(lǐng)域可能密碼不一樣。至少告訴用戶是什么主機(jī)做認(rèn)證,他可能會(huì)提示用哪個(gè)用戶名登錄,類似一個(gè)Email。
domain:一個(gè)URI列表,指示要保護(hù)的域??赡苁且粋€(gè)列表。提示用戶這些URI采用一樣的認(rèn)證。如果為空或忽略則為整個(gè)服務(wù)器。
nonce:隨機(jī)字符串,每次401都不一樣。跟算法有關(guān)。算法類似Base64加密:time-stamp H(time-stamp ":" ETag ":" private-key) 。time-stamp為服務(wù)器時(shí)鐘,ETag為請(qǐng)求的Etag頭。private-key為服務(wù)器知道的一個(gè)值。
opaque:服務(wù)器產(chǎn)生的由客戶下去請(qǐng)求時(shí)原樣返回。最好是Base64串或十六進(jìn)制字符串。
auth-param:為擴(kuò)展用的,現(xiàn)階段忽略。
其他域請(qǐng)參考RFC2617。授權(quán)頭語(yǔ)法:
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ù)器看到同樣的計(jì)數(shù),就是一次重放。
示例:
401響應(yīng): HTTP/1.1 401 Unauthorized
WWW-Authenticate: Digest
realm="testrealm@host.com",
qop="auth,auth-int",
nonce="dcd98b7102dd2f0e8b11d0f600bfb0c093",
opaque="5ccc069c403ebaf9f0171e9517f40e41"
再次請(qǐng)求:
Authorization: Digest username="Mufasa",
realm="testrealm@host.com",
nonce="dcd98b7102dd2f0e8b11d0f600bfb0c093",
uri="/dir/index.html",
qop=auth,
nc=00000001,
cnonce="0a4f113b",
response="6629fae49393a05397450978507c4ef1",
opaque="5ccc069c403ebaf9f0171e9517f40e41"
下面是一個(gè)http basic 的事例,是轉(zhuǎn)載別的博客上的:
在瀏覽網(wǎng)頁(yè)時(shí)候,瀏覽器會(huì)彈出一個(gè)登錄驗(yàn)證的對(duì)話框,如下圖,這就是使用HTTP基本認(rèn)證。

1、 客戶端發(fā)送http request 給服務(wù)器,服務(wù)器驗(yàn)證該用戶是否已經(jīng)登錄驗(yàn)證過(guò)了,如果沒(méi)有的話,
服務(wù)器會(huì)返回一個(gè)401 Unauthozied給客戶端,并且在Response 的 header “WWW-Authenticate” 中添加信息。 如下

2、:瀏覽器在接受到401 Unauthozied后,會(huì)彈出登錄驗(yàn)證的對(duì)話框。用戶輸入用戶名和密碼后,
瀏覽器用BASE64編碼后,放在Authorization header中發(fā)送給服務(wù)器。如下圖:

openId和 Oauth 很像都是用于提供第三方登錄。
SecurityContextHolder, SecurityContext和Authentication 對(duì)象
最根本的對(duì)象是SecurityContextHolder。我們把當(dāng)前應(yīng)用程序的當(dāng)前安全環(huán)境的細(xì)節(jié)存儲(chǔ)到它里邊了, 它也包含了應(yīng)用當(dāng)前使用的主體細(xì)節(jié)。默認(rèn)情況下SecurityContextHolder使用ThreadLocal存儲(chǔ)這些信息, 這意味著,安全環(huán)境在同一個(gè)線程執(zhí)行的方法一直是有效的, 即使這個(gè)安全環(huán)境沒(méi)有作為一個(gè)方法參數(shù)傳遞到那些方法里。這種情況下使用ThreadLocal是非常安全的,只要記得在處理完當(dāng)前主體的請(qǐng)求以后,把這個(gè)線程清除就行了。當(dāng)然,Spring Security自動(dòng)幫你管理這一切了, 你就不用擔(dān)心什么了。
有些程序并不適合使用ThreadLocal,因?yàn)樗鼈兲幚砭€程的特殊方法。比如Swing客戶端也許希望Java Virtual Machine里所有的線程 都使用同一個(gè)安全環(huán)境。SecurityContextHolder可以配置啟動(dòng)策略來(lái)指定你希望上下文怎么被存儲(chǔ)。對(duì)于一個(gè)獨(dú)立的應(yīng)用程序,你會(huì)使用SecurityContextHolder.MODE_GLOBAL策略。其他程序可能也想由安全線程產(chǎn)生的線程也承擔(dān)同樣的安全標(biāo)識(shí)。這是通過(guò)使用SecurityContextHolder.MODE_INHERITABLETHREADLOCAL實(shí)現(xiàn)。你可以通過(guò)兩種方式更改默認(rèn)的SecurityContextHolder.MODE_THREADLOCAL模式。第一個(gè)是設(shè)置系統(tǒng)屬性,第二個(gè)是調(diào)用SecurityContextHolder的靜態(tài)方法。大多數(shù)應(yīng)用程序不需要修改默認(rèn)值,但是如果你想要修改,可以看一下SecurityContextHolder的JavaDocs中的詳細(xì)信息了解更多。
當(dāng)前用戶獲取信息
我們?cè)?code>SecurityContextHolder內(nèi)存儲(chǔ)目前與應(yīng)用程序交互的主要細(xì)節(jié)。Spring Security使用一個(gè)Authentication對(duì)象來(lái)表示這些信息。 你通常不需要?jiǎng)?chuàng)建一個(gè)自我認(rèn)證的對(duì)象,但它是很常見(jiàn)的用戶查詢的Authentication對(duì)象。你可以使用以下代碼塊-從你的應(yīng)用程序的任何部分-獲得當(dāng)前身份驗(yàn)證的用戶的名稱,例如:
Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal();
if (principal instanceof UserDetails) {
String username = ((UserDetails)principal).getUsername();
} else {
String username = principal.toString();
}
通過(guò)調(diào)用getContext()返回的對(duì)象是SecurityContext接口的實(shí)例。這是保存在線程本地存儲(chǔ)中的對(duì)象。我們將在下面看到,大多數(shù)的認(rèn)證機(jī)制以Spring Security返回UserDetails實(shí)例為主。
The UserDetailsService
從上面的代碼片段中還可以看出一件事,就是你可以從Authentication對(duì)象中獲得安全主體。這個(gè)安全主體就是一個(gè)Object。大多數(shù)情況下,可以強(qiáng)制轉(zhuǎn)換成UserDetails對(duì)象 。 UserDetails是一個(gè)Spring Security的核心接口。它代表一個(gè)主體,是擴(kuò)展的,而且是為特定程序服務(wù)的。 想一下UserDetails章節(jié),在你自己的用戶數(shù)據(jù)庫(kù)和如何把Spring Security需要的數(shù)據(jù)放到SecurityContextHolder里。為了讓你自己的用戶數(shù)據(jù)庫(kù)起作用,我們常常把UserDetails轉(zhuǎn)換成你系統(tǒng)提供的類,這樣你就可以直接調(diào)用業(yè)務(wù)相關(guān)的方法了(比如 getEmail(), getEmployeeNumber()等等)。
現(xiàn)在,你可能想知道,我應(yīng)該什么時(shí)候提供這個(gè)UserDetails對(duì)象呢?我怎么做呢?我想你說(shuō)這個(gè)東西是聲明式的,我不需要寫(xiě)任何代碼,怎么辦?簡(jiǎn)單的回答是,這里有一個(gè)特殊的接口叫UserDetailsService。這個(gè)接口里的唯一的一個(gè)方法,接收String類型的用戶名參數(shù),返回UserDetails:
UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
這是Spring Security用戶加載信息的最常用的方法并且每當(dāng)需對(duì)用戶的信息時(shí)你會(huì)看到它使用的整個(gè)框架。
成功認(rèn)證后,UserDetails用于構(gòu)建存儲(chǔ)在SecurityContextHolder(詳見(jiàn) 以下)的Authentication對(duì)象。好消息是,我們提供了一些UserDetailsService的實(shí)現(xiàn),包括一個(gè)使用內(nèi)存映射(InMemoryDaoImpl)而另一個(gè)使用JDBC(JdbcDaoImpl)。大多數(shù)用戶傾向于寫(xiě)自己的,常常放到已有的數(shù)據(jù)訪問(wèn)對(duì)象(DAO)上使用這些實(shí)現(xiàn),表示他們的雇員,客戶或其他企業(yè)應(yīng)用中的用戶。記住這個(gè)優(yōu)勢(shì),無(wú)論你用UserDetailsService返回的什么數(shù)據(jù)都可以通過(guò)SecurityContextHolder獲得,就像上面的代碼片段講的一樣。
GrantedAuthority
除了主體,另一個(gè)Authentication提供的重要方法是getAuthorities()。這個(gè)方法提供了GrantedAuthority對(duì)象數(shù)組。毫無(wú)疑問(wèn),GrantedAuthority是賦予到主體的權(quán)限。這些權(quán)限通常使用角色表示,比如ROLE_ADMINISTRATOR或ROLE_HR_SUPERVISOR。這些角色會(huì)在后面,對(duì)web驗(yàn)證,方法驗(yàn)證和領(lǐng)域?qū)ο篁?yàn)證進(jìn)行配置。Spring Security的其他部分用來(lái)攔截這些權(quán)限,期望他們被表現(xiàn)出現(xiàn)。GrantedAuthority對(duì)象通常是使用UserDetailsService讀取的。
通常情況下,GrantedAuthority對(duì)象是應(yīng)用程序范圍下的授權(quán)。它們不會(huì)特意分配給一個(gè)特定的領(lǐng)域?qū)ο?。因此,你不能設(shè)置一個(gè)GrantedAuthority,讓他有權(quán)限展示編號(hào)54的Employee對(duì)象,因?yàn)槿绻谐汕先f(wàn)的這種授權(quán),你會(huì)很快用光內(nèi)存(或者,至少,導(dǎo)致程序花費(fèi)大量時(shí)間去驗(yàn)證一個(gè)用戶)。當(dāng)然,Spring Security被明確設(shè)計(jì)成處理常見(jiàn)的需求,但是你最好別因?yàn)檫@個(gè)目的使用項(xiàng)目領(lǐng)域模型安全功能。
Spring Security主要由以下幾部分組成的:
SecurityContextHolder, 提供幾種訪問(wèn) SecurityContext的方式。
SecurityContext, 保存Authentication信息和請(qǐng)求對(duì)應(yīng)的安全信息。
Authentication, 展示Spring Security特定的主體。
GrantedAuthority, 反應(yīng),在應(yīng)用程序范圍你,賦予主體的權(quán)限。
UserDetails,通過(guò)你的應(yīng)用DAO,提供必要的信息,構(gòu)建Authentication對(duì)象。
UserDetailsService, 創(chuàng)建一個(gè)UserDetails,傳遞一個(gè) String類型的用戶名(或者證書(shū)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);
}