轉自:https://blog.csdn.net/weixin_37689658/article/details/92752890
1、基本配置使用
(1)創建配置類
創建一個配置類SecurityConfig繼承自WebSecurityConfigurerAdapter,重寫里面的configure(HttpSecurity http)這個方法,配置好需要認證的登錄url,以及提交表單的url,這里除了登錄url不需要認證之外,其他的url都需要認證才能訪問,並且formLogin表名這是一個表單提交,loginprocessingUrl中是設置的提交登錄表單的url。
@Override
protected void configure(HttpSecurity http) throws Exception {
.formLogin()
.loginPage("/authentication/require") //指定沒有認證時跳轉到的認證url
.loginProcessingUrl("/authentication/form") //提交登錄表單的url
.and()
.authorizeRequests()
.antMatchers("/authentication/require").permitAll()
.anyRequest()
.authenticated()
.and()
.csrf().disable();
}
(2)獲取認證信息
我們需要繼承一個UserDetailSevice接口並且加入到容器中,實現loadUserByUsername方法,里面的邏輯通常是從數據庫查找出對應用戶名的密碼然后構造一個UserDetail對象,spring security會根據返回的這個帶有正確用戶信息的對象和前台傳過來的用戶名密碼進行比對來判斷是否認證通過。
/**
* 表單登錄的時候會調用loadUserByUsername來驗證前端傳過來的賬號密碼是否正確
*/
@Component
public class MyUserDetailService implements UserDetailsService {
private Logger logger = LoggerFactory.getLogger(MyUserDetailService.class);
@Autowired
private PasswordEncoder passwordEncoder;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
logger.info("登錄用戶名===========" + username);
//這里需要去數據庫查詢用戶的賬號密碼來比對是否正確,以及賬號是否過期等等
String password = passwordEncoder.encode("123456");
logger.info("數據庫密碼是==============" + password);
return new User(username, password,
true,
true,
true,
true,
AuthorityUtils.commaSeparatedStringToAuthorityList("admin"));
}
}
這樣配置好的話,比如說我們訪問一個一個http://localhost:8080/user的接口,那么spring security 發現配置中這個接口是需要攔截的,並且當前的請求還沒有通過認證,就會重定向到loginPage設置的這個接口或者頁面中去。當我們調用loginProcessingUrl這個接口去提交表單的時候,如果通過了認證,那么就會重定向到原來我們想要訪問的接口中去了,如果認證不成功,那么就出現認證失敗等信息。
那么對於這個過程,spring security內部的流程到底是怎樣的尼?我們先通過一張圖來看。

我們可以看到在spring security內部其實是通過一個過濾器鏈來實現認證流程的,比如說這里的UsernamePasswordAuthenticationFilter就是攔截我們通過表單提交接口提交的用戶名和密碼,如果是Basic提交的話,就會被BasicAuthenticationFilter攔截,最后的橙色FilterSecurityInterceptor是首先判斷我們當前請求的url是否需要認證,如果需要認證,那么就看當前請求是否已經認證,是的話就放行到我們要訪問的接口,否則重定向到認證頁面。
2、認證流程
這里已表單提交為例,當我們提交表單時,UsernamePasswordAuthenticationFilter首先會攔截請求,而UsernamePasswordAuthenticationFilter是繼承於AbstractAuthenticationProcessingFilter的,在這個抽象類中已經定義好了doFilter的方法,而里面有一個attemptAuthentication方法是由子類實現的。所以當提交表單時spring security會發現這個一個表單提交,然后就調用了UsernamePasswordAuthenticationFilter的doFilter方法

然后我們來看 UsernamePasswordAuthenticationFilter里面重寫的這個方法


然后我們可以看到首先是創建了一個UsernamePasswordAuthenticationToken對象,把用戶名和密碼傳進去,再調用了getAuthenticationManager().authentication()方法,並把 UsernamePasswordAuthenticationToken對象傳進去,那么這個AuthenticationManager是什么尼,其實它是一個接口

我們這里要看的是它的實現類ProviderManager,進去找到它的authenticate方法

其實這個ProviderManager就是管理所有的Provider對象的,通過for循環遍歷找到適合的provider對象來調用其authenticate方法,那么provider對象又是什么東西?其實它也是一個接口

