詳解Spring Security認證流程
前言
Spring Seuciry相關(guān)的內(nèi)容看了實在是太多了,但總覺得還是理解地不夠鞏固,還是需要靠知識輸出做鞏固。
相關(guān)版本:
java: jdk 8 spring-boot: 2.1.6.RELEASE
過濾器鏈和認證過程

一個認證過程,其實就是過濾器鏈上的一個綠色矩形Filter所要執(zhí)行的過程。
基本的認證過程有三步驟:
- Filter攔截請求,生成一個未認證的
Authentication,交由AuthenticationManager進行認證; AuthenticationManager的默認實現(xiàn)ProviderManager會通過AuthenticationProvider對Authentication進行認證,其本身不做認證處理;- 如果認證通過,則創(chuàng)建一個認證通過的
Authentication返回;否則拋出異常,以表示認證不通過。
要理解這個過程,可以從類UsernamePasswordAuthenticationFilter,ProviderManager,DaoAuthenticationProvider和InMemoryUserDetailsManager(UserDetailsService實現(xiàn)類,由UserDetailsServiceAutoConfiguration默認配置提供)進行了解。只要創(chuàng)建一個含有spring-boot-starter-security的springboot項目,在適當(dāng)?shù)卮蛏蠑帱c接口看到這個流程。
用認證部門進行講解
)
請求到前臺之后,負責(zé)該請求的前臺會將請求的內(nèi)容封裝為一個Authentication對象交給認證管理部門,認證管理部門僅管理認證部門,不做具體的認證操作,具體的操作由與該前臺相關(guān)的認證部門進行處理。當(dāng)然,每個認證部門需要判斷Authentication是否為該部門負責(zé),是則由該部門負責(zé)處理,否則交給下一個部門處理。認證部門認證成功之后會創(chuàng)建一個認證通過的Authentication返回。否則要么拋出異常表示認證不通過,要么交給下一個部門處理。
如果需要新增認證類型,只要增加相應(yīng)的前臺(Filter)和與該前臺(Filter)想對應(yīng)的認證部門(AuthenticationProvider)就即可,當(dāng)然也可以增加一個與已有前臺對應(yīng)的認證部門。認證部門會通過前臺生成的Authentication來判斷該認證是否由該部門負責(zé),因而也許提供一個兩者相互認同的Authentication.
認證部門需要人員資料時,則可以從人員資料部門獲取。不同的系統(tǒng)有不同的人員資料部門,需要我們提供該人員資料部門,否則將拿到空白檔案。當(dāng)然,人員資料部門不一定是唯一的,認證部門可以有自己的專屬資料部門。
上圖還可以有如下的畫法:

