最簡單易懂的Spring Security 身份認證流程講解java
相信大夥對Spring Security這個框架又愛又恨,愛它的強大,恨它的繁瑣,其實這是一個誤區,Spring Security確實很是繁瑣,繁瑣到讓人生厭。討厭也木有辦法呀,做爲JavaEE的工程師們仍是要面對的,在開始以前,先打一下比方(比方好可憐):ios
Spring Security 就像一個行政服務中心,若是咱們去裏面辦事,能夠辦啥事呢?能夠小到諮詢簡單問題、查詢社保信息,也能夠戶籍登記、補辦身份證,一樣也能夠大到企業事項、各類複雜的資質辦理。可是咱們並不須要跑一次行政服務中心,就挨個把業務所有辦理一遍,現實中沒有這樣的人吧。數據庫
啥意思呢,就是說選擇您須要的服務(功能),無視那些不須要的,等有須要的時候再瞭解不遲。這也是給衆多工程師們的一個建議,特別是體系異常龐大的Java系,別動不動就精通,擼遍源碼之類的,真沒啥意義,我大腦的存儲比較小,人生苦短,不必。後端
回到正題!本文會以一種比較輕鬆的方式展開,不會是堆代碼。瀏覽器
Web 身份認證是一個後端工程師永遠沒法避開的領域,身份認證Authentication,和受權Authorization是不一樣的,Authentication指的是用戶身份的認證,並不介入這個用戶可以作什麼,不可以作什麼,僅僅是確認存在這個用戶而已。而Authorization受權是創建的認證的基礎上的,存在這個用戶了,再來約定這個用戶能補可以作一件事,這點你們要區分開。本文講的是Authentication的故事,並不會關注權限。緩存
熱熱身,讓咱們來溫習一下身份認證的方式演變:安全
先是最著名的入門留言板程序,相信不少作後端的工程師都作過留言板,那是一個基本沒有框架的階段,回想一下是怎麼認證的。表單輸入用戶名密碼Submit,而後後端取到數據數據庫查詢,查不到的話無情地拋出一個異常,哦,密碼錯了;查到了,愉快的將用戶ID和相關信息加密寫入到Session標識中存起來,響應寫入Cookie,後續的請求都解密後驗證就好了,對吧。是的,身認證真能夠簡單到僅僅是匹配Session標識而已。使人沮喪的是現代互聯網的發展早已通過了 Web2.0 的時代,客戶端的出現讓身份認證更加複雜。咱們繼續服務器
隨着移動端的崛起,Android和ios佔據主導,一樣是用戶登陸認證,取到用戶信息,正準備按圖索驥寫入Session回寫Cookie的時候,等等!啥?Android不支持Cookie?這聽起來不科學是吧,有點反人類是吧,有點手足無措是吧。網絡
嘿嘿,聰明的人兒也許想到了辦法,嗯,Android客戶端不是有本地存儲嗎?把回傳的數據存起來不就好了嗎?又要抱歉了,Android本地存儲並無瀏覽器Cookie那麼人性化,不會自動過時。沒事,再註明過時時間,每次讀取的時候判斷就行啦,貌似能夠了。框架
等等。客戶端的Api接口要求輕量級,某一天一個隊友想實現個性化的事情,居然往Cookie了回傳了一串字符串,貌似很方便,嗯。因而其餘隊友也效仿,而後Cookie變得更加複雜。此時Android隊友一聲吼,大家夠了!STOP!我只要一個認證標識而已,夠簡單大家知道嗎?還有Cookie過時了就要從新登錄,用戶體驗極差,產品經理都找我談了幾十次了,用戶都快跑光了,大家還在往Cookie里加一些奇怪的東西。
Oauth 2.0來了
有問題總要想辦法解決是吧。客戶端不是瀏覽器,有本身特有的交互約定,Cookie仍是放棄掉了。這裏就要解決五個問題:
需求一旦肯定,方案呼之欲出,讓咱們來簡單構思一下。
Userkey
爲Header名,值就是那個加密過的標識,夠簡潔粗暴吧,後端對每個請求都攔截處理,若是可以解密成功而且表示有效,就告訴後邊排隊的小夥伴,這個傢伙是本身人,叫xxx,兜裏有100塊錢。這個也搞定了。打完收工,要開始實現這套系統了。先別急呀,難道沒以爲似曾相識嗎?沒錯就是 Oauth 2.0 的 password Grant 模式!
先來一段你們很熟悉的代碼:
http.formLogin()
.loginPage("/auth/login")
.permitAll()
.failureHandler(loginFailureHandler)
.successHandler(loginSuccessHandler);
複製代碼
Spring Security 就像一個害羞的大姑娘,就這麼一段鬼知道他是怎麼認證的,封裝的有點過哈。不着急先看一張圖:
這裏作了一個簡化,
根據JavaEE的流程,本質就是Filter過濾請求,轉發到不一樣處理模塊處理,最後通過業務邏輯處理,返回Response的過程。
當請求匹配了咱們定義的Security Filter的時候,就會導向Security 模塊進行處理,例如UsernamePasswordAuthenticationFilter,源碼獻上:
public class UsernamePasswordAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
public static final String SPRING_SECURITY_FORM_USERNAME_KEY = "username";
public static final String SPRING_SECURITY_FORM_PASSWORD_KEY = "password";
private String usernameParameter = "username";
private String passwordParameter = "password";
private boolean postOnly = true;
public UsernamePasswordAuthenticationFilter() {
super(new AntPathRequestMatcher("/login", "POST"));
}
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
if (this.postOnly && !request.getMethod().equals("POST")) {
throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
} else {
String username = this.obtainUsername(request);
String password = this.obtainPassword(request);
if (username == null) {
username = "";
}
if (password == null) {
password = "";
}
username = username.trim();
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);
this.setDetails(request, authRequest);
return this.getAuthenticationManager().authenticate(authRequest);
}
}
protected String obtainPassword(HttpServletRequest request) {
return request.getParameter(this.passwordParameter);
}
protected String obtainUsername(HttpServletRequest request) {
return request.getParameter(this.usernameParameter);
}
protected void setDetails(HttpServletRequest request, UsernamePasswordAuthenticationToken authRequest) {
authRequest.setDetails(this.authenticationDetailsSource.buildDetails(request));
}
public void setUsernameParameter(String usernameParameter) {
Assert.hasText(usernameParameter, "Username parameter must not be empty or null");
this.usernameParameter = usernameParameter;
}
public void setPasswordParameter(String passwordParameter) {
Assert.hasText(passwordParameter, "Password parameter must not be empty or null");
this.passwordParameter = passwordParameter;
}
public void setPostOnly(boolean postOnly) {
this.postOnly = postOnly;
}
public final String getUsernameParameter() {
return this.usernameParameter;
}
public final String getPasswordParameter() {
return this.passwordParameter;
}
}
複製代碼
有點複雜是吧,不用擔憂,我來作一些僞代碼,讓他看起來更友善,更好理解。注意我寫的單行註釋
public class UsernamePasswordAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
public static final String SPRING_SECURITY_FORM_USERNAME_KEY = "username";
public static final String SPRING_SECURITY_FORM_PASSWORD_KEY = "password";
private String usernameParameter = "username";
private String passwordParameter = "password";
private boolean postOnly = true;
public UsernamePasswordAuthenticationFilter() {
//1.匹配URL和Method
super(new AntPathRequestMatcher("/login", "POST"));
}
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
if (this.postOnly && !request.getMethod().equals("POST")) {
//啥?你沒有用POST方法,給你一個異常,本身反思去
throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
} else {
//從請求中獲取參數
String username = this.obtainUsername(request);
String password = this.obtainPassword(request);
//我不知道用戶名密碼是否是對的,因此構造一個未認證的Token先
UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(username, password);
//順便把請求和Token存起來
this.setDetails(request, token);
//Token給誰處理呢?固然是給當前的AuthenticationManager嘍
return this.getAuthenticationManager().authenticate(token);
}
}
}
複製代碼
是否是很清晰,問題又來了,Token是什麼鬼?爲啥還有已認證和未認證的區別?彆着急,我們順藤摸瓜,來看看Token長啥樣。上UsernamePasswordAuthenticationToken:
public class UsernamePasswordAuthenticationToken extends AbstractAuthenticationToken {
private static final long serialVersionUID = 510L;
private final Object principal;
private Object credentials;
public UsernamePasswordAuthenticationToken(Object principal, Object credentials) {
super((Collection)null);
this.principal = principal;
this.credentials = credentials;
this.setAuthenticated(false);
}
public UsernamePasswordAuthenticationToken(Object principal, Object credentials, Collection<? extends GrantedAuthority> authorities) {
super(authorities);
this.principal = principal;
this.credentials = credentials;
super.setAuthenticated(true);
}
public Object getCredentials() {
return this.credentials;
}
public Object getPrincipal() {
return this.principal;
}
public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException {
if (isAuthenticated) {
throw new IllegalArgumentException("Cannot set this token to trusted - use constructor which takes a GrantedAuthority list instead");
} else {
super.setAuthenticated(false);
}
}
public void eraseCredentials() {
super.eraseCredentials();
this.credentials = null;
}
}
複製代碼
一坨坨的真鬧心,我再備註一下:
public class UsernamePasswordAuthenticationToken extends AbstractAuthenticationToken {
private static final long serialVersionUID = 510L;
//隨便怎麼理解吧,暫且理解爲認證標識吧,沒看到是一個Object麼
private final Object principal;
//同上
private Object credentials;
//這個構造方法用來初始化一個沒有認證的Token實例
public UsernamePasswordAuthenticationToken(Object principal, Object credentials) {
super((Collection)null);
this.principal = principal;
this.credentials = credentials;
this.setAuthenticated(false);
}
//這個構造方法用來初始化一個已經認證的Token實例,爲啥要畫蛇添足,不能直接Set狀態麼,不着急,日後看
public UsernamePasswordAuthenticationToken(Object principal, Object credentials, Collection<? extends GrantedAuthority> authorities) {
super(authorities);
this.principal = principal;
this.credentials = credentials;
super.setAuthenticated(true);
}
//便於理解無視他
public Object getCredentials() {
return this.credentials;
}
//便於理解無視他
public Object getPrincipal() {
return this.principal;
}
public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException {
if (isAuthenticated) {
//若是是Set認證狀態,就無情的給一個異常,意思是:
//不要在這裏設置已認證,不要在這裏設置已認證,不要在這裏設置已認證
//應該從構造方法裏建立,別忘了要帶上用戶信息和權限列表哦
//原來如此,是避免犯錯吧
throw new IllegalArgumentException("Cannot set this token to trusted - use constructor which takes a GrantedAuthority list instead");
} else {
super.setAuthenticated(false);
}
}
public void eraseCredentials() {
super.eraseCredentials();
this.credentials = null;
}
}
複製代碼
搞清楚了Token是什麼鬼,其實只是一個載體而已啦。接下來進入核心環節,AuthenticationManager是怎麼處理的。這裏我簡單的過渡一下,可是會讓你明白。
AuthenticationManager會註冊多種AuthenticationProvider,例如UsernamePassword對應的DaoAuthenticationProvider,既然有多種選擇,那怎麼肯定使用哪一個Provider呢?我截取了一段源碼,你們一看便知:
public interface AuthenticationProvider {
Authentication authenticate(Authentication var1) throws AuthenticationException;
boolean supports(Class<?> var1);
}
複製代碼
這是一個接口,我喜歡接口,簡潔明瞭。裏面有一個supports方法,返回時一個boolean值,參數是一個Class,沒錯,這裏就是根據Token的類來肯定用什麼Provider來處理,你們還記得前面的那段代碼嗎?
//Token給誰處理呢?固然是給當前的AuthenticationManager嘍
return this.getAuthenticationManager().authenticate(token);
複製代碼
所以咱們進入下一步,DaoAuthenticationProvider,繼承了AbstractUserDetailsAuthenticationProvider,恭喜您再堅持一會就到曙光啦。這個比較複雜,爲了避免讓你跑掉,我將兩個複雜的類合併,摘取直接觸達接口核心的邏輯,直接上代碼,會有所刪減,讓你看得更清楚,注意看註釋:
public class DaoAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider {
//熟悉的supports,須要UsernamePasswordAuthenticationToken
public boolean supports(Class<?> authentication) {
return UsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication);
}
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
//取出Token裏保存的值
String username = authentication.getPrincipal() == null ? "NONE_PROVIDED" : authentication.getName();
boolean cacheWasUsed = true;
//從緩存取
UserDetails user = this.userCache.getUserFromCache(username);
if (user == null) {
cacheWasUsed = false;
//啥,沒緩存?使用retrieveUser方法獲取呀
user = this.retrieveUser(username, (UsernamePasswordAuthenticationToken)authentication);
}
//...刪減了一大部分,這樣更簡潔
Object principalToReturn = user;
if (this.forcePrincipalAsString) {
principalToReturn = user.getUsername();
}
return this.createSuccessAuthentication(principalToReturn, authentication, user);
}
protected final UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
try {
//熟悉的loadUserByUsername
UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username);
if (loadedUser == null) {
throw new InternalAuthenticationServiceException("UserDetailsService returned null, which is an interface contract violation");
} else {
return loadedUser;
}
} catch (UsernameNotFoundException var4) {
this.mitigateAgainstTimingAttack(authentication);
throw var4;
} catch (InternalAuthenticationServiceException var5) {
throw var5;
} catch (Exception var6) {
throw new InternalAuthenticationServiceException(var6.getMessage(), var6);
}
}
//檢驗密碼
protected void additionalAuthenticationChecks(UserDetails userDetails, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
if (authentication.getCredentials() == null) {
this.logger.debug("Authentication failed: no credentials provided");
throw new BadCredentialsException(this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
} else {
String presentedPassword = authentication.getCredentials().toString();
if (!this.passwordEncoder.matches(presentedPassword, userDetails.getPassword())) {
this.logger.debug("Authentication failed: password does not match stored value");
throw new BadCredentialsException(this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
}
}
}
}
複製代碼
到此爲止,就完成了用戶名密碼的認證校驗邏輯,根據認證用戶的信息,系統作相應的Session持久化和Cookie回寫操做。
Spring Security的基本認證流程先寫到這裏,其實複雜的背後是一些預約,熟悉了以後就不難了。
Filter->構造Token->AuthenticationManager->轉給Provider處理->認證處理成功後續操做或者不經過拋異常
有了這些基礎,後面咱們再來擴展短信驗證碼登陸,以及基於Oauth 2.0 的短信驗證碼登陸。