可以看到它的實現類有很多,我們這里主要來看DaoAuthenticationProvider這個實現類,但是在這個類中並沒有找到authenticate方法,那么我們來它的父類AbstractUserDetailAuthenticationProvider里面來看下

里面調用了一個子類實現的方法,我們看這個子類的這個方法l

里面拿到我們之前加入到容器中的userDetailService然后調用loadUserByUsername方法拿到我們包含數據庫用戶信息的UserDetail對象,並且捕獲用戶名找不到等異常,所以這個方法就是來獲取我們之前定義的userDetailService返回的UserDetail。
回到其父類中去,父類拿到了UserDetail對象,對這個對象進行了一系列的判斷

首先我們來看下前置判斷

里面就是調用了UserDetail對象一些實現方法進行判斷,比如說賬戶是否鎖定,賬戶是否能用,賬戶是否過期的判斷,判斷通過后來到了密碼判斷

在判斷了密碼是否正確之后,后置判斷就是來判斷密碼是否過期了,

前置判斷判斷賬戶異常問題,然后到密碼判斷,最后到密碼是否過期判斷,因為你登錄先看你賬戶是否有異常才能,有異常的話就不進行密碼判斷了,密碼正確也要看密碼是夠過期 ,所以這三個操作合乎常理。
判斷都沒問題之后,最后返回了一個authentication對象,這里把用戶名,密碼,權限都傳到UsernamePasswordAuthenticationToken中去,因為所有判斷都沒問題,這些信息都是正確的了。

3.認證結果如何在多個線程中共享?
首先,我們還是先來看一張圖

當請求過來的時候都會最先經過一個叫SecurityContextPersistenceFilter的過濾器,這個過濾器的作用就是在請求經過的時候檢查session中是否有認證的authentication對象,如果有的話就把它放進一個叫securityContext ,然后再交給securityContextHolder處理
看一下securityContext

它是一個接口,由子類SecurityContextImpl實現,里面就是一個authentication屬性,所以它的作用就是包裝了authentication
接下來再看一下securityContextHolder

里面的getContext,clearContext方法都是使用的一個叫 SecurityContextHolderStrategy對象,點進去看一下這個對象

我們發現它又是一個接口,我們看它的實現類ThreadLocalSecurityContextHolderStrategy
里面只有一個ThreadLocal對象,放的是SecurityContext,我們知道ThreadLocal有線程隔離的作用,每一個對象在ThreadLocal中都是線程級別的。
接着我們回到securityContextpersistenceFilter中去看里面的doFilter方法

有一個loadContext方法是加載出來SecurityContext對象的,那么它是怎么加載的?點進去

它是一個接口,看它的實現類HttpSessionSecurityContextRepository

所以上面請求經過securityContextpersistenceFilter就是先從判斷session是否有SecurityContext,有的話就放進當前線程中
那么響應的時候做什么?這里直接給出

那么認證成功后,是誰把認證對象放在放在當前請求中的?答案就是FilterSecurityInterceptor,FilterSecurityInterceptor會判斷當前請求的url是否要攔截,要攔截的話如果當前用戶沒有經過認證,那么就跳轉到認證頁面,如果認證了就直接放行,並且把認證信息放在當前線程中。
總的流程:一個請求過來時通過各個過濾器,最后通過FilterSecurityInterceptor來判斷這個請求url是否是不需要驗證的,如果是
就直接訪問到我們的接口api,如果不是的話,再判斷當前請求線程中是否有authentication的認證對象,如果有就放行,如果沒有就返回登錄頁面(比如這里我們設置的是登錄表單的方式),來到登錄頁面輸入賬號密碼登錄后就會來到 UsernamePasswordAuthenticationFilter,經過一系列的操作,最后驗證成功就會把認證對象authentication放進securityContext中,然后FilterSecurityInterceptor判斷到當前請求線程中這個認證對象就放行,返回的時候最后會通過securityContextpersistenceFilter,判斷當前線程是否有securityContext,如果有就放進session,那么下次再請求這個url的時候會首先通過securityContextpersistenceFilter這個過濾器,判斷session中是否有securityContextduxiiang,如果有就放進當前請求線程中,然后最后經過FilterSecurityInterceptor時再判斷當前請求線程是否有認證對象,由於最前面經過securityContextpersistenceFilter,已經從session中把認證對象放進了當前請求線程中,所以FilterSecurityInterceptor會直接放行,這樣就訪問到我們的接口api。