這個畫法可能會和FilterChain更加符合。每一個前臺其實就是FilterChain中的一個,客戶拿著請求逐個前臺請求認證,找到正確的前臺之后進行認證判斷。
前臺(Filter)
這里的前臺Filter僅僅指實現(xiàn)認證的Filter,Spring Security Filter Chain中處理這些Filter還有其他的Filter,比如CsrfFilter。如果非要給角色給他們,那么就當(dāng)他們是保安人員吧。
Spring Security為我們提供了3個已經(jīng)實現(xiàn)的Filter。UsernamePasswordAuthenticationFilter,BasicAuthenticationFilter和 RememberMeAuthenticationFilter。如果不做任何個性化的配置,UsernamePasswordAuthenticationFilter和BasicAuthenticationFilter會在默認的過濾器鏈中。這兩種認證方式也就是默認的認證方式。
UsernamePasswordAuthenticationFilter僅僅會對/login路徑生效,也就是說UsernamePasswordAuthenticationFilter負責(zé)發(fā)布認證,發(fā)布認證的接口為/login。
public class UsernamePasswordAuthenticationFilter extends
AbstractAuthenticationProcessingFilter {
...
public UsernamePasswordAuthenticationFilter() {
super(new AntPathRequestMatcher("/login", "POST"));
}
...
}
UsernamePasswordAuthenticationFilter為抽象類AbstractAuthenticationProcessingFilter的一個實現(xiàn),而BasicAuthenticationFilter為抽象類BasicAuthenticationFilter的一個實現(xiàn)。這四個類的源碼提供了不錯的前臺(Filter)實現(xiàn)思路。
AbstractAuthenticationProcessingFilter
AbstractAuthenticationProcessingFilter 提供了認證前后需要做的事情,其子類只需要提供實現(xiàn)完成認證的抽象方法attemptAuthentication(HttpServletRequest, HttpServletResponse)即可。使用AbstractAuthenticationProcessingFilter時,需要提供一個攔截路徑(使用AntPathMatcher進行匹配)來攔截對應(yīng)的特定的路徑。
UsernamePasswordAuthenticationFilter
UsernamePasswordAuthenticationFilter作為實際的前臺,會將客戶端提交的username和password封裝成一個UsernamePasswordAuthenticationToken交給認證管理部門(AuthenticationManager)進行認證。如此,她的任務(wù)就完成了。
BasicAuthenticationFilter
該前臺(Filter)只會處理含有Authorization的Header,且小寫化后的值以basic開頭的請求,否則該前臺(Filter)不負責(zé)處理。該Filter會從header中獲取Base64編碼之后的username和password,創(chuàng)建UsernamePasswordAuthenticationToken提供給認證管理部門(AuthenticationMananager)進行認證。
認證資料(Authentication)
前臺接到請求之后,會從請求中獲取所需的信息,創(chuàng)建自家認證部門(AuthenticationProvider)所認識的認證資料(Authentication),認證部門(AuthenticationProvider)則主要是通過認證資料(Authentication)的類型判斷是否由該部門處理。
public interface Authentication extends Principal, Serializable {
// 該principal具有的權(quán)限。AuthorityUtils工具類提供了一些方便的方法。
Collection<? extends GrantedAuthority> getAuthorities();
// 證明Principal的身份的證書,比如密碼。
Object getCredentials();
// authentication request的附加信息,比如ip。
Object getDetails();
// 當(dāng)事人。在username+password模式中為username,在有userDetails之后可以為userDetails。
Object getPrincipal();
// 是否已經(jīng)通過認證。
boolean isAuthenticated();
// 設(shè)置通過認證。
void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException;
}
在Authentication被認證之后,會保存到一個thread-local的SecurityContext中。
// 設(shè)置
SecurityContextHolder.getContext().setAuthentication(anAuthentication);
// 獲取
Authentication existingAuth = SecurityContextHolder.getContext()
.getAuthentication();
在寫前臺Filter的時候,可以先檢查SecurityContextHolder.getContext()中是否已經(jīng)存在通過認證的Authentication了,如果存在,則可以直接跳過該Filter。已經(jīng)通過驗證的Authentication建議設(shè)置為一個不可修改的實例。
目前從Authentication的類圖中看到的實現(xiàn)類,均為Authentication的抽象子類AbstractAuthenticationToken的實現(xiàn)類。實現(xiàn)類有好幾個,與前面的講到的Filter相關(guān)的有UsernamePasswordAuthenticationToken和RememberMeAuthenticationToken。
AbstractAuthenticationToken為CredentialsContainer和Authentication的子類。實現(xiàn)了一些簡單的方法,但主要的方法還需要實現(xiàn)。該類的getName()方法的實現(xiàn)可以看到常用的principal類為UserDetails、AuthenticationPrincipal和Princial。如果有需要將對象設(shè)置為principal,可以考慮繼承這三個類中的一個。
public String getName() {
if (this.getPrincipal() instanceof UserDetails) {
return ((UserDetails) this.getPrincipal()).getUsername();
}
if (this.getPrincipal() instanceof AuthenticatedPrincipal) {
return ((AuthenticatedPrincipal) this.getPrincipal()).getName();
}
if (this.getPrincipal() instanceof Principal) {
return ((Principal) this.getPrincipal()).getName();
}
return (this.getPrincipal() == null) ? "" : this.getPrincipal().toString();
}
認證管理部門(AuthenticationManager)
AuthenticationManager是一個接口,認證Authentication,如果認證通過之后,返回的Authentication應(yīng)該帶上該principal所具有的GrantedAuthority。
public interface AuthenticationManager {
Authentication authenticate(Authentication authentication)
throws AuthenticationException;
}
該接口的注釋中說明,必須按照如下的異常順序進行檢查和拋出:
- DisabledException:賬號不可用
- LockedException:賬號被鎖
- BadCredentialsException:證書不正確
Spring Security提供一個默認的實現(xiàn)ProviderManager。認證管理部門(ProviderManager)僅執(zhí)行管理職能,具體的認證職能由認證部門(AuthenticationProvider)執(zhí)行。
public class ProviderManager implements AuthenticationManager, MessageSourceAware,
InitializingBean {
...
public ProviderManager(List<AuthenticationProvider> providers) {
this(providers, null);
}
public ProviderManager(List<AuthenticationProvider> providers,
AuthenticationManager parent) {
Assert.notNull(providers, "providers list cannot be null");
this.providers = providers;
this.parent = parent;
checkState();
}
public Authentication authenticate(Authentication authentication)
throws AuthenticationException {
Class<? extends Authentication> toTest = authentication.getClass();
AuthenticationException lastException = null;
AuthenticationException parentException = null;
Authentication result = null;
Authentication parentResult = null;
boolean debug = logger.isDebugEnabled();
for (AuthenticationProvider provider : getProviders()) {
// #1, 檢查是否由該認證部門進行認證`AuthenticationProvider`
if (!provider.supports(toTest)) {
continue;
}
if (debug) {
logger.debug("Authentication attempt using "
+ provider.getClass().getName());
}
try {
// #2, 認證部門進行認證
result = provider.authenticate(authentication);
if (result != null) {
copyDetails(authentication, result);
// #3,認證通過則不再進行下一個認證部門的認證,否則拋出的異常被捕獲,執(zhí)行下一個認證部門(AuthenticationProvider)
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 = parentResult = 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 = parentException = e;
}
}
// #4, 如果認證通過,執(zhí)行認證通過之后的操作
if (result != null) {
if (eraseCredentialsAfterAuthentication
&& (result instanceof CredentialsContainer)) {
// Authentication is complete. Remove credentials and other secret data
// from authentication
((CredentialsContainer) result).eraseCredentials();
}
// If the parent AuthenticationManager was attempted and successful than it will publish an AuthenticationSuccessEvent
// This check prevents a duplicate AuthenticationSuccessEvent if the parent AuthenticationManager already published it
if (parentResult == null) {
eventPublisher.publishAuthenticationSuccess(result);
}
return result;
}
// Parent was null, or didn't authenticate (or throw an exception).
// #5,如果認證不通過,必然有拋出異常,否則表示沒有配置相應(yīng)的認證部門(AuthenticationProvider)
if (lastException == null) {
lastException = new ProviderNotFoundException(messages.getMessage(
"ProviderManager.providerNotFound",
new Object[] { toTest.getName() },
"No AuthenticationProvider found for {0}"));
}
// If the parent AuthenticationManager was attempted and failed than it will publish an AbstractAuthenticationFailureEvent
// This check prevents a duplicate AbstractAuthenticationFailureEvent if the parent AuthenticationManager already published it
if (parentException == null) {
prepareException(lastException, authentication);
}
throw lastException;
}
...
}
遍歷所有的認證部門(AuthenticationProvider),找到支持的認證部門進行認證認證部門進行認證認證通過則不再進行下一個認證部門的認證,否則拋出的異常被捕獲,執(zhí)行下一個認證部門(AuthenticationProvider)如果認證通過,執(zhí)行認證通過之后的操作如果認證不通過,必然有拋出異常,否則表示沒有配置相應(yīng)的認證部門(AuthenticationProvider)
當(dāng)使用到Spring Security OAuth2的時候,會看到另一個實現(xiàn)OAuth2AuthenticationManager。
認證部門(AuthenticationProvider)
認證部門(AuthenticationProvider)負責(zé)實際的認證工作,與認證管理部門(ProvderManager)協(xié)同工作。也許其他的認證管理部門(AuthenticationManager)并不需要認證部門(AuthenticationProvider)的協(xié)作。
public interface AuthenticationProvider {
// 進行認證
Authentication authenticate(Authentication authentication)
throws AuthenticationException;
// 是否由該AuthenticationProvider進行認證
boolean supports(Class<?> authentication);
}
該接口有很多的實現(xiàn)類,其中包含了RememberMeAuthenticationProvider(直接AuthenticationProvider)和DaoAuthenticationProvider(通過AbastractUserDetailsAuthenticationProvider簡介繼承)。這里重點講講AbastractUserDetailsAuthenticationProvider和DaoAuthenticationProvider。
AbastractUserDetailsAuthenticationProvider
顧名思義,AbastractUserDetailsAuthenticationProvider是對UserDetails支持的Provider,其他的Provider,如RememberMeAuthenticationProvider就不需要用到UserDetails。該抽象類有兩個抽象方法需要實現(xiàn)類完成:
// 獲取 UserDetails
protected abstract UserDetails retrieveUser(String username,
UsernamePasswordAuthenticationToken authentication)
throws AuthenticationException;
protected abstract void additionalAuthenticationChecks(UserDetails userDetails,
UsernamePasswordAuthenticationToken authentication)
throws AuthenticationException;
retrieveUser()方法為校驗提供UserDetails。先看下UserDetails:
public interface UserDetails extends Serializable {
Collection<? extends GrantedAuthority> getAuthorities();
String getPassword();
String getUsername();
// 賬號是否過期
boolean isAccountNonExpired();
// 賬號是否被鎖
boolean isAccountNonLocked();
// 證書(password)是否過期
boolean isCredentialsNonExpired();
// 賬號是否可用
boolean isEnabled();
}
AbastractUserDetailsAuthenticationProvider#authentication(Authentication)分為三步驗證:
- preAuthenticationChecks.check(user);
- additionalAuthenticationChecks(user,
- (UsernamePasswordAuthenticationToken) authentication);
- postAuthenticationChecks.check(user);
preAuthenticationChecks的默認實現(xiàn)為DefaultPreAuthenticationChecks,負責(zé)完成校驗:
- UserDetails#isAccountNonLocked()
- UserDetails#isEnabled()
- UserDetails#isAccountNonExpired()
postAuthenticationChecks的默認實現(xiàn)為DefaultPostAuthenticationChecks,負責(zé)完成校驗:
UserDetails#user.isCredentialsNonExpired()
additionalAuthenticationChecks需要由實現(xiàn)類完成。
校驗成功之后,AbstractUserDetailsAuthenticationProvider會創(chuàng)建并返回一個通過認證的Authentication。
protected Authentication createSuccessAuthentication(Object principal,
Authentication authentication, UserDetails user) {
// Ensure we return the original credentials the user supplied,
// so subsequent attempts are successful even with encoded passwords.
// Also ensure we return the original getDetails(), so that future
// authentication events after cache expiry contain the details
UsernamePasswordAuthenticationToken result = new UsernamePasswordAuthenticationToken(
principal, authentication.getCredentials(),
authoritiesMapper.mapAuthorities(user.getAuthorities()));
result.setDetails(authentication.getDetails());
return result;
}
DaoAuthenticationProvider
如下為DaoAuthenticationProvider對AbstractUserDetailsAuthenticationProvider抽象方法的實現(xiàn)。
// 檢查密碼是否正確
protected void additionalAuthenticationChecks(UserDetails userDetails,
UsernamePasswordAuthenticationToken authentication)
throws AuthenticationException {
if (authentication.getCredentials() == null) {
logger.debug("Authentication failed: no credentials provided");
throw new BadCredentialsException(messages.getMessage(
"AbstractUserDetailsAuthenticationProvider.badCredentials",
"Bad credentials"));
}
String presentedPassword = authentication.getCredentials().toString();
if (!passwordEncoder.matches(presentedPassword, userDetails.getPassword())) {
logger.debug("Authentication failed: password does not match stored value");
throw new BadCredentialsException(messages.getMessage(
"AbstractUserDetailsAuthenticationProvider.badCredentials",
"Bad credentials"));
}
}
// 通過資料室(UserDetailsService)獲取UserDetails對象
protected final UserDetails retrieveUser(String username,
UsernamePasswordAuthenticationToken authentication)
throws AuthenticationException {
prepareTimingAttackProtection();
try {
UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username);
if (loadedUser == null) {
throw new InternalAuthenticationServiceException(
"UserDetailsService returned null, which is an interface contract violation");
}
return loadedUser;
}
...
}
在以上的代碼中,需要提供UserDetailsService和PasswordEncoder實例。只要實例化這兩個類,并放入到Spring容器中即可。
資料部門(UserDetailsService)
UserDetailsService接口提供認證過程所需的UserDetails的類,如DaoAuthenticationProvider需要一個UserDetailsService實例。
public interface UserDetailsService {
UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}
Spring Security提供了兩個UserDetailsService的實現(xiàn):InMemoryUserDetailsManager和JdbcUserDetailsManager。InMemoryUserDetailsManager為默認配置,從UserDetailsServiceAutoConfiguration的配置中可以看出。當(dāng)然也不容易理解,基于數(shù)據(jù)庫的實現(xiàn)需要增加數(shù)據(jù)庫的配置,不適合做默認實現(xiàn)。這兩個類均為UserDetailsManager的實現(xiàn)類,UserDetailsManager定義了UserDetails的CRUD操作。InMemoryUserDetailsManager使用Map<String, MutableUserDetails>做存儲。
public interface UserDetailsManager extends UserDetailsService {
void createUser(UserDetails user);
void updateUser(UserDetails user);
void deleteUser(String username);
void changePassword(String oldPassword, String newPassword);
boolean userExists(String username);
}
如果我們需要增加一個UserDetailsService,可以考慮實現(xiàn)UserDetailsService或者UserDetailsManager。
增加一個認證流程
到這里,我們已經(jīng)知道Spring Security的流程了。從上面的內(nèi)容可以知道,如要增加一個新的認證方式,只要增加一個[前臺(Filter) + 認證部門(AuthenticationProvider) + 資料室(UserDetailsService)]組合即可。事實上,資料室(UserDetailsService)不是必須的,可根據(jù)認證部門(AuthenticationProvider)需要實現(xiàn)。

