基於表單的登陸認證
1、默認安全驗證
當咱們項目裏添加spring security依賴它就已經起做用了,啓動項目訪問時,會出現彈出框。spring security默認
採用basic模式認證。瀏覽器發送http報文請求一個受保護的資源,瀏覽器會彈出對話框讓輸入用戶名和密碼。並以用
戶名:密碼的形式base64加密,加入Http報文頭部的Authorization(默認用戶名爲user,密碼則是會在啓動程序時後
臺console裏輸出,每次都不同)。後臺獲取Http報文頭部相關認證信息,認證成功返回相應內容,失敗則繼續認證。
下面會詳細介紹具體認證流程。
複製代碼
2、基於表單的認證原理
基本認證流程:
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接口成功處理。
複製代碼
3、表單登陸實現
在第二部分介紹具體原理,接下來實現自定義登陸驗證。首先編寫一個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就行。
複製代碼
4、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認證成功。
複製代碼
4、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)
複製代碼
5、代碼地址
https://github.com/qumaoming/spring-security
複製代碼