spring security 認證源碼跟蹤
在跟蹤認證源碼之前,我們先根據官網說明一下security的內部原理,主要是依據一系列的filter來實現,大家可以根據https://docs.spring.io/spring-security/site/docs/5.5.3/reference/html5/#servlet-hello 查看相關的文檔說明,英文不好的可以配合使用google翻譯。
security 原理說明
在上圖中,紅色方框圈出來的是security 的filter,每一個http request都會經過上圖的每一個指定的過濾器。請求其中:
DelegatingFilterProxy:主要負責在servlet容器的生命周期和Spring上下文進行銜接,也就是說security的所有過濾器都委托給它進行代理。
FilterChainProxy:是一個特殊的過濾器,被包裝在DelegatingFilterProxy內部。它代理代理了SecurityFilterChain
SecurityFilterChain:SecurityFilterChain 確定應為此請求調用哪些 Spring 安全過濾器。
DelegatingFilterProxy
這是一個過濾器,所以肯定會有doFilter方法,我們主要查看內部的2個方法,首先從doFilter方法看起:
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
// Lazily initialize the delegate if necessary.
Filter delegateToUse = this.delegate;
if (delegateToUse == null) {
synchronized (this.delegateMonitor) {
delegateToUse = this.delegate;
if (delegateToUse == null) {
// 拿到Spring Web上下文
WebApplicationContext wac = findWebApplicationContext();
if (wac == null) {
throw new IllegalStateException("No WebApplicationContext found: " +
"no ContextLoaderListener or DispatcherServlet registered?");
}
// 初始化委托filter
delegateToUse = initDelegate(wac);
}
this.delegate = delegateToUse;
}
}
// Let the delegate perform the actual doFilter operation.
invokeDelegate(delegateToUse, request, response, filterChain);
}
// 初始化委托filter
protected Filter initDelegate(WebApplicationContext wac) throws ServletException {
// 眾多filter中,會有一個是FilterChainProxy
String targetBeanName = getTargetBeanName();
Assert.state(targetBeanName != null, "No target bean name set");
Filter delegate = wac.getBean(targetBeanName, Filter.class);
if (isTargetFilterLifecycle()) {
delegate.init(getFilterConfig());
}
return delegate;
}
FilterChainProxy
它也是一個過濾器,那一定也會有doFilter方法,我們查看該方法
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
// 當前request是否已經清除了上下文,因為每一個請求都會經過這個過濾器
boolean clearContext = request.getAttribute(FILTER_APPLIED) == null;
if (!clearContext) {
doFilterInternal(request, response, chain);
return;
}
try {
request.setAttribute(FILTER_APPLIED, Boolean.TRUE);
// 內部的filter方法,我們看到該方法
doFilterInternal(request, response, chain);
}
catch (RequestRejectedException ex) {
this.requestRejectedHandler.handle((HttpServletRequest) request, (HttpServletResponse) response, ex);
}
finally {
SecurityContextHolder.clearContext();
request.removeAttribute(FILTER_APPLIED);
}
}
private void doFilterInternal(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
// 拿到防火牆配置,對於這里不重要
FirewalledRequest firewallRequest = this.firewall.getFirewalledRequest((HttpServletRequest) request);
HttpServletResponse firewallResponse = this.firewall.getFirewalledResponse((HttpServletResponse) response);
// 這里可以看到,FilterChainProxy在這里拿到了這次請求request具體還要經過的一系列過濾器鏈,其中包括CsrfFilter、UsernamePasswordAuthenticationFilter等過濾器,包含了SecurityFilterChain 涉及的filter
List<Filter> filters = getFilters(firewallRequest);
if (filters == null || filters.size() == 0) {
if (logger.isTraceEnabled()) {
logger.trace(LogMessage.of(() -> "No security for " + requestLine(firewallRequest)));
}
firewallRequest.reset();
chain.doFilter(firewallRequest, firewallResponse);
return;
}
if (logger.isDebugEnabled()) {
logger.debug(LogMessage.of(() -> "Securing " + requestLine(firewallRequest)));
}
VirtualFilterChain virtualFilterChain = new VirtualFilterChain(firewallRequest, chain, filters);
virtualFilterChain.doFilter(firewallRequest, firewallResponse);
}
認證源碼跟蹤
回到認證這里,在網上隨便搜一搜就能搜到spring scurity
認證的幾種方式,這次我們主要跟蹤第三種認證方式:數據庫認證,也是我們平時在用的方式。先給大家說明一下數據庫認證的知識點,有個大概印象:
- UsernamePasswordAuthenticationFilter
- 實現
UserDetailsService
接口並注入到spring管理
這三種認證方式分為為:
1、在xml中配置賬號密碼
spring.security.user.name=user
spring.security.user.password=123456
2、在代碼中將賬號、密碼加載到內存中
@Bean
public UserDetailsService userDetailsService() {
UserDetails userDetails = User.withDefaultPasswordEncoder()
.username("user")
.password("password")
.roles("USER")
.build();
return new InMemoryUserDetailsManager(userDetails);
}
3、從數據庫中讀取賬號進行認證校驗
public class MyUserDetailsService implements UserDetailsService {
@Autowired
private UserMapper userMapper;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// 從數據庫嘗試讀取該用戶
User user = userMapper.findByUserName(username);
// 用戶不存在,拋出異常
if (user == null) {
throw new UsernameNotFoundException("用戶不存在");
}
// 將數據庫形式的roles解析為UserDetails的權限集
// AuthorityUtils.commaSeparatedStringToAuthorityList是Spring Security
//提供的用於將逗號隔開的權限集字符串切割成可用權限對象列表的方法
// 當然也可以自己實現,如用分號來隔開等,參考generateAuthorities
user.setAuthorities(AuthorityUtils.commaSeparatedStringToAuthorityList(user.getRoles()));
return user;
}
}
在這個例子中,我們會有一個自定義WebSecurityConfig
類,其中定義了哪些Url路徑需要攔截,以及需要哪些權限才能夠訪問,同時在這個配置中,注入一個一個密碼編碼類,默認是不采用加密方式NoOpPasswordEncoder
。
@EnableWebSecurity(debug = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/admin/api/**").hasRole("ADMIN")
.antMatchers("/user/api/**").hasRole("USER")
.antMatchers("/app/api/**").permitAll()
.antMatchers("/css/**", "/index").permitAll()
.antMatchers("/user/**").hasRole("USER")
.and()
.formLogin()
.loginPage("/login")
.failureUrl("/login-error")
.permitAll();
}
@Bean
public PasswordEncoder passwordEncoder() {
return NoOpPasswordEncoder.getInstance();
}
}
我們先不去實現UserDetailsService
接口,看看spring security
是怎么去實現認證的?
UsernamePasswordAuthenticationFilter
首先找到UsernamePasswordAuthenticationFilter
類,發現它繼承了AbstractAuthenticationProcessingFilter
類,那我們就先看一下AbstractAuthenticationProcessingFilter
類,發現這個類中主要有四個方法,分別是:
- doFilter(reqeust,response,chain):每個filter都會有的方法,最重要的一個。
- attemptAuthentication(request,response); 是個抽象方法,交給具體的實現類去實現認證的邏輯
- successfulAuthentication(request,response,chain,authenticationResult); 認證成功后的處理邏輯,通過不同的策略實現
- unsuccessfulAuthentication(request,resonse,failed);認證失敗后的處理邏輯,通過不同的策略實現
private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws IOException, ServletException {
if (!requiresAuthentication(request, response)) {
chain.doFilter(request, response);
return;
}
try {
// 具體的認證方法,是個抽象方法,交給具體的實現類去實現認證的邏輯
Authentication authenticationResult = attemptAuthentication(request, response);
if (authenticationResult == null) {
// return immediately as subclass has indicated that it hasn't completed
return;
}
this.sessionStrategy.onAuthentication(authenticationResult, request, response);
// Authentication success
if (this.continueChainBeforeSuccessfulAuthentication) {
chain.doFilter(request, response);
}
// 認證成功后的處理邏輯
successfulAuthentication(request, response, chain, authenticationResult);
}
catch (InternalAuthenticationServiceException failed) {
this.logger.error("An internal error occurred while trying to authenticate the user.", failed);
unsuccessfulAuthentication(request, response, failed);
}
catch (AuthenticationException ex) {
// Authentication failed
// 認證失敗后的處理邏輯
unsuccessfulAuthentication(request, response, ex);
}
}
```
// 從http.formLogin() 可以知道即將要設置默認的登錄頁,登錄接口等配置
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.formLogin()
}
// 點擊formLogin()方法,發現默認自定義配置的話,security給我們配置了默認的登錄認證過濾器UsernamePasswordAuthenticationFilter
public FormLoginConfigurer() {
super(new UsernamePasswordAuthenticationFilter(), null);
usernameParameter("username");
passwordParameter("password");
}
接着我們看回`UsernamePasswordAuthenticationFilter`類,發現它主要是重寫了`AbstractAuthenticationProcessingFilter`類的**attemptAuthentication(request,response)**認證方法。具體如下:
```java
@Override
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());
}
// 獲取請求的用戶名
String username = obtainUsername(request);
username = (username != null) ? username : "";
username = username.trim();
// 獲取請求輸入的密碼
String password = obtainPassword(request);
password = (password != null) ? password : "";
// 構造帶有用戶名、密碼的UsernamePasswordAuthenticationToken對象
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);
// Allow subclasses to set the "details" property
// 設置認證的對象
setDetails(request, authRequest);
return this.getAuthenticationManager().authenticate(authRequest);
}
接着我們運行程序,直接訪問http://localhost:8080/admin/api/hello地址,重定向到登錄頁后,隨意輸入賬號、密碼后,在UsernamePasswordAuthenticationFilter
類的attemptAuthentication
方法上打斷點進行跟蹤,跟蹤到DaoAuthenticationProvider
類的retrieveUser(String username, UsernamePasswordAuthenticationToken authentication)
方法,
在AbstractUserDetailsAuthenticationProvider
抽象中的需要指定一個實現UserDetailsService
接口的實現類,如果我們沒有指定,就是會去加載默認的InMemoryUserDetailManager
類。
因為采用的是上面提過的第二種方式:在代碼中將賬號、密碼加載到內存中,然后我們並沒有在內存中預先加載我們輸入的賬號、密碼,所以自然是認證不通過的。
UserDetailsService 接口
想要通過自定義的認證方式,也就是上面提到的第三種認證方式:從數據庫中讀取賬號進行認證校驗。所以需要自己去實現UserDetailsService
接口。剛才我們在跟蹤代碼的過程中,發現AbstractUserDetailsAuthenticationProvider
類是需要一個實現了UserDetailsService
接口的對象,於是我們就自定義一個實現該接口的實現類,並注入到spring容器中。
@Service
public class MyUserDetailsService implements UserDetailsService {
@Autowired
private UserMapper userMapper;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// 從數據庫嘗試讀取該用戶
User user = userMapper.findByUserName(username);
// 用戶不存在,拋出異常
if (user == null) {
throw new UsernameNotFoundException("用戶不存在");
}
// 將數據庫形式的roles解析為UserDetails的權限集
// AuthorityUtils.commaSeparatedStringToAuthorityList是Spring Security
//提供的用於將逗號隔開的權限集字符串切割成可用權限對象列表的方法
// 當然也可以自己實現,如用分號來隔開等,參考generateAuthorities
user.setAuthorities(AuthorityUtils.commaSeparatedStringToAuthorityList(user.getRoles()));
return user;
}
}
如上圖,我們重寫了UserDetailsService
接口的loadUserByUsername(String username)
方法,從而實現我們的自定義認證邏輯。然后我們再重啟服務,重新訪問http://localhost:8080/admin/api/hello,再次登錄,並進行代碼跟蹤,
這個時候就發現DaoAuthenticationProvider
從自己的userDetailsService
拿到了我們自定義的對象,接着就會走我們的自定義認證邏輯。
認證源碼跟蹤就到這里,接下來是授權的源碼跟蹤,跟蹤文章較短,但大家了解一下還是有些收獲的。加油!