1. 用戶名密碼認(rèn)證
用戶名密碼讀取方式有三:
- 表單
- Basic認(rèn)證
- Digest認(rèn)證
存儲(chǔ)機(jī)制:
- 內(nèi)存存儲(chǔ)
- JDBC存儲(chǔ)
- 自定義存儲(chǔ) UserDetailsService
- LDAP存儲(chǔ)
1.1 表單登錄
用戶是如何被重定向到登錄表單的:
- 用戶向/private發(fā)起未認(rèn)證請求.
-
FilterSecurityInterceptor
通過拋出AccessDeniedException
表示拒絕這個(gè)請求. - 因?yàn)橛脩粑凑J(rèn)證,
ExceptionTranslationFilter
開始認(rèn)證過程并通過AuthenticationEntryPoint
發(fā)送一個(gè)重定向到登錄頁面的響應(yīng). - 瀏覽器重定向到登錄頁面.
- 顯示登錄頁面.
用戶提交用戶名密碼后的處理過程:
- 當(dāng)用戶提交用戶名密碼后,
UserPasswordAuthenticationFilter
創(chuàng)建一個(gè)UsernamePasswordAuthenticationToken
(從請求中解析用戶名密碼) - token被傳遞給
AuthenticationManager
進(jìn)行認(rèn)證. - 后續(xù)過程同
AbstractAuthenticationProcessingFilter
(UserPasswordAuthenticationFilter繼承自AbstractAuthenticationProcessingFilter
)
Spring Security 的表單登錄功能默認(rèn)開啟, 但是如果提供了任何基于servlet的配置, 則需要顯式配置:
// java
protected void configure(HttpSecurity http) {
http
// ...
.formLogin(withDefaults());
}
// xml
<http>
<form-login />
</http>
自定義登錄頁面
// java
protected void configure(HttpSecurity http) throws Exception {
http
// ...
.formLogin(form -> form
.loginPage("/login")
.permitAll()
);
}
// xml
<http>
<intercept-url pattern="/login" access="permitAll" />
<form-login login-page="/login" />
</http>
登錄表單
- 表單通過
/login
POST請求提交登錄數(shù)據(jù) - 表單需要包含一個(gè)CSRF token.
- 用戶名參數(shù)名為 username
- 密碼參數(shù)為 password
以上都可以自行配置, 如自定義登錄頁面, 則需要提供一個(gè)get方式的login請求, 以跳轉(zhuǎn)到登錄頁面.
java配置見FormLoginConfigurer
. XML配置見<http>
1.2 Basic驗(yàn)證
Basic驗(yàn)證使用的是BasicAuthenticationEntryPoint
.
配置:
// java
protected void configure(HttpSecurity http) {
http
// ...
.httpBasic(withDefaults());
}
// xml
<http>
<http-basic />
</http>
1.3 UserDetailsService
UserDetailsService
通過使用DaoAuthenticationProvider
來獲取用戶名,密碼及其他擴(kuò)展信息.
1.4 PasswordEncoder
Spring Security加密支持. 常用實(shí)現(xiàn)類為BCryptPasswordEncoder
.
1.5 DaoAuthenticationProvider
AuthenticationProvider
的實(shí)現(xiàn)類, 通過UserDetailsService
和PasswordEncoder
支持驗(yàn)證用戶名和密碼.
- 讀取用戶名和密碼到
UsernamePasswordAuthenticationToken
中, 并將其傳遞給ProviderManager. - ProviderManager選擇調(diào)用DaoAuthenticationProvider.
- DaoAuthenticationProvider 通過UserDetailsService獲取UserDetails.
- 調(diào)用PasswordEncoder驗(yàn)證密碼.
- 如果驗(yàn)證成功, 返回UsernamePasswordAuthenticationToken(其principal為UserDetails對象). 并將其存儲(chǔ)到SecurityContextHolder中.
2. Session Management
與Session相關(guān)的功能由SessionManagementFilter
和SessionAuthenticationStrategy
實(shí)現(xiàn). 功能包括防止Session固定會(huì)話攻擊, 檢測session超時(shí)和限制session并發(fā)數(shù).
2.1 檢測超時(shí)
session失效后可以重定向到session無效頁面.
<http>
<session-management invalid-session-url="/invalidSession.html"/>
</http>
如果使用以上配置檢測session超時(shí), 如果用戶退出后又不關(guān)閉瀏覽器重新登錄叠必,可能會(huì)報(bào)錯(cuò). 這是因?yàn)閏ookie沒有被清除,即使用戶已經(jīng)注銷牌捷,會(huì)話cookie也會(huì)被重新提交亚脆。因此需要在logout時(shí)顯式地刪除JSESSIONID cookie.
<http>
<logout delete-cookies="JSESSIONID" />
</http>
**注意: **如果在代理后面運(yùn)行應(yīng)用程序禀忆,那么還可以通過配置代理服務(wù)器來刪除會(huì)話cookie房轿。
2.2 session并發(fā)控制
限制用戶的登錄行為.
首先: 需要在web.xml中添加session事件監(jiān)聽器.
<listener>
<listener-class>
org.springframework.security.web.session.HttpSessionEventPublisher
</listener-class>
</listener>
然后: 添加并發(fā)數(shù)量限制
// 當(dāng)error-if-maximum-exceeded為false時(shí), 第二次登錄將會(huì)剔掉第一次登錄.
// 如果為true, 第二次登錄將會(huì)被拒絕. 如果是表單登錄,則會(huì)進(jìn)入認(rèn)證失敗頁面, 如果是rememberme, 則會(huì)返回401.
<http>
<session-management>
<concurrency-control max-sessions="1" error-if-maximum-exceeded="true" />
</session-management>
</http>
2.3 Session固定保護(hù)
session固定會(huì)話攻擊是利用服務(wù)器的session不變機(jī)制, 攻擊者欺騙用戶登錄(此時(shí)登錄所攜帶的sessionId為攻擊者設(shè)置的sessionId), 用戶登錄后, 攻擊者設(shè)置的sessionId對應(yīng)的會(huì)話合法了, 然后就可以利用這個(gè)sessionId冒充用戶以達(dá)到目的.
解決辦法就是用戶登錄后就修改session信息.
Spring Security通過session-fixation-protection
屬性控制session策略.
- none: 不做任何改變.
- newSession: 創(chuàng)建一個(gè)全新的session, 不會(huì)復(fù)制session數(shù)據(jù), 但與spring security相關(guān)的屬性會(huì)被復(fù)制到新session中.
- migrateSession: 創(chuàng)建一個(gè)新的session, 然后將舊session的數(shù)據(jù)復(fù)制到新的session中. servlet3.0及之前的默認(rèn)策略.
- changeSessionId: 修改sessionId. servlet3.1及以后版本的默認(rèn)策略, 這里使用的是servlet的固定會(huì)話攻擊防護(hù)機(jī)制(HttpServletRequest#changeSessionId)
當(dāng)發(fā)生session固定攻擊時(shí), spring 容器會(huì)發(fā)布一個(gè)SessionFixationProtectionEvent
. 如果使用changeSessionId機(jī)制, HttpSessionIdListener也會(huì)收到通知.
2.4 SessionManagementFilter
工作流程:
- 根據(jù)當(dāng)前
SecurityContextHolder
的內(nèi)容檢查SecurityContextRepository
的內(nèi)容愈涩,以確定用戶在當(dāng)前請求期間是否已通過身份驗(yàn)證. - 如果包含SecurityContext, 則不做任何事情.如果不包含, 但是當(dāng)前的 SecurityContext中包含一個(gè)匿名的
Authentication
對象, 它則假設(shè)用戶已被前面的filter進(jìn)行驗(yàn)證過, 它將調(diào)用SessionAuthenticationStrategy
. - 如果當(dāng)前用戶沒有經(jīng)過身份驗(yàn)證肠骆,篩選器將檢查是否請求了無效的會(huì)話ID(例如由于超時(shí))蚌本,如果設(shè)置了InvalidSessionStrategy盔粹,則將調(diào)用配置的InvalidSessionStrategy。最常見的行為就是重定向到一個(gè)固定的URL魂毁,這封裝在標(biāo)準(zhǔn)實(shí)現(xiàn)SimpleRedirectInvalidSessionStrategy中玻佩。
2.5 SessionAuthenticationStrategy
它被SessionManagementFilter
和AbstractAuthenticationProcessingFilter
使用. 因此,如果使用定制的表單登錄類席楚,需要將它注入到這兩個(gè)類中咬崔。
<http>
<custom-filter position="FORM_LOGIN_FILTER" ref="myAuthFilter" />
<session-management session-authentication-strategy-ref="sas"/>
</http>
<beans:bean id="myAuthFilter" class=
"org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter">
<beans:property name="sessionAuthenticationStrategy" ref="sas" />
...
</beans:bean>
<beans:bean id="sas" class=
"org.springframework.security.web.authentication.session.SessionFixationProtectionStrategy" />
2.6 并發(fā)控制
除了前面的配置, 還需要配置ConcurrentSessionFilter
到FilterChainProxy
. 它有兩個(gè)參數(shù): SessionRegistry, SessionInformationExpiredStrategy.
<http>
<custom-filter position="CONCURRENT_SESSION_FILTER" ref="concurrencyFilter" />
<custom-filter position="FORM_LOGIN_FILTER" ref="myAuthFilter" />
<session-management session-authentication-strategy-ref="sas"/>
</http>
<beans:bean id="redirectSessionInformationExpiredStrategy"
class="org.springframework.security.web.session.SimpleRedirectSessionInformationExpiredStrategy">
<beans:constructor-arg name="invalidSessionUrl" value="/session-expired.htm" />
</beans:bean>
<beans:bean id="concurrencyFilter"
class="org.springframework.security.web.session.ConcurrentSessionFilter">
<beans:constructor-arg name="sessionRegistry" ref="sessionRegistry" />
<beans:constructor-arg name="sessionInformationExpiredStrategy" ref="redirectSessionInformationExpiredStrategy" />
</beans:bean>
<beans:bean id="myAuthFilter" class=
"org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter">
<beans:property name="sessionAuthenticationStrategy" ref="sas" />
<beans:property name="authenticationManager" ref="authenticationManager" />
</beans:bean>
<beans:bean id="sas" class="org.springframework.security.web.authentication.session.CompositeSessionAuthenticationStrategy">
<beans:constructor-arg>
<beans:list>
<beans:bean class="org.springframework.security.web.authentication.session.ConcurrentSessionControlAuthenticationStrategy">
<beans:constructor-arg ref="sessionRegistry"/>
<beans:property name="maximumSessions" value="1" />
<beans:property name="exceptionIfMaximumExceeded" value="true" />
</beans:bean>
<beans:bean class="org.springframework.security.web.authentication.session.SessionFixationProtectionStrategy">
</beans:bean>
<beans:bean class="org.springframework.security.web.authentication.session.RegisterSessionAuthenticationStrategy">
<beans:constructor-arg ref="sessionRegistry"/>
</beans:bean>
</beans:list>
</beans:constructor-arg>
</beans:bean>
<beans:bean id="sessionRegistry"
class="org.springframework.security.core.session.SessionRegistryImpl" />
2.7. 類圖
3. Remeber-Me 認(rèn)證
網(wǎng)站可以記住用戶身份, 當(dāng)用戶再次訪問網(wǎng)站時(shí)可自動(dòng)登錄. 這是通過cookie機(jī)制來實(shí)現(xiàn)的. Spring Security提供了兩種實(shí)現(xiàn): 一是基于cookie的實(shí)現(xiàn), 二是持久存儲(chǔ)的實(shí)現(xiàn)(數(shù)據(jù)庫). 這兩種實(shí)現(xiàn)都需要UserDetailsService
.
3.1 基于hash的token方式
在用戶身份認(rèn)證成功之后會(huì)發(fā)送一個(gè)cookie到瀏覽器. 其格式如下:
base64(username + ":" + expirationTime + ":" +
md5Hex(username + ":" + expirationTime + ":" password + ":" + key))
username: 用戶名
password: 密碼
expirationTime: token過期時(shí)間, 單位為毫秒
key: 防止remember-me令牌被修改的私鑰
啟用remeber-me
<http>
<remember-me key="myAppKey"/>
</http>
3.2 持久化token
使用這種方式需要提供數(shù)據(jù)源.
<http>
<remember-me data-source-ref="someDataSource"/>
</http>
數(shù)據(jù)庫表建表SQL:
create table persistent_logins (username varchar(64) not null,
series varchar(64) primary key,
token varchar(64) not null,
last_used timestamp not null)
3.3 RemeberMe接口及實(shí)現(xiàn)
UsernamePasswordAuthenticationFilter
具有rememberme功能, 它是通過AbstractAuthenticationProcessingFilter
中的鉤子實(shí)現(xiàn), 這個(gè)鉤子調(diào)用RememberMeServices
.
// RememberMeServices
Authentication autoLogin(HttpServletRequest request, HttpServletResponse response);
void loginFail(HttpServletRequest request, HttpServletResponse response);
void loginSuccess(HttpServletRequest request, HttpServletResponse response,
Authentication successfulAuthentication);
UsernamePasswordAuthenticationFilter只會(huì)調(diào)用loginFail
或loginSuccess
方法. autoLogin
是在RememberMeAuthenticationFilter
中調(diào)用.
PersistentTokenBasedRememberMeServices
PersistentTokenBasedRememberMeServices還需要一個(gè)UserDetailsService, 以獲取用戶信息進(jìn)行驗(yàn)證并生成RemeberMeAuthenticationToken. 另外它還依賴PersistentTokenRepository
以用于存儲(chǔ)token. 它默認(rèn)有兩種實(shí)現(xiàn):
- InMemoryTokenRepositoryImpl
- JdbcTokenRepositoryImpl
3.4 RememberMeAuthenticationFilter類圖
4. 匿名認(rèn)證
4.1 介紹
“匿名身份驗(yàn)證”的用戶和未經(jīng)身份驗(yàn)證的用戶之間并沒有真正的概念上的區(qū)別。匿名身份驗(yàn)證的好處是,所有URI模式都可以應(yīng)用安全性, 還可保證SecutiryContextHolder總是包含一個(gè)Authentication對象而不是null.
4.2 配置
<http>
<anonymous/>
</http>
三個(gè)類提供了身份認(rèn)證功能:
- AnonymousAuthenticationToken
用于存儲(chǔ)為匿名用戶授權(quán)的權(quán)限(GrantedAuthority) - AnonymousAuthenticationProvider
- AnonymousAuthenticationFilter
<bean id="anonymousAuthFilter"
class="org.springframework.security.web.authentication.AnonymousAuthenticationFilter">
<property name="key" value="foobar"/>
<property name="userAttribute" value="anonymousUser,ROLE_ANONYMOUS"/>
</bean>
<bean id="anonymousAuthenticationProvider"
class="org.springframework.security.authentication.AnonymousAuthenticationProvider">
<property name="key" value="foobar"/>
</bean>
key在provider和filter之間共享,
4.3 AuthenticationTrustResolver
4.4 spring mvc中獲取匿名Authentication對象.
使用@CurrentSecurityContext
.
@GetMapping("/")
public String method(@CurrentSecurityContext SecurityContext context) {
return context.getAuthentication().getName();
}