SpringSecurity是一個權(quán)限驗證的安裝框架,有身份驗證(用戶名密碼)和用戶授權(quán)(權(quán)限)等功能.
快速上手
SpringSecurity在整合springboot的時候非常簡單,只在maven文件中引入jar即可
pom.xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
在日志中有密碼,在訪問restfull接口的時候,需要輸入用戶名和密碼,
密碼是在日志中提示的,用戶名是user
Using generated security password: afdda172-b982-40aa-9552-18560c8e8ecc
進階
快速上手是springsecurity默認的,如何定制化呢
需要實現(xiàn)WebSecurityConfigurerAdapter,首先實現(xiàn)類要用@Configuration
和@EnableWebSecurity標注
重寫兩個方法configure(HttpSecurity http)配置規(guī)則和configure(AuthenticationManagerBuilder auth)身份認證,從內(nèi)存或者數(shù)據(jù)庫中獲取用戶信息,configure(WebSecurity web)可以規(guī)定忽略哪些路徑
@Configuration
@EnableWebSecurity
public class SpringSecurityConfiguration extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception{
http
.authorizeRequests()
.antMatchers("/hello").permitAll() // 對于hello路徑放行
.anyRequest().authenticated()
.and()
.formLogin().and()
; //瀏覽器以form表單形式
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception{
// 用戶信息存儲在內(nèi)存中
auth.inMemoryAuthentication().withUser("user")
.password(new BCryptPasswordEncoder().encode("1234")).authorities("ADMIN");
}
@Override
public void configure(WebSecurity web) throws Exception{
//忽略/world路徑
web.ignoring().antMatchers("/world");
}
@Bean
public PasswordEncoder passwordEncoder(){
// 官網(wǎng)建議的加密方式,相同的密碼,每次加密都不一樣,安全性更好一點
return new BCryptPasswordEncoder();
}
上面的示例,在內(nèi)存中生成了用戶,使用form表單的方式,放行了 hello和world路徑的訪問
filter
那springsecurity是如何工作的,就是依托于servlet的filter調(diào)用鏈,加上springsecurity的filter,這個filter中又維護了一個filter鏈,springsecurity終止自己的調(diào)用鏈后,繼續(xù)執(zhí)行servlet的filter
org.springframework.security.web.FilterChainProxy.VirtualFilterChain#doFilter看這個方法就能理解security的filter的大致工作原理,currentPosition為security的filter自己調(diào)用鏈的數(shù)量,如果都執(zhí)行完,就繼續(xù)執(zhí)行servlet的filter
springsecurit主要的filter為
--UsernamePasswordAuthenticationFilter 校驗用戶名密碼,主要的作用為驗證用戶名密碼,如果正確則退出springsecurity自己的過濾器鏈.
--ExceptionTranslationFilter 如果FilterSecurityInterceptor權(quán)限不通過,重定向到登入頁
--FilterSecurityInterceptor 校驗權(quán)限等
當然不止這三個,所有filter之間的order為100,目的是為了可以將自定義的filter放到容器中并指定順序,
一般都是放到UsernamePasswordAuthenticationFilter 前后,加入自己的filter之后,訪問都會執(zhí)行整個調(diào)用鏈,也就都會訪問自己添加的filter,所以需要考慮加判斷,不滿足條件直接跳過,執(zhí)行下一個filter,獲取或者終止調(diào)用鏈,直接跳過當前filter就是執(zhí)行chain.doFilter(request, response);終止就是不執(zhí)行
chain.doFilter(request, response);那調(diào)用鏈就結(jié)束了.如果要深入了解可以關(guān)注
org.springframework.security.web.FilterChainProxy.VirtualFilterChain#doFilter
這里分析UsernamePasswordAuthenticationFilter ,因為我們自定義的filter都是遵循
UsernamePasswordAuthenticationFilter 的模式,比如想做一個手機驗證.
UsernamePasswordAuthenticationFilter 是如何工作的,這里就不詳細分析每一個細節(jié)了,大概分析思路,就可以擴展了.
UsernamePasswordAuthenticationFilter 是AbstractAuthenticationProcessingFilter的子類,在構(gòu)造方法中初始化了AntPathRequestMatcher
意思是我只對login路徑,方法為post進行攔截,自定義自己的filter的時候,就可以模仿這個構(gòu)造器
doFilter方法在父類AbstractAuthenticationProcessingFilter中,可以看到調(diào)用了
attemptAuthentication方法,
authResult = attemptAuthentication(request, response);
UsernamePasswordAuthenticationFilter 的attemptAuthentication方法的返回值
return this.getAuthenticationManager().authenticate(authRequest);
this.getAuthenticationManager()返回的是ProviderManager,那看一下authenticate
方法中的for循環(huán)
for (AuthenticationProvider provider : getProviders())
for循環(huán)里關(guān)注兩個代碼,
if (!provider.supports(toTest))和result = provider.authenticate(authentication);
toTest是放進來的Token,這里指的是UsernamePasswordAuthenticationToken,
如果想要自定義,比如手機驗證使用自定義mobileAuthenticationToken,
還需要提供自己的provider,因為getProviders()返回的是provider集合,那如何知道使用哪個provider,就是通過if (!provider.supports(toTest))
舉例手機驗證就能清晰一點
MobileAuthenticationToken 直接復(fù)制UsernamePasswordAuthenticationToken代碼,改一下類名就可以,
MobileAuthenticationProvider繼承AuthenticationProvider,實現(xiàn)兩個方法
authenticate(Authentication authentication)和supports(Class<?> authentication)
這兩個方法就對應(yīng)上面說的for循環(huán)中的兩行代碼
authenticate(Authentication authentication)方法中,驗證手機驗證碼是否正確,返回MobileAuthenticationToken 中this.authenticated要復(fù)制為true,證明已經(jīng)驗證成功了,這個非常重要,
創(chuàng)建MobileAuthenticationFilter,復(fù)制UsernamePasswordAuthenticationFilter就好,簡單改一下就可以,構(gòu)造方法中創(chuàng)建AntPathRequestMatcher,攔截路徑/mobile/form,方法POST,那如何將自定義的filter放到springsecurity的調(diào)用鏈呢,最開始說的WebSecurityConfigurerAdapter子類重寫的configure(HttpSecurity http)方法中
另一個要考慮的問題就是,登入之后其他路徑是怎么被放行的呢,先講解通過session的方式
這就是登入成功之后,可以被訪問的原因,這是其中一個filter,SecurityContextPersistenceFilter,
通過sessionid從內(nèi)存中獲取MobileAuthenticationToken ,并放到SecurityContextHolder中,是ThreadLocal類型的變量,該線程就可以使用了,會在springsecurity的最后一個filter(FilterSecurityInterceptor),從SecurityContextHolder獲取MobileAuthenticationToken,并判斷
token中的authenticated是否為true,如果是則證明已經(jīng)認證過,當然還有判斷是否有訪問該接口的權(quán)限,就不展開了.
說回UsernamePasswordAuthenticationToken,
流程就是先調(diào)用父類的AbstractAuthenticationProcessingFilter的doFilter
-->authResult = attemptAuthentication(request, response);
UsernamePasswordAuthenticationFilter#attemptAuthentication
-->this.getAuthenticationManager().authenticate(authRequest);
在for循環(huán)中調(diào)用
result = provider.authenticate(authentication);
UsernamePasswordAuthenticationFilter在調(diào)用attemptAuthentication方法的時候,使用UsernamePasswordAuthenticationToken,在for循環(huán)里調(diào)用provider的support方法,判斷應(yīng)該使用DaoAuthenticationProvider,provider.authenticate(authentication)方法其實是父類的authenticate方法,authenticate作用是通過調(diào)用DaoAuthenticationProvider#retrieveUser方法,獲取用戶信息,并將用戶密碼和前端輸入的密碼比較,如果成功之后,要返回UsernamePasswordAuthenticationToken,并將authenticated要為true,證明已經(jīng)驗證過了,這個在自定義filter的時候非常重要.
filter中還有認證成功處理器和認證失敗處理器
如果是前后端分離的,就response寫入信息就好了.
剛提到的DaoAuthenticationProvider#retrieveUser方法,獲取用戶信息,這也是一個擴展點,這種源碼用到了成員變量,基本就是可以擴展的地方,流程就是默認給一個值,也可以程序員手動的賦值.
UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username);
這里說的成員變量,就是 this.getUserDetailsService()的userDetailsService
創(chuàng)建一個類實現(xiàn)UserDetailsService,重寫loadUserByUsername方法,該方法從數(shù)據(jù)庫里獲取用戶信息,并封裝為UserDetails返回即可.
流程就是這樣,下篇文章實戰(zhàn),對上面說的能有個更好的理解.