我會在另一篇文章中以手機號碼+驗證碼登錄為例進行講解。
以上就是本文的全部內(nèi)容,希望對大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
相關(guān)文章
java利用easyexcel實現(xiàn)導(dǎo)入與導(dǎo)出功能
這篇文章主要介紹了java利用easyexcel實現(xiàn)導(dǎo)入與導(dǎo)出功能,文章圍繞主題展開詳細的內(nèi)容介紹,具有一定的參考價值,感興趣的小伙伴可以參考一下,希望對你的學(xué)習(xí)有所幫助2022-09-09
java網(wǎng)絡(luò)通信技術(shù)之簡單聊天小程序
這篇文章主要為大家詳細介紹了java網(wǎng)絡(luò)通信技術(shù)之簡單聊天小程序,文中示例代碼介紹的非常詳細,具有一定的參考價值,感興趣的小伙伴們可以參考一下2018-07-07
一文搞懂SpringMVC中@InitBinder注解的使用
@InitBinder方法可以注冊控制器特定的java.bean.PropertyEditor或Spring Converter和 Formatter組件。本文通過示例為大家詳細講講@InitBinder注解的使用,需要的可以參考一下2022-06-06
Java利用策略模式實現(xiàn)條件判斷,告別if else
策略模式定義了一系列算法,并且將每個算法封裝起來,使得他們可以相互替換,而且算法的變化不會影響使用算法的客戶端。本文將通過案例講解如何利用Java的策略模式實現(xiàn)條件判斷,告別if----else條件硬編碼,需要的可以參考一下2022-02-02
Java?Chassis3熔斷機制的改進路程技術(shù)解密
這篇文章主要介紹了Java?Chassis?3技術(shù)解密之熔斷機制的改進路程實例分析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2024-01-01
springboot中如何配置LocalDateTime JSON返回時間戳
這篇文章主要介紹了springboot中如何配置LocalDateTime JSON返回時間戳問題。具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2023-06-06

