基於表單的登入認證
一、預設安全驗證
當我們專案裡新增spring security依賴它就已經起作用了,啟動專案訪問時,會出現彈出框。spring security預設
採用basic模式認證。瀏覽器傳送http報文請求一個受保護的資源,瀏覽器會彈出對話方塊讓輸入使用者名稱和密碼。並以用
戶名:密碼的形式base64加密,加入Http報文頭部的Authorization(預設使用者名稱為user,密碼則是會在啟動程式時後
臺console裡輸出,每次都不一樣)。後臺獲取Http報文頭部相關認證資訊,認證成功返回相應內容,失敗則繼續認證。
下面會詳細介紹具體認證流程。
複製程式碼
二、基於表單的認證原理
基本認證流程:
SecurityContextPersistenceFilter
↓
AbstractAuthenticationProcessingFilter
↓
UsernamePasswordAuthenticationFilter
↓
AuthenticationManager
↓
AuthenticationProvider
↓
userDetailsService
↓
userDetails
↓
認證通過
↓
SecurityContext
↓
SecurityContextHolder
↓
RememberMeServices
↓
AuthenticationSuccessHandler
SecurityContextPersistenceFilter會校驗請求中session是否有SecurityContext,有放SecurityContextHolder
中,返回時校驗SecurityContextHolder中是否有securityContext,有放session,從而實現認證資訊在多個請求中共
享。
AbstractAuthenticationProcessingFilter中會調自身attemptAuthentication抽象方法,
UsernamePasswordAuthenticationFilter是其一個實現類。
複製程式碼
它從請求中獲取賬號密碼後,構造了一個token,此時沒有認證,隨後會呼叫AuthenticationManager介面的
authenticate方法
複製程式碼
下面是其建構函式,我們可看到這個token沒有認證
複製程式碼
public UsernamePasswordAuthenticationToken(Object principal, Object credentials) {
super((Collection)null);
this.principal = principal;
this.credentials = credentials;
this.setAuthenticated(false);
}
複製程式碼
ProviderManager實現了AuthenticationManager介面,它獲取所有provider後遍歷呼叫其supports方法,去匹配哪
個provider支援之前申明的token,找到provider後呼叫authenticate方法。
到provider才開始認證使用者資訊,表單登入的是DaoAuthenticationProvider其父類進行以下操作:
①呼叫userDetailsService介面的loadUserByUsername方法獲取UserDetails使用者資訊
②檢查UserDetails使用者是否可用、是否賬戶沒有過期、是否賬戶沒有被鎖定
③檢查UserDetails密碼是否正確
④檢查UserDetails密碼是否沒有過期
⑤都通過會重新申明一個token,不過多了許可權集合引數如下
複製程式碼
public UsernamePasswordAuthenticationToken(Object principal, Object credentials,
Collection authorities) {
super(authorities);
this.principal = principal;
this.credentials = credentials;
super.setAuthenticated(true);
}
複製程式碼
此時使用者才被認證通過,上述步驟有一個錯了會丟擲AuthenticationException異常的子類,下面是其繼承圖:
複製程式碼
認證結果返回給AbstractAuthenticationProcessingFilter,將結果放到SecurityContextHolder的
SecurityContext中,若新增了記住我功能又會呼叫rememberMeServices來實現,最後呼叫
AuthenticationSuccessHandler介面成功處理。
複製程式碼
三、表單登入實現
在第二部分介紹具體原理,接下來實現自定義登入驗證。首先編寫一個config類繼承WebSecurityConfigurerAdapter
類重寫configure方法填寫配置
複製程式碼
protected void configure(HttpSecurity http) throws Exception {
http
/**
* 表單登入配置
*/
.formLogin() //表單登入
.loginPage("/authentication/login.html") //自定義登入頁面
.loginProcessingUrl("/authentication/form") //與自定義登入頁面處理路徑一致
// http.httpBasic() basic模式彈出框登入
.successHandler(myAuthenticationSuccessHandler) //自定義認證成功處理
.failureHandler(myAuthenticationFailureHandler) //自定義認證失敗處理
.and()
/**
* 需要認證的請求配置,
* 注:最為具體的請求路徑放在前面,而最不具體的路徑(如anyRequest())放在最後面。
* 如果不這樣做的話,那不具體的路徑配置將會覆蓋掉更為具體的路徑配置
*/
.authorizeRequests()
//允許這樣請求通過,需要將登入所需路徑配好,不然會一直重定向
.antMatchers("/authentication/*").permitAll()
.anyRequest().authenticated()//任何請求都需要認證
.and()
.csrf().disable(); //關閉csrf防禦機制
}
複製程式碼
自定義使用者實現UserDetailsService介面重寫loadUserByUsername方法
複製程式碼
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
return new User(username, "123456", true, true,
true, true, new ArrayList());
}
複製程式碼
這裡的User是security自己的,寫死了密碼為123456,正常應該根據使用者名稱從資料庫查出使用者資訊。當然密碼也不可
能為明文,這裡推薦使用BCryptPasswordEncoder加密,因為其每次加密都會生成隨機鹽加入字串中。需要在之前申
明的config類新增即可
複製程式碼
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
複製程式碼
這四個boolean值分別表示是否可用、是否賬戶沒有過期、是否密碼沒有過期、是否賬戶沒有被鎖定。建構函式
最後一個引數為該使用者用哪些介面許可權,同樣也從資料庫查,但是這個許可權只會在登入時初始化一次,若我登入後修
改許可權,則無法同步,後續介紹解決辦法。
接下來就是兩個登入的自定義登入處理,成功的實現AuthenticationSuccessHandler介面,失敗的則實現
AuthenticationFailureHandler介面
複製程式碼
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException{
response.setContentType("application/json;UTF-8");
response.getWriter().println("{"success":true,"result":"" + "登入成功" + ""}");
}
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
response.setContentType("application/json;UTF-8");
response.getWriter().println("{"error":true,"message":"登入名或者密碼錯誤"}");
}
複製程式碼
注意成功和失敗最後的方法引數是不同的,一個是成功的認證資訊,另一個失敗丟擲的認證異常json串的key可以
根據自身登入頁面ajax處理結果的屬性來定,必須保持一致。
頁面程式碼就不復制了,做一個簡單html就行。
複製程式碼
四、Remember Me
remember me顧名思義就是記住我,在登入成功認證以後服務端會傳送cookie給瀏覽器,同時把使用者名稱、base64加密隨
機序列、生成token存入persistent_logins表中。當我下次再訪問時,會讀取cookie中的token與資料庫中的token做
對比,若一直能表示認證通過,會重新生成新的token存入資料庫中,序列戶不變,再重新生成新的cookie發給瀏覽器。
下面看具體原始碼:
複製程式碼
在第二部分說了登入認證成功後會呼叫rememberMeServices的loginSuccess方法,這是最後呼叫的方法,我們看
到了它構造了一個rememberMeToken,將其存入資料庫,併傳送cookie。
複製程式碼
當下次登入,會請求到RememberMeAuthenticationFilter,它呼叫rememberMeServices介面的autoLogin方法,
去對比cookie中的token與資料庫中的token,又判斷了token是否過期,驗證通過會根據token中的使用者名稱呼叫
userDetailsService的loadUserByUsername方法獲取使用者資訊。最後根據使用者資訊構造
一個RememberMeAuthenticationToken認證成功。
複製程式碼
四、Remember Me實現
remember me實現較為簡單,只需要在申明的config類中新增
複製程式碼
@Autowired
private DataSource dataSource;
@Autowired
private UserDetailsService userDetailsService;
@Bean
public PersistentTokenRepository persistentTokenRepository() {
JdbcTokenRepositoryImpl tokenRepository = new JdbcTokenRepositoryImpl();
tokenRepository.setDataSource(dataSource);
//tokenRepository.setCreateTableOnStartup(true);
return tokenRepository;
}
複製程式碼
在configure方法里加
複製程式碼
.rememberMe()
.tokenRepository(persistentTokenRepository())
.tokenValiditySeconds(3600) //有效時間
.userDetailsService(userDetailsService)
複製程式碼
五、程式碼地址
https://github.com/qumaoming/spring-security
複製程式碼