wordpress 文檔導入樹枝seo
1、SpringSecurity 入門
1.1、簡介
????????Spring Security是一個功能強大且高度可定制的身份驗證和訪問控制框架。它是用于保護基于Spring的應用程序的實際標準。Spring Security是一個框架,致力于為Java應用程序提供身份驗證和授權。與所有Spring項目一樣,Spring Security的真正強大之處在于可以輕松擴展以滿足自定義要求。(官網(wǎng)地址)
1.2、技術方案對比
????????目前在整個Java開發(fā)的系統(tǒng)中,需要用于身份驗證和訪問控制框架的框架除了Spring Security, 還有一個就是Apache shiro框架。
Shiro:
????????Shiro是一個強大而靈活的開源安全框架,能夠非常清晰的處理認證、授權、管理會話以及密碼加密。如下是它所具有的特點:
- 易于理解的 Java Security API;
- 簡單的身份認證(登錄),支持多種數(shù)據(jù)源(LDAP,JDBC,Kerberos,ActiveDirectory等);
- 對角色的簡單的鑒權(訪問控制),支持細粒度的鑒權;
- 支持一級緩存,以提升應用程序的性能
- 內(nèi)置的基于 POJO 企業(yè)會話管理,適用于 Web 以及非 Web 的環(huán)境
- 異構客戶端會話訪問
- 非常簡單的加密 API
- 不跟任何的框架或者容器捆綁,可以獨立運行。
SpringSecurity:
????????除了不能脫離Spring,shiro的功能它都有。而且Spring Security對Oauth、OpenID也有支持,Shiro則需要自己手動實現(xiàn)。Spring Security的權限細粒度更高。
????????OAuth在"客戶端"與"服務提供商"之間,設置了一個授權層(authorization layer)。"客戶端"不能直接登錄"服務提供商",只能登錄授權層,以此將用戶與客戶端區(qū)分開來。"客戶端"登錄授權層所用的令牌(token),與用戶的密碼不同。用戶可以在登錄的時候,指定授權層令牌的權限范圍和有效期。
????????"客戶端"登錄授權層以后,"服務提供商"根據(jù)令牌的權限范圍和有效期,向"客戶端"開放用戶儲存的資料。
????????OpenID 系統(tǒng)的第一部分是身份驗證,即如何通過 URI 來認證用戶身份。目前的網(wǎng)站都是依靠用戶名和密碼來登錄認證,這就意味著大家在每個網(wǎng)站都需要注冊用戶名和密碼,即便你使用的是同樣的密碼。如果使用 OpenID ,你的網(wǎng)站地址(URI)就是你的用戶名,而你的密碼安全的存儲在一個 OpenID 服務網(wǎng)站上(你可以自己建立一個 OpenID 服務網(wǎng)站,也可以選擇一個可信任的 OpenID 服務網(wǎng)站來完成注冊)。
????????與OpenID同屬性的身份識別服務商還有ⅥeID,ClaimID,CardSpace,Rapleaf,Trufina ID Card等,其中ⅥeID通用賬戶的應用最為廣泛。
1.3、應用場景
傳統(tǒng)用戶登錄:
用戶授權,在系統(tǒng)中用戶擁有哪些權限
單一登錄:
????????一個賬號在同一時間只能在一個地方進行登錄, 如果在其他地方進行第二次登錄,則剔除之前登錄操作。
集成CAS:
????????做單點登錄,即多個系統(tǒng)只需登錄一次,無需重復登錄。
集成OAuth2:
????????做登錄授權, 可以用于app登錄和第三方登錄(QQ,微信等), 也可以實現(xiàn)cas的功能.
1.4、入門案例
1.4.1、創(chuàng)建SpringBoot項目
1.4.2、未加依賴訪問
package com.blnp.net.demo.controller;import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;/*** <p></p>** @author lyb 2045165565@qq.com* @version 1.0* @since 2024/9/3 17:28*/
// 該注解為組合注解,等同于Spring中@Controller+@ResponseBody注解
@RestController
public class DemoController {@RequestMapping("/demo")public String demo(){return "Blnp spring Boot";}
}
1.4.3、添加依賴后訪問
<!--添加Spring Security 依賴 -->
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-security</artifactId>
</dependency>
? ? ? ? 再次刷新訪問后被自動跳轉(zhuǎn)至SpringSecurity內(nèi)置的登錄頁面了,如下所示:
????????這里有三點需要注意下:
- 表單的提交方式和路徑: post /login
- input輸入項的name值: username password
- 隱藏域input的name: 值為: _csrf value值為d4329889-796a-447a-9d08-69e56bc7c296
????????SpringBoot已經(jīng)為SpringSecurity提供了默認配置,默認所有資源都必須認證通過才能訪問。那么問題來了!此刻并沒有連接數(shù)據(jù)庫,也并未在內(nèi)存中指定認證用戶,如何認證呢?其實SpringBoot已經(jīng)提供了默認用戶名user,密碼在項目啟動時隨機生成,如圖:
????????認證通過后可以繼續(xù)訪問處理器資源:
2、SpringSecurity認證
2.1、認證方式&基本原理
????????在使用SpringSecurity框架,該框架會默認自動地替我們將系統(tǒng)中的資源進行保護,每次訪問資源的時候都必須經(jīng)過一層身份的校驗,如果通過了則重定向到我們輸入的url中,否則訪問是要被拒絕的。那么SpringSecurity框架是如何實現(xiàn)的呢?
????????Spring Security功能的實現(xiàn)主要是由一系列過濾器相互配合完成。也稱之為過濾器鏈。
2.1.1、過濾器鏈介紹
????????過濾器是一種典型的AOP思想,下面簡單了解下這些過濾器鏈,后續(xù)再源碼剖析中在涉及到過濾器鏈在仔細說明。
1、WebAsyncManagerIntegrationFilter:
- 完整路徑:org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter
- 作用:根據(jù)請求封裝獲取WebAsyncManager,從WebAsyncManager獲取/注冊的安全上下文可調(diào)用處理攔截器
2、SecurityContextPersistenceFilter:
- 完整路徑:org.springframework.security.web.context.SecurityContextPersistenceFilter
- 作用:SecurityContextPersistenceFilter主要是使用SecurityContextRepository在session中保存或更新一個SecurityContext,并將SecurityContext給以后的過濾器使用,來為后續(xù)fifilter建立所需的上下文。SecurityContext中存儲了當前用戶的認證以及權限信息。
3、HeaderWriterFilter:
- 完整路徑:org.springframework.security.web.header.HeaderWriterFilter
- 作用:向請求的Header中添加相應的信息,可在http標簽內(nèi)部使用security:headers來控制
4、CsrfFilter:
- 完整路徑:org.springframework.security.web.csrf.CsrfFilter
- 作用:csrf又稱跨域請求偽造,SpringSecurity會對所有post請求驗證是否包含系統(tǒng)生成的csrf的token信息,如果不包含,則報錯。起到防止csrf攻擊的效果。
5、LogoutFilter:
- 完整路徑:org.springframework.security.web.authentication.logout.LogoutFilter
- 作用:匹配URL為/logout的請求,實現(xiàn)用戶退出,清除認證信息。
6、UsernamePasswordAuthenticationFilter:
- 完整路徑:org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter
- 作用:表單認證操作全靠這個過濾器,默認匹配URL為/login且必須為POST請求。
7、DefaultLoginPageGeneratingFilter:
- 完整路徑:org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter
- 作用:如果沒有在配置文件中指定認證頁面,則由該過濾器生成一個默認認證頁面。
8、DefaultLogoutPageGeneratingFilter:
- 完整路徑:org.springframework.security.web.authentication.ui.DefaultLogoutPageGeneratingFilter
- 作用:由此過濾器可以生產(chǎn)一個默認的退出登錄頁面
9、BasicAuthenticationFilter:
- 完整路徑:org.springframework.security.web.authentication.www.BasicAuthenticationFilter
- 作用:此過濾器會自動解析HTTP請求中頭部名字為Authentication,且以Basic開頭的頭信息。
10、RequestCacheAwareFilter:
- 完整路徑:org.springframework.security.web.savedrequest.RequestCacheAwareFilter
- 作用:通過HttpSessionRequestCache內(nèi)部維護了一個RequestCache,用于緩存?HttpServletRequest
11、SecurityContextHolderAwareRequestFilter:
- 完整路徑:org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter
- 作用:針對ServletRequest進行了一次包裝,使得request具有更加豐富的API
12、AnonymousAuthenticationFilter:
- 完整路徑:org.springframework.security.web.authentication.AnonymousAuthenticationFilter
- 作用:當SecurityContextHolder中認證信息為空,則會創(chuàng)建一個匿名用戶存入到SecurityContextHolder中。spring security為了兼容未登錄的訪問,也走了一套認證流程,只不過是一個匿名的身份。
13、SessionManagementFilter:
- 完整路徑:org.springframework.security.web.session.SessionManagementFilter
- 作用:securityContextRepository限制同一用戶開啟多個會話的數(shù)量
14、ExceptionTranslationFilter:
- 完整路徑:org.springframework.security.web.access.ExceptionTranslationFilter
- 作用:異常轉(zhuǎn)換過濾器位于整個springSecurityFilterChain的后方,用來轉(zhuǎn)換整個鏈路中出現(xiàn)的異常
15、FilterSecurityInterceptor:
- 完整路徑:org.springframework.security.web.access.intercept.FilterSecurityInterceptor
- 作用:獲取所配置資源訪問的授權信息,根據(jù)SecurityContextHolder中存儲的用戶信息來決定其是否有權限。
2.1.2、認證方式
1、HttpBasic 認證
????????HttpBasic登錄驗證模式是Spring Security實現(xiàn)登錄驗證最簡單的一種方式,也可以說是最簡陋的一種方式。它的目的并不是保障登錄驗證的絕對安全,而是提供一種“防君子不防小人”的登錄驗證。
????????在使用的Spring Boot早期版本為1.X版本,依賴的Security 4.X版本,那么就無需任何配置,啟動項目訪問則會彈出默認的httpbasic認證。現(xiàn)在使用的是spring boot2.0以上版本(依賴Security5.X版本),HttpBasic不再是默認的驗證模式,在spring security 5.x默認的驗證模式已經(jīng)是表單模式。
????????HttpBasic模式要求傳輸?shù)挠脩裘艽a使用Base64模式進行加密。如果用戶名是 "admin" ,密碼是“ admin”,則將字符串"admin:admin" 使用Base64編碼算法加密。加密結果可能是:YWtaW46YWRtaW4=。HttpBasic模式真的是非常簡單又簡陋的驗證模式,Base64的加密算法是可逆的,想要破解并不難.
2、FormLogin 登錄認證
????????Spring Security的HttpBasic模式,該模式比較簡單,只是進行了通過攜帶Http的Header進行簡單的登錄驗證,而且沒有定制的登錄頁面,所以使用場景比較窄。對于一個完整的應用系統(tǒng),與登錄驗證相關的頁面都是高度定制化的,非常美觀而且提供多種登錄方式。這就需要SpringSecurity支持我們自己定制登錄頁面, spring boot2.0以上版本(依賴Security 5.X版本)默認會生成一個登錄頁面.
2.2、表單認證
2.2.1、自定義表單登錄頁面
????????在config包下編寫SecurityConfiguration配置類。
特別說明:這里我列舉兩種配置方法,至于為什么兩種配置方法是因為跟Spring Security 的版本有關,擴展 WebSecurityConfigurerAdapter 的配置方法在5.7.1版本以后或者SpringBoot 2.7.0版本以后已經(jīng)被棄用了。
- SpringBoot 2.7.0 版本前用法:
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
@Configuration
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {/*** http請求處理方法** @param http* @throws Exception*/@Overrideprotected void configure(HttpSecurity http) throws Exception {/*http.httpBasic()//開啟httpbasic認證.and().authorizeRequests().anyRequest().authenticated();//所有請求都需要登錄認證才能訪問*/http.formLogin() //開啟表單認證.and().authorizeRequests()..anyRequest().authenticated(); //所有請求都需要登錄認證才能訪問;}
}
- SpringBoot 2.7.0 版本以后
@Slf4j
@Configuration
@EnableWebSecurity //開啟SpringSecurity的默認行為
@RequiredArgsConstructor //bean注解
// 新版不需要繼承WebSecurityConfigurerAdapter
public class WebSecurityConfig {// 這個類主要是獲取庫中的用戶信息,交給securityprivate final UserDetailServiceImpl userDetailsService;// 這個的類是認證失敗處理(我在這里主要是把錯誤消息以json方式返回)private final JwtAuthenticationEntryPoint authenticationEntryPoint;// 鑒權失敗的時候的處理類private final JwtAccessDeniedHandler jwtAccessDeniedHandler;// 登錄成功處理private final LoginSuccessHandler loginSuccessHandler;// 登錄失敗處理private final LoginFailureHandler loginFailureHandler;// 登出成功處理private final LoginLogoutSuccessHandler loginLogoutSuccessHandler;// token過濾器private final JwtTokenFilter jwtTokenFilter;@Beanpublic AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception {return authenticationConfiguration.getAuthenticationManager();}// 加密方式@Beanpublic PasswordEncoder passwordEncoder() {return new BCryptPasswordEncoder();}/*** 核心配置*/@Beanpublic SecurityFilterChain filterChain(HttpSecurity http) throws Exception {log.info("------------filterChain------------");http// 禁用basic明文驗證.httpBasic(Customizer.withDefaults())// 基于 token ,不需要 csrf.csrf(AbstractHttpConfigurer::disable)// 禁用默認登錄頁.formLogin(fl - > fl.loginPage(PathMatcherUtil.FORM_LOGIN_URL).loginProcessingUrl(PathMatcherUtil.TO_LOGIN_URL).usernameParameter("username").passwordParameter("password").successHandler(loginSuccessHandler).failureHandler(loginFailureHandler).permitAll())// 禁用默認登出頁.logout(lt - > lt.logoutSuccessHandler(loginLogoutSuccessHandler))// 基于 token , 不需要 session.sessionManagement(session - > session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))// 設置 處理鑒權失敗、認證失敗.exceptionHandling(exceptions - > exceptions.authenticationEntryPoint(authenticationEntryPoint).accessDeniedHandler(jwtAccessDeniedHandler))// 下面開始設置權限.authorizeHttpRequests(authorizeHttpRequest - > authorizeHttpRequest// 允許所有 OPTIONS 請求.requestMatchers(PathMatcherUtil.AUTH_WHITE_LIST).permitAll()// 允許直接訪問 授權登錄接口// .requestMatchers(HttpMethod.POST, "/web/authenticate").permitAll()// 允許 SpringMVC 的默認錯誤地址匿名訪問// .requestMatchers("/error").permitAll()// 其他所有接口必須有Authority信息,Authority在登錄成功后的UserDetailImpl對象中默認設置“ROLE_USER”//.requestMatchers("/**").hasAnyAuthority("ROLE_USER")// .requestMatchers("/heartBeat/**", "/main/**").permitAll()// 允許任意請求被已登錄用戶訪問,不檢查Authority.anyRequest().authenticated())// 添加過濾器.addFilterBefore(jwtTokenFilter, UsernamePasswordAuthenticationFilter.class);//可以加載fram嵌套頁面http.headers(headers - > headers.frameOptions(HeadersConfigurer.FrameOptionsConfig::sameOrigin));return http.build();}@Beanpublic UserDetailsService userDetailsService() {return userDetailsService::loadUserByUsername;}/*** 調(diào)用loadUserByUserName獲取userDetail信息,在AbstractUserDetailsAuthenticationProvider里執(zhí)行用戶狀態(tài)檢查** @return*/@Beanpublic AuthenticationProvider authenticationProvider() {DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider();authProvider.setUserDetailsService(userDetailsService);authProvider.setPasswordEncoder(passwordEncoder());return authProvider;}/*** 配置跨源訪問(CORS)** @return*/@BeanCorsConfigurationSource corsConfigurationSource() {UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();source.registerCorsConfiguration("/**", new CorsConfiguration().applyPermitDefaultValues());return source;}
}
????????棄用原因,這是因為Spring框架的開發(fā)人員鼓勵用戶轉(zhuǎn)向基于組件的安全配置。
? ? ? ? 這里開始我也將SpringBoot的版本從3.3.3版本降回到2.3.4版本,為了方便后文的說明與復現(xiàn)。
1、發(fā)現(xiàn)問題
問題一:重定向次數(shù)過多
?????????因為設置登錄頁面為login.html 后面配置的是所有請求都登錄認證,陷入了死循環(huán). 所以需要將login.html放行不需要登錄認證。
//開啟表單認證http.formLogin().and().authorizeRequests()//放行登錄頁.antMatchers("/login.html").permitAll().anyRequest()//表示所有請求都需要登錄認證才能訪問.authenticated();
問題二:訪問登錄頁顯示404
????????spring boot整合thymeleaf 之后 所有的靜態(tài)頁面以放在resources/templates下面,所以得通過請求訪問到模板頁面, 將/login.html修改為 /toLoginPage
//開啟表單認證http.formLogin().loginPage("/toLoginPage").and().authorizeRequests()//放行登錄頁.antMatchers("/toLoginPage").permitAll().anyRequest()//表示所有請求都需要登錄認證才能訪問.authenticated();
問題三:訪問登錄頁,頁面樣式丟失
????????因為訪問login.html需要一些js , css , image等靜態(tài)資源信息, 所以需要將靜態(tài)資源放行, 不需要認證。
//解決靜態(tài)資源被攔截的問題
web.ignoring().antMatchers("/css/**","/images/**","/js/**","/favion.ico","/login/**");
????????Spring Security 中,安全構建器HttpSecurity 和WebSecurity 的區(qū)別是:
- WebSecurity 不僅通過HttpSecurity 定義某些請求的安全控制,也通過其他方式定義其他某些請求可以忽略安全控制;
- HttpSecurity 僅用于定義需要安全控制的請求(當然HttpSecurity 也可以指定某些請求不需要安全控制);
- 可以認為HttpSecurity 是WebSecurity 的一部分, WebSecurity 是包含HttpSecurity 的更大的一個概念;
- 構建目標不同
- WebSecurity 構建目標是整個Spring Security 安全過濾器FilterChainProxy`,
- HttpSecurity 的構建目標僅僅是FilterChainProxy 中的一個SecurityFilterChain 。
2.2.2、表單登錄
????????通過講解過濾器鏈中我們知道有個過濾器UsernamePasswordAuthenticationFilter是處理表單登錄的. 那么下面我們來通過源碼觀察下這個過濾器.
????????在源碼中可以觀察到, 表單中的input的name值是username和password, 并且表單提交的路徑為/login , 表單提交方式method為post , 這些可以修改為自定義的值.
@Override
protected void configure(HttpSecurity http) throws Exception {//開啟表單認證http.formLogin()//配置自定義登錄頁面.loginPage("/toLoginPage")//配置登錄請求的URL.loginProcessingUrl("/login")//修改登錄表單字段信息.usernameParameter("username").passwordParameter("password")//修改登錄成功后跳轉(zhuǎn)地址.successForwardUrl("/").and().authorizeRequests()//放行登錄頁.antMatchers("/toLoginPage").permitAll().anyRequest()//表示所有請求都需要登錄認證才能訪問.authenticated();//開閉CSRF防護http.csrf().disable();
}
? ? ? ? 最終登錄成功后,顯示如下圖:
? ? ? ? 可以看到雖然登錄成功了,但是頁面又有一個新問題。
????????發(fā)現(xiàn)行內(nèi)框架iframe這里出現(xiàn)問題了. Spring Security下,X-Frame-Options默認為DENY,非SpringSecurity環(huán)境下,X-Frame-Options的默認大多也是DENY,這種情況下,瀏覽器拒絕當前頁面加載任何Frame頁面,設置含義如下:
- DENY:瀏覽器拒絕當前頁面加載任何Frame頁面 此選擇是默認的.
- SAMEORIGIN:frame頁面的地址只能為同源域名下的頁面
@Override
protected void configure(HttpSecurity http) throws Exception {//開啟表單認證http.formLogin()//配置自定義登錄頁面.loginPage("/toLoginPage")//配置登錄請求的URL.loginProcessingUrl("/login")//修改登錄表單字段信息.usernameParameter("username").passwordParameter("password")//修改登錄成功后跳轉(zhuǎn)地址.successForwardUrl("/").and().authorizeRequests()//放行登錄頁.antMatchers("/toLoginPage").permitAll().anyRequest()//表示所有請求都需要登錄認證才能訪問.authenticated();//開閉CSRF防護http.csrf().disable();//允許Iframe頁面加載配置http.headers().frameOptions().sameOrigin();
}
2.2.3、基于數(shù)據(jù)庫實現(xiàn)認證功能
????????之前我們所使用的用戶名和密碼是來源于框架自動生成的, 那么我們?nèi)绾螌崿F(xiàn)基于數(shù)據(jù)庫中的用戶名和密碼功能呢? 要實現(xiàn)這個得需要實現(xiàn)security的一個UserDetailsService接口, 重寫這個接口里面 loadUserByUsername 即可。
????????編寫MyUserDetailsService并實現(xiàn)UserDetailsService接口,重寫loadUserByUsername方法。
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.Collection;@Service
public class MyUserDetailsService implements UserDetailsService {@AutowiredUserService userService;/*** 根據(jù)username查詢用戶實體** @param username* @return* @throws UsernameNotFoundException*/@Overridepublic UserDetails loadUserByUsername(String username) throwsUsernameNotFoundException {User user = userService.findByUsername(username);if(user == null) {throw new UsernameNotFoundException(username); // 用戶名沒有找到}// 先聲明一個權限集合, 因為構造方法里面不能傳入nullCollection < ? extends GrantedAuthority > authorities = new ArrayList < > ();// 需要返回一個SpringSecurity的UserDetails對象UserDetails userDetails = new org.springframework.security.core.userdetails.User(user.getUsername(), "{noop}" + user.getPassword(), // {noop}表示不加密認證。true, // 用戶是否啟用 true 代表啟用true, // 用戶是否過期 true 代表未過期true, // 用戶憑據(jù)是否過期 true 代表未過期true, // 用戶是否鎖定 true 代表未鎖定authorities);return userDetails;}
}
????????在SecurityConfiguration配置類中指定自定義用戶認證:
/*** 身份驗證管理器** @param auth* @throws Exception*/
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {auth.userDetailsService(myUserDetailsService); // 使用自定義用戶認證
}
2.2.4、密碼加密認證
????????在基于數(shù)據(jù)庫完成用戶登錄的過程中,我們所是使用的密碼是明文的,規(guī)則是通過對密碼明文添加?{noop} 前綴。那么下面 Spring Security 中的密碼編碼進行一些探討。
????????Spring Security 中PasswordEncoder 就是我們對密碼進行編碼的工具接口。該接口只有兩個功能:一個是匹配驗證。另一個是密碼編碼。
算法介紹:
- BCrypt算法:
????????任何應用考慮到安全,絕不能明文的方式保存密碼。密碼應該通過哈希算法進行加密。 有很多標準的算法比如SHA或者MD5,結合salt(鹽)是一個不錯的選擇。 Spring Security 提供了BCryptPasswordEncoder類,實現(xiàn)Spring的PasswordEncoder接口使用BCrypt強哈希方法來加密密碼。BCrypt強哈希方法 每次加密的結果都不一樣,所以更加的安全。
????????bcrypt算法相對來說是運算比較慢的算法,在密碼學界有句常話:越慢的算法越安全。黑客破解成本越高.通過salt和const這兩個值來減緩加密過程,它的加密時間(百ms級)遠遠超過md5(大概1ms左右)。對于計算機來說,Bcrypt 的計算速度很慢,但是對于用戶來說,這個過程不算慢。bcrypt是單向的,而且經(jīng)過salt和cost的處理,使其受攻擊破解的概率大大降低,同時破解的難度也提升不少,相對于MD5等加密方式更加安全,而且使用也比較簡單。
????????bcrypt加密后的字符串形如:$2a$10$wouq9P/HNgvYj2jKtUN8rOJJNRVCWvn1XoWy55N3sCkEHZPo3lyWq
????????其中$是分割符,無意義;2a是bcrypt加密版本號;10是const的值;而后的前22位是salt值;再然后的字符串就是密碼的密文了;這里的const值即生成salt的迭代次數(shù),默認值是10,推薦值12。
- 在項目中使用BCrypt:
????????首先看下PasswordEncoderFactories 密碼器工廠:
????????之前我們在項目中密碼使用的是明文的是noop , 代表不加密使用明文密碼, 現(xiàn)在用BCrypt只需要將 noop 換成 bcrypt 即可。
@Override
public UserDetails loadUserByUsername(String username) throws
UsernameNotFoundException {User user = userService.findByUsername(username);if(user == null) {throw new UsernameNotFoundException(username); // 用戶名沒有找到}// 先聲明一個權限集合, 因為構造方法里面不能傳入nullCollection < ? extends GrantedAuthority > authorities = new ArrayList < > ();// 需要返回一個SpringSecurity的UserDetails對象UserDetails userDetails = neworg.springframework.security.core.userdetails.User(user.getUsername(), "{bcrypt}" + user.getPassword(), // {noop}表示不加密認證。 {bcrypt}加密認證 true, // 用戶是否啟用 true 代表啟用true, // 用戶是否過期 true 代表未過期true, // 用戶憑據(jù)是否過期 true 代表未過期true, // 用戶是否鎖定 true 代表未鎖定authorities);return userDetails;
}
????????同時需要將數(shù)據(jù)庫中的明文密碼修改為加密密碼:
????????選擇一個放入數(shù)據(jù)庫即可。
2.2.5、獲取當前登錄用戶
????????在傳統(tǒng)web系統(tǒng)中, 我們將登錄成功的用戶放入session中, 在需要的時候可以從session中獲取用戶,那么Spring Security中我們?nèi)绾潍@取當前已經(jīng)登錄的用戶呢?
- SecurityContextHolder:
????????保留系統(tǒng)當前的安全上下文SecurityContext,其中就包括當前使用系統(tǒng)的用戶的信息。
- SecurityContext:
????????安全上下文,獲取當前經(jīng)過身份驗證的主體或身份驗證請求令牌。
/*** 獲取當前登錄用戶** @return*/
@RequestMapping("/loginUser")
@ResponseBody
public UserDetails getCurrentUser() {UserDetails userDetails = (UserDetails)SecurityContextHolder.getContext().getAuthentication().getPrincipal();return userDetails;
}
????????除了上述方法, Spring Security 還提供了2種方式可以獲取.
/*** 獲取當前登錄用戶** @return*/
@RequestMapping("/loginUser2")
@ResponseBody
public UserDetails getCurrentUser(Authentication authentication) {UserDetails userDetails = (UserDetails) authentication.getPrincipal();return userDetails;
}/*** 獲取當前登錄用戶** @return*/
@RequestMapping("/loginUser3")
@ResponseBody
public UserDetails getCurrentUser(@AuthenticationPrincipal UserDetails userDetails) {return userDetails;
}
2.2.6、記住我
????????在大多數(shù)網(wǎng)站中,都會實現(xiàn)RememberMe這個功能,方便用戶在下一次登錄時直接登錄,避免再次輸入用戶名以及密碼去登錄,Spring Security針對這個功能已經(jīng)幫助我們實現(xiàn), 下面我們來看下他的原理圖。
簡單的Token生成方法:
????????Token=MD5(username+分隔符+expiryTime+分隔符+password)
????????注意: 這種方式不推薦使用, 有嚴重的安全問題. 就是密碼信息在前端瀏覽器cookie中存放. 如果cookie被盜取很容易破解.
<div class="form-group"><div><!--記住我 name為remember-me value值可選true yes 1 on 都行--><input type="checkbox" name="remember-me" value="true" />記住我</div>
</div>
@Override
protected void configure(HttpSecurity http) throws Exception {//開啟表單認證http.formLogin()//配置自定義登錄頁面.loginPage("/toLoginPage")//配置登錄請求的URL.loginProcessingUrl("/login")//修改登錄表單字段信息.usernameParameter("username").passwordParameter("password")//修改登錄成功后跳轉(zhuǎn)地址.successForwardUrl("/")//開啟記住我的功能.and().rememberMe()//設置token失效時間為兩周.tokenValiditySeconds(1209600)//自定義表單name值.rememberMeParameter("remember-me").and().authorizeRequests()//放行登錄頁.antMatchers("/toLoginPage").permitAll().anyRequest()//表示所有請求都需要登錄認證才能訪問.authenticated();//開閉CSRF防護http.csrf().disable();//允許Iframe頁面加載配置http.headers().frameOptions().sameOrigin();
}
持久化的Token生成方法:
存入數(shù)據(jù)庫Token包含:
- token: 隨機生成策略,每次訪問都會重新生成
- series: 登錄序列號,隨機生成策略。用戶輸入用戶名和密碼登錄時,該值重新生成。使用?remember-me功能,該值保持不變
- expiryTime: token過期時間。
CookieValue=encode(series+token)
/*** http請求處理方法** @param http* @throws Exception*/
@Override
protected void configure(HttpSecurity http) throws Exception {/*http.httpBasic()//開啟httpbasic認證.and().authorizeRequests().anyRequest().authenticated();//所有請求都需要登錄認證才能訪問*/http.formLogin() //開啟表單認證.loginPage("/toLoginPage") //自定義登錄頁面.loginProcessingUrl("/login") // 登錄處理Url//.usernameParameter().passwordParameter(). 修改自定義表單name值..successForwardUrl("/") // 登錄成功后跳轉(zhuǎn)路徑.and().authorizeRequests().antMatchers("/toLoginPage").permitAll() //放行登錄頁面與靜態(tài)資源.anyRequest().authenticated() //所有請求都需要登錄認證才能訪問;.and().rememberMe() //開啟記住我功能.tokenValiditySeconds(1209600) // token失效時間默認2周.rememberMeParameter("remember-me") // 自定義表單name值.tokenRepository(getPersistentTokenRepository()); // 設置tokenRepository// 關閉csrf防護http.csrf().disable();// 允許iframe加載頁面http.headers().frameOptions().sameOrigin();
}@Autowired
DataSource dataSource;/*** 持久化token,負責token與數(shù)據(jù)庫之間的相關操作** @return*/
@Bean
public PersistentTokenRepository getPersistentTokenRepository() {JdbcTokenRepositoryImpl tokenRepository = newJdbcTokenRepositoryImpl();tokenRepository.setDataSource(dataSource); //設置數(shù)據(jù)源// 啟動時創(chuàng)建一張表, 第一次啟動的時候創(chuàng)建, 第二次啟動的時候需要注釋掉, 否則會報錯tokenRepository.setCreateTableOnStartup(true);return tokenRepository;
}
????????項目啟動成功后,觀察數(shù)據(jù)庫,會幫助我們創(chuàng)建persistent_logins表。
? ? ? ? 當再次訪問登錄功能時,觀察數(shù)據(jù)庫,會插入一條記錄.說明持久化token方式已經(jīng)生效。
Cookie 偽造演示:
????????使用網(wǎng)頁登錄系統(tǒng),記錄remember-me的值,使用postman 偽造cookie。
安全驗證:
/*** 根據(jù)用戶ID查詢用戶** @return*/
@GetMapping("/{id}")
@ResponseBody
public User getById(@PathVariable Integer id) {//獲取認證信息Authentication authentication = SecurityContextHolder.getContext().getAuthentication();// 判斷認證信息是否來源于RememberMeif(RememberMeAuthenticationToken.class.isAssignableFrom(authentication.getClas s())) {throw new RememberMeAuthenticationException("認證信息來源于RememberMe, 請重新登錄 ");}User user = userService.getById(id);return user;}
????????在重要操作步驟可以加以驗證, true代表自動登錄,則引導用戶重新表單登錄, false正常進行。
2.2.7、自定義登錄成功&失敗處理
????????在某些場景下,用戶登錄成功或失敗的情況下用戶需要執(zhí)行一些后續(xù)操作,比如登錄日志的搜集,或者在現(xiàn)在目前前后端分離的情況下用戶登錄成功和失敗后需要給前臺頁面返回對應的錯誤信息, 有前臺主導登錄成功或者失敗的頁面跳轉(zhuǎn). 這個時候需要要到用到AuthenticationSuccessHandler與AnthenticationFailureHandler.
自定義成功處理:
????????實現(xiàn)AuthenticationSuccessHandler接口,并重寫onAnthenticationSuccesss()方法。
自定義失敗處理:
????????實現(xiàn)AuthenticationFailureHandler接口,并重寫onAuthenticationFailure()方法;
@Override
protected void configure(HttpSecurity http) throws Exception {//開啟表單認證http.formLogin()//配置自定義登錄頁面.loginPage("/toLoginPage")//配置登錄請求的URL.loginProcessingUrl("/login")//修改登錄表單字段信息.usernameParameter("username").passwordParameter("password")//修改登錄成功后跳轉(zhuǎn)地址.successForwardUrl("/").successHandler(myAuthenticationService).failureHandler(myAuthenticationService) //登錄成功或者失敗的處理//開啟記住我的功能.and().rememberMe()//設置token失效時間為兩周.tokenValiditySeconds(1209600)//自定義表單name值.rememberMeParameter("remember-me").and().authorizeRequests()//放行登錄頁.antMatchers("/toLoginPage").permitAll().anyRequest()//表示所有請求都需要登錄認證才能訪問.authenticated();//開閉CSRF防護http.csrf().disable();//允許Iframe頁面加載配置http.headers().frameOptions().sameOrigin();
}
/*** 自定義登錄成功或失敗處理器,退出登錄處理器*/
@Service
public class MyAuthenticationService implements AuthenticationSuccessHandler,AuthenticationFailureHandler, LogoutSuccessHandler {RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();@AutowiredObjectMapper objectMapper;/*** 登錄成功后處理邏輯** @param request* @param response* @param authentication* @throws IOException* @throws ServletException*/@Overridepublic void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {System.out.println("登錄成功后繼續(xù)處理......");// 重定向到index頁面// redirectStrategy.sendRedirect(request, response, "/");Map result = new HashMap();result.put("code", HttpStatus.OK.value());//200result.put("message", "登錄成功");response.setContentType("application/json;charset=UTF-8");response.getWriter().write(objectMapper.writeValueAsString(result));}/*** 登錄失敗的處理邏輯** @param request* @param response* @param exception* @throws IOException* @throws ServletException*/@Overridepublic void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {System.out.println("登錄失敗后繼續(xù)處理......");// 重定向到login頁面// redirectStrategy.sendRedirect(request, response, "/toLoginPage");Map result = new HashMap();result.put("code", HttpStatus.UNAUTHORIZED.value());//401result.put("message", exception.getMessage());response.setContentType("application/json;charset=UTF-8");response.getWriter().write(objectMapper.writeValueAsString(result));}@Overridepublic void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {System.out.println("退出之后繼續(xù)處理...");redirectStrategy.sendRedirect(request, response, "/toLoginPage");}
}
異步用戶登錄實現(xiàn):
<form id="formLogin" action="/login" method="post"><div class="panel loginbox">.....<div style="padding:30px;"><input type="button" onclick="login()"class="button button-block bg-main textbig
input-big" value="登錄"></div></div>
</form>
</div>
</div>
</div>
<script>function login() {$.ajax({type: "POST",//方法類型dataType: "json",//服務器預期返回類型url: "/login", // 登錄urldata: $("#formLogin").serialize(),success: function (data) {console.log(data)if (data.code == 200) {window.location.href = "/";} else {alert(data.message);}}});}
</script>
2.2.8、退出登錄
實現(xiàn)拓展類:
- 類全路徑:org.springframework.security.web.authentication.logout.LogoutFilter
- 作用:匹配URL為/logout的請求,實現(xiàn)用戶退出,清除認證信息。
????????只需要發(fā)送請求,請求路徑為/logout即可, 當然這個路徑也可以自行在配置類中自行指定, 同時退出操作也有對應的自定義處理LogoutSuccessHandler,退出登錄成功后執(zhí)行,退出的同時如果有remember-me的數(shù)據(jù),同時一并刪除。
- 前端頁面
<a class="button button-little bg-red" href="/logout"><span class="icon-power-off"></span>退出登錄
</a>
- 后端代碼
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.DefaultRedirectStrategy;
import org.springframework.security.web.RedirectStrategy;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;
import org.springframework.stereotype.Service;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;/*** 自定義登錄成功,失敗,退出處理類*/
@Service
public class MyAuthenticationService implements AuthenticationSuccessHandler,AuthenticationFailureHandler, LogoutSuccessHandler {private RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();//................@Overridepublic void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throwsIOException, ServletException {System.out.println("退出成功后續(xù)處理....");redirectStrategy.sendRedirect(request, response, "/toLoginPage");}
}
@Overrideprotected void configure(HttpSecurity http) throws Exception {//開啟表單認證http.formLogin()//配置自定義登錄頁面.loginPage("/toLoginPage")//配置登錄請求的URL.loginProcessingUrl("/login")//修改登錄表單字段信息.usernameParameter("username").passwordParameter("password")//修改登錄成功后跳轉(zhuǎn)地址.successForwardUrl("/")//開啟記住我的功能.and().rememberMe()//設置token失效時間為兩周.tokenValiditySeconds(1209600)//自定義表單name值.rememberMeParameter("remember-me")/*** 退出相關操作配置**/.and().logout()//設置退出url地址.logoutUrl("/logout").logoutSuccessHandler(myAuthenticationService).and().authorizeRequests()//放行登錄頁.antMatchers("/toLoginPage").permitAll().anyRequest()//表示所有請求都需要登錄認證才能訪問.authenticated();//開閉CSRF防護http.csrf().disable();//允許Iframe頁面加載配置http.headers().frameOptions().sameOrigin();}
2.3、圖形驗證碼認證
????????圖形驗證碼一般是防止惡意,人眼看起來都費勁,何況是機器。不少網(wǎng)站為了防止用戶利用機器人自動注冊、登錄、灌水,都采用了驗證碼技術。所謂驗證碼,就是將一串隨機產(chǎn)生的數(shù)字或符號,生成一幅圖片, 圖片里加上一些干擾, 也有目前需要手動滑動的圖形驗證碼. 這種可以有專門去做的第三方平臺. 比如極驗。spring security添加驗證碼大致可以分為三個步驟:
- 根據(jù)隨機數(shù)生成驗證碼圖片
- 將驗證碼圖片顯示到登錄頁面
- 認證流程中加入驗證碼校驗
????????Spring Security的認證校驗是由UsernamePasswordAuthenticationFilter過濾器完成的,所以我們的驗證碼校驗邏輯應該在這個過濾器之前。驗證碼通過后才能到后續(xù)的操作. 流程如下:
自定義驗證碼過濾器 ValidateCodeFilter:
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.ServletRequestBindingException;
import org.springframework.web.filter.OncePerRequestFilter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;@Component
public class ValidateCodeFilter extends OncePerRequestFilter {@AutowiredMyAuthenticationService myAuthenticationService;@AutowiredStringRedisTemplate stringRedisTemplate;@Overrideprotected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throwsServletException, IOException {// 判斷是否是登錄請求,只有登錄請求才去校驗驗證碼if(httpServletRequest.getRequestURI().equals("/login") && httpServletRequest.getMethod().equalsIgnoreCase("post")) {try {validate(httpServletRequest);}catch (ValidateCodeException e) {myAuthenticationService.onAuthenticationFailure(httpServletRequest, httpServletResponse, e);return;}}//如果不是登錄請求,直接調(diào)用后面的過濾器鏈filterChain.doFilter(httpServletRequest, httpServletResponse);}private void validate(HttpServletRequest request) throws ServletRequestBindingException {//獲取ipString remoteAddr = request.getRemoteAddr();//拼接redis的keyString redisKey = ValidateCodeController.REDIS_KEY_IMAGE_CODE + "-" + remoteAddr;//從redis中獲取imageCodeString redisImageCode = stringRedisTemplate.boundValueOps(redisKey).get();String imageCode = request.getParameter("imageCode");if(!StringUtils.hasText(imageCode)) {throw new ValidateCodeException("驗證碼的值不能為空!");}if(redisImageCode == null) {throw new ValidateCodeException("驗證碼已過期!");}if(!redisImageCode.equals(imageCode)) {throw new ValidateCodeException("驗證碼不正確!");}// 從redis中刪除imageCodestringRedisTemplate.delete(redisKey);}
}
自定義驗證碼異常類:
import org.springframework.security.core.AuthenticationException;/*** 驗證碼異常類*/
public class ValidateCodeException extends AuthenticationException {public ValidateCodeException(String msg) {super(msg);}
}
security配置類:
@Autowired
ValidateCodeFilter validateCodeFilter;/*** http請求處理方法** @param http* @throws Exception*/
@Override
protected void configure(HttpSecurity http) throws Exception {/*http.httpBasic()//開啟httpbasic認證.and().authorizeRequests().anyRequest().authenticated();//所有請求都需要登錄認證才能訪問*/// 加在用戶名密碼過濾器的前面http.addFilterBefore(validateCodeFilter, UsernamePasswordAuthenticationFilter.class);http.formLogin() //開啟表單認證.loginPage("/toLoginPage") //自定義登錄頁面.loginProcessingUrl("/login") // 登錄處理Url//.usernameParameter().passwordParameter(). 修改自定義表單name 值..successForwardUrl("/") // 登錄成功后跳轉(zhuǎn)路徑.successHandler(myAuthenticationService) //自定義登錄成功處理.failureHandler(myAuthenticationService) //自定義登錄失敗處理.and().logout().logoutUrl("/logout") //設置退出url.logoutSuccessHandler(myAuthenticationService) //自定義退出處理.and().authorizeRequests().antMatchers("/toLoginPage", "/code/**").permitAll() //放行登錄頁面與靜態(tài)資源.anyRequest().authenticated() //所有請求都需要登錄認證才能訪問;.and().rememberMe() //開啟記住我功能.tokenValiditySeconds(1209600) // token失效時間默認2周.rememberMeParameter("remember-me") // 自定義表單name值.tokenRepository(getPersistentTokenRepository()); // 設置tokenRepository// 關閉csrf防護http.csrf().disable();// 允許iframe加載頁面http.headers().frameOptions().sameOrigin();
}
2.4、session 管理
????????Spring Security可以與Spring Session庫配合使用,只需要做一些簡單的配置就可以實現(xiàn)一些功能,如(會話過期、一個賬號只能同時在線一個、集群session等)。
2.4.1、會話超時
????????配置session會話超時時間,默認為30分鐘,但是Spring Boot中的會話超時時間至少為60秒。當session超時后, 默認跳轉(zhuǎn)到登錄頁面。
#session設置
#配置session超時時間
server.servlet.session.timeout=60
????????自定義設置session超時后地址,設置session管理和失效后跳轉(zhuǎn)地址。
http.sessionManagement()//設置session管理.invalidSessionUrl("/toLoginPage")// session無效后跳轉(zhuǎn)的路徑,默認是登錄頁面
2.4.2、并發(fā)控制
????????并發(fā)控制即同一個賬號同時在線個數(shù),同一個賬號同時在線個數(shù)如果設置為1表示,該賬號在同一時間內(nèi)只能有一個有效的登錄,如果同一個賬號又在其它地方登錄,那么就將上次登錄的會話過期,即后面的登錄會踢掉前面的登錄。
1、修改超時時間
#session設置
#配置session超時時間
server.servlet.session.timeout=600
2、設置最大會話數(shù)量
http.sessionManagement() //設置session管理.invalidSessionUrl("/toLoginPage") // session無效后跳轉(zhuǎn)的路徑, 默認是登錄頁面.maximumSessions(1) //設置session最大會話數(shù)量 ,1同一時間只能有一個用戶登錄.expiredUrl("/toLoginPage"); //設置session過期后跳轉(zhuǎn)路徑
3、阻止用戶第二次登錄
????????sessionManagement也可以配置 maxSessionsPreventsLogin:boolean值,當達到maximumSessions設置的最大會話個數(shù)時阻止登錄。
http.sessionManagement()//設置session管理.invalidSessionUrl("/toLoginPage")// session無效后跳轉(zhuǎn)的路徑, 默認是登錄頁面.maximumSessions(1)//設置session最大會話數(shù)量 ,1同一時間只能有一個用戶登錄.maxSessionsPreventsLogin(true)//當達到最大會話個數(shù)時阻止登錄。.expiredUrl("/toLoginPage");//設置session過期后跳轉(zhuǎn)路徑
2.4.3、集群 Session
????????實際場景中一個服務會至少有兩臺服務器在提供服務,在服務器前面會有一個nginx做負載均衡,用戶訪問nginx,nginx再決定去訪問哪一臺服務器。當一臺服務宕機了之后,另一臺服務器也可以繼續(xù)提供服務,保證服務不中斷。如果我們將session保存在Web容器(比如tomcat)中,如果一個用戶第一次訪問被分配到服務器1上面需要登錄,當某些訪問突然被分配到服務器二上,因為服務器二上沒有用戶在服務器一上登錄的會話session信息,服務器二還會再次讓用戶登錄,用戶已經(jīng)登錄了還讓登錄就感覺不正常了。
????????解決這個問題的思路是用戶登錄的會話信息不能再保存到Web服務器中,而是保存到一個單獨的庫(redis、mongodb、jdbc等)中,所有服務器都訪問同一個庫,都從同一個庫來獲取用戶的session信息,如用戶在服務器一上登錄,將會話信息保存到庫中,用戶的下次請求被分配到服務器二,服務器二從庫中檢查session是否已經(jīng)存在,如果存在就不用再登錄了,可以直接訪問服務了。
引用依賴:
<!-- 基于redis實現(xiàn)session共享 -->
<dependency><groupid>org.springframework.session</groupid><artifactid>spring-session-data-redis</artifactid>
</dependency>
設置session存儲類型:
#使用redis共享session
spring.session.store-type=redis
2.5、CSRF 防護機制
2.5.1、什么是 CSRF?
????????CSRF(Cross-site request forgery),中文名稱:跨站請求偽造。
????????你這可以這么理解CSRF攻擊:攻擊者盜用了你的身份,以你的名義發(fā)送惡意請求。CSRF能夠做的事情包括:以你名義發(fā)送郵件,發(fā)消息,盜取你的賬號,甚至于購買商品,虛擬貨幣轉(zhuǎn)賬......造成的問題包括:個人隱私泄露以及財產(chǎn)安全。
????????CSRF這種攻擊方式在2000年已經(jīng)被國外的安全人員提出,但在國內(nèi),直到06年才開始被關注,08年,國內(nèi)外的多個大型社區(qū)和交互網(wǎng)站分別爆出CSRF漏洞,如:NYTimes.com(紐約時報)、Metafilter(一個大型的BLOG網(wǎng)站),YouTube和百度HI......而現(xiàn)在,互聯(lián)網(wǎng)上的許多站點仍對此毫無防備,以至于安全業(yè)界稱CSRF為“沉睡的巨人”。
2.5.2、CSRF 的原理
????????從上圖可以看出,要完成一次CSRF攻擊,受害者必須依次完成三個步驟:
- 登錄受信任網(wǎng)站A,并在本地生成Cookie。
- 在不登出A的情況下,訪問危險網(wǎng)站B。
- 觸發(fā)網(wǎng)站B中的一些元素
2.5.3、CSRF 的防御策略
????????在業(yè)界目前防御 CSRF 攻擊主要有三種策略:驗證 HTTP Referer 字段;在請求地址中添加 token?并驗證;在 HTTP 頭中自定義屬性并驗證。
1、驗證 HTTP Referer 字段:
????????根據(jù) HTTP 協(xié)議,在 HTTP 頭中有一個字段叫 Referer,它記錄了該 HTTP 請求的來源地址。在通常情況下,訪問一個安全受限頁面的請求來自于同一個網(wǎng)站,在后臺請求驗證其 Referer 值,如果是以自身安全網(wǎng)站開頭的域名,則說明該請求是是合法的。如果 Referer 是其他網(wǎng)站的話,則有可能是黑客的 CSRF 攻擊,拒絕該請求。
2、在請求地址中添加 token 并驗證:
????????CSRF 攻擊之所以能夠成功,是因為黑客可以完全偽造用戶的請求,該請求中所有的用戶驗證信息都是存在于 cookie 中,因此黑客可以在不知道這些驗證信息的情況下直接利用用戶自己的cookie 來通過安全驗證。要抵御 CSRF,關鍵在于在請求中放入黑客所不能偽造的信息,并且該信息不存在于 cookie 之中。可以在 HTTP 請求中以參數(shù)的形式加入一個隨機產(chǎn)生的 token,并在服務器端建立一個攔截器來驗證這個 token,如果請求中沒有 token 或者 token 內(nèi)容不正確,則認為可能是 CSRF 攻擊而拒絕該請求。
3、在 HTTP 頭中自定義屬性并驗證:
????????這種方法也是使用 token 并進行驗證,和上一種方法不同的是,這里并不是把 token 以參數(shù)的形式置于 HTTP 請求之中,而是把它放到 HTTP 頭中自定義的屬性里。
2.5.4、SpringSecurity CSRF 的防御機制
- 完整類路徑:org.springframework.security.web.csrf.CsrfFilter
- 作用:csrf又稱跨站請求偽造,SpringSecurity會對所有post請求驗證是否包含系統(tǒng)生成的csrf的token信息,如果不包含,則報錯。起到防止csrf攻擊的效果。(1. 生成token 2.驗證token)
1、開啟CSRF防護:
//開啟csrf防護, 可以設置哪些不需要防護
http.csrf().ignoringAntMatchers("/user/save");
2、頁面需要添加token值:
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}"/>
2.6、跨域與CORS
2.6.1、跨域
????????跨域,實質(zhì)上是瀏覽器的一種保護處理。如果產(chǎn)生了跨域,服務器在返回結果時就會被瀏覽器攔截(注意:此時請求是可以正常發(fā)起的,只是瀏覽器對其進行了攔截),導致響應的內(nèi)容不可用. 產(chǎn)生跨域的幾種情況有一下:
當前頁面URL | 被請求頁面URL | 是否跨域 | 原因 |
---|---|---|---|
http://www.blnp.com/ | http://www.blnp.com/index.html | 否 | 同源(協(xié)議、域名、端口號等一致) |
http://www.blnp.com/ | https://www.blnp.com/index.html | 跨域 | 協(xié)議不同(http/https) |
http://www.blnp.com/ | 百度一下,你就知道 | 跨域 | 主域名不同(blnp/baidu) |
http://www.blnp.com/ | http://kuale.blnp.com/ | 跨域 | 子域名不同(www/kuale) |
http://www.blnp.com:8080 | http://www.blnp.com:8090 | 跨域 | 端口號不同(8080/8090) |
2.6.2、解決跨域
1、JSONP
????????瀏覽器允許一些帶src屬性的標簽跨域,也就是在某些標簽的src屬性上寫url地址是不會產(chǎn)生跨域問題。
2、CORS 解決跨域
????????CORS是一個W3C標準,全稱是"跨域資源共享"(Cross-origin resource sharing)。CORS需要瀏覽器和服務器同時支持。目前,所有瀏覽器都支持該功能,IE瀏覽器不能低于IE10。瀏覽器在發(fā)起真正的請求之前,會發(fā)起一個OPTIONS類型的預檢請求,用于請求服務器是否允許跨域,在得到許可的情況下才會發(fā)起請求。
2.6.3、SpringSecurity 的CORS 支持
1、聲明跨域配置源
/*** 跨域配置信息源** @return*/
public CorsConfigurationSource corsConfigurationSource() {CorsConfiguration corsConfiguration = new CorsConfiguration();// 設置允許跨域的站點corsConfiguration.addAllowedOrigin("*");// 設置允許跨域的http方法corsConfiguration.addAllowedMethod("*");// 設置允許跨域的請求頭corsConfiguration.addAllowedHeader("*");// 允許帶憑證corsConfiguration.setAllowCredentials(true);// 對所有的url生效UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();source.registerCorsConfiguration("/**", corsConfiguration);return source;
}
2、開啟跨域支持
//允許跨域
http.cors().configurationSource(corsConfigurationSource());
3、前端跨域測試代碼
function toCors() {$.ajax({// 默認情況下,標準的跨域請求是不會發(fā)送cookie的xhrFields: {withCredentials: true},// 登錄urlurl: "http://localhost:8090/user/1",success: function(data) {alert("請求成功." + data)}});
}
3、SpringSecurity 授權
3.1、簡介
3.1.1、SpringSecurity 授權定義
說明:安全權限控制問題其實就是控制能否訪問url
3.1.2、SpringSecurity 授權原理
????????在我們應用系統(tǒng)里面,如果想要控制用戶權限,需要有2部分數(shù)據(jù)。
- 系統(tǒng)配置信息數(shù)據(jù):寫著系統(tǒng)里面有哪些URL,每一個url擁有哪些權限才允許被訪問。
- 另一份數(shù)據(jù)就是用戶權限信息:請求用戶擁有權限
????????系統(tǒng)用戶發(fā)送一個請求:系統(tǒng)配置信息和用戶權限信息作比對,如果比對成功則允許訪問。
????????當一個系統(tǒng)授權規(guī)則比較簡單,基本不變時候,系統(tǒng)的權限配置信息可以寫在我們的代碼里面去的。比如前臺門戶網(wǎng)站等權限比較單一,可以使用簡單的授權配置即可完成,如果權限復雜, 例如辦公OA, 電商后臺管理系統(tǒng)等就不能使用寫在代碼里面了. 需要RBAC權限模型設計.
3.2、SpringSecurity 授權
3.2.1、內(nèi)置權限表達式
????????Spring Security 使用Spring EL來支持,主要用于Web訪問和方法安全上, 可以通過表達式來判斷是否具有訪問權限. 下面是Spring Security常用的內(nèi)置表達式.? ExpressionUrlAuthorizationConfigurer 定義了所有的表達式:
表達式 | 說明 |
---|---|
permitAll | 指定任何人都允許訪問。 |
denyAll | 指定任何人都不允許訪問 |
anonymous | 指定匿名用戶允許訪問。 |
rememberMe | 指定已記住的用戶允許訪問。 |
authenticated | 指定任何經(jīng)過身份驗證的用戶都允許訪問,不包含 anonymous |
fullyAuthenticated | 指定由經(jīng)過身份驗證的用戶允許訪問,不包含 anonymous和rememberMe |
hasRole(role) | 指定需要特定的角色的用戶允許訪問, 會自動在角色前面插入'ROLE_' |
hasAnyRole([role1,role2]) | 指定需要任意一個角色的用戶允許訪問, 會自動在角色前面插入'ROLE_' |
hasAuthority(authority) | 指定需要特定的權限的用戶允許訪問 |
hasAnyAuthority([authority,authority]) | 指定需要任意一個權限的用戶允許訪問 |
hasIpAddress(ip) | 指定需要特定的IP地址可以訪問 |
3.2.2、URL安全表達式
????????基于web訪問使用表達式保護url請求路徑.
1、設置 URL 訪問權限
// 設置/user/** 訪問需要ADMIN角色
http.authorizeRequests().antMatchers("/user/**").hasRole("ADMIN");
// 設置/user/** 訪問需要PRODUCT角色和IP地址為127.0.0.1 .hasAnyRole("PRODUCT,ADMIN")
http.authorizeRequests().antMatchers("/product/**").access("hasAnyRole('ADMIN,PRODUCT') and hasIpAddress('127.0.0.1')");
// 設置自定義權限不足信息.
http.exceptionHandling().accessDeniedHandler(accessDeniedHandler);
2、MyAccessDeniedHandler自定義權限不足類
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.stereotype.Component;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/*** 自定義權限不足信息*/
@Component
public class MyAccessDeniedHandler implements AccessDeniedHandler {@Overridepublic void handle(HttpServletRequest httpServletRequest, HttpServletResponse resp, AccessDeniedException e) throws IOException,ServletException {resp.setStatus(HttpServletResponse.SC_FORBIDDEN);resp.setContentType("text/html;charset=UTF-8");resp.getWriter().write("權限不足,請聯(lián)系管理員!");}
}
3、設置用戶對應的角色權限
// 先聲明一個權限集合, 因為構造方法里面不能傳入null
Collection < GrantedAuthority > authorities = new ArrayList < > ();
if("admin".equalsIgnoreCase(user.getUsername())) {authorities.add(new SimpleGrantedAuthority("ROLE_ADMIN"));
}
else {authorities.add(new SimpleGrantedAuthority("ROLE_PRODUCT"));
}
3.2.3、Web 安全表達式引用自定義 Bean 授權
1、定義自定義授權類
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.User;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpServletRequest;
import java.util.Collection;
/*** 自定義授權類*/
@Component
public class MyAuthorizationService {/*** 檢查用戶是否有對應的訪問權限** @param authentication 登錄用戶* @param request 請求對象* @return*/public boolean check(Authentication authentication, HttpServletRequest request) {User user = (User) authentication.getPrincipal();// 獲取用戶所有權限Collection < GrantedAuthority > authorities = user.getAuthorities();// 獲取用戶名String username = user.getUsername();// 如果用戶名為admin,則不需要認證if(username.equalsIgnoreCase("admin")) {return true;}else {// 循環(huán)用戶的權限, 判斷是否有ROLE_ADMIN權限, 有返回truefor(GrantedAuthority authority: authorities) {String role = authority.getAuthority();if("ROLE_ADMIN".equals(role)) {return true;}}}return false;}
}
2、配置類
//使用自定義Bean授權
http.authorizeRequests().antMatchers("/user/**").access("@myAuthorizationService.check(authentication,request)");
3、攜帶路徑變量
/*** 檢查用戶是否有對應的訪問權限** @param authentication 登錄用戶* @param request 請求對象* @param id 參數(shù)ID* @return*/
public boolean check(Authentication authentication, HttpServletRequest request, Integer id) {if(id > 10) {return false;}return true;
}//使用自定義Bean授權,并攜帶路徑參數(shù)
http.authorizeRequests().antMatchers("/user/delete/{id}").access("@myAuthorizationService.check(authentication,request,#id)");
3.2.4、Method 安全表達式
????????針對方法級別的訪問控制比較復雜, spring security 提供了4種注解分別是:
- @PreAuthorize
- @PostAuthorize
- @PreFilter
- @PostFilter
1、開啟方法級別的注解配置
????????在security配置類中添加注解:
/*** Security配置類*/
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true) //開啟注解支持
public class SecurityConfiguration extends WebSecurityConfigurerAdapter
2、在方法上使用注解
@ProAuthorize: 注解適合進入方法前的權限驗證
/*** 查詢所有用戶** @return*/
@RequestMapping("/findAll")
@PreAuthorize("hasRole('ADMIN')") //需要ADMIN權限
public String findAll(Model model) {List < User > userList = userService.list();model.addAttribute("userList", userList);return "user_list";
}
/*** 用戶修改頁面跳轉(zhuǎn)** @return*/
@RequestMapping("/update/{id}")
@PreAuthorize("#id<10") //針對參數(shù)權限限定 id<10可以訪問
public String update(@PathVariable Integer id, Model model) {User user = userService.getById(id);model.addAttribute("user", user);return "user_update";
}
@PostAuthorize:
????????@PostAuthorize在方法執(zhí)行后再進行權限驗證,適合驗證帶有返回值的權限, Spring EL 提供返回對象能夠在表達式語言中獲取到返回對象的 returnObject。
/*** 根據(jù)ID查詢用戶** @return*/
@GetMapping("/{id}")
@ResponseBody
@PostAuthorize("returnObject.username == authentication.principal.username ") //判斷查詢用戶信息是否是當前登錄用戶信息.否則沒有權限
public User getById(@PathVariable Integer id) {User user = userService.getById(id);return user;
}
@PreFilter:
????????可以用來對集合類型的參數(shù)進行過濾, 將不符合條件的元素剔除集合。
/*** 商品刪除-多選刪除** @return*/
@GetMapping("/delByIds")
@PreFilter(filterTarget = "ids", value = "filterObject%2==0") //剔除參數(shù)為基數(shù)的值
public String delByIds(@RequestParam(value = "id") List < Integer > ids) {for(Integer id: ids) {System.out.println(id);}return "redirect:/user/findAll";
}
@PostFilter:
????????可以用來對集合類型的返回值進行過濾, 將不符合條件的元素剔除集合。
/*** 查詢所有用戶-返回json數(shù)據(jù)** @return*/
@RequestMapping("/findAllTOJson")
@ResponseBody
@PostFilter("filterObject.id%2==0") //剔除返回值ID為偶數(shù)的值
public List < User > findAllTOJson() {List < User > userList = userService.list();return userList;
}
3.3、 基于數(shù)據(jù)庫的RBAC數(shù)據(jù)模型的權限控制
????????我們開發(fā)一個系統(tǒng),必然面臨權限控制的問題,不同的用戶具有不同的訪問、操作、數(shù)據(jù)權限。形成理論的權限控制模型有:自主訪問控制(DAC: Discretionary Access Control)、強制訪問控制(MAC: Mandatory Access Control)、基于屬性的權限驗證(ABAC: Attribute-Based Access?Control)等。最常被開發(fā)者使用也是相對易用、通用的就是RBAC權限模型(Role-Based Access?Control)。
3.3.1、RBAC 權限模型簡介
????????RBAC權限模型(Role-Based Access Control)即:基于角色的權限控制。模型中有幾個關鍵的術語:
- 用戶:系統(tǒng)接口及訪問的操作者
- 權限:能夠訪問某接口或者做某操作的授權資格
- 角色:具有一類相同操作權限的總稱
????????RBAC權限模型核心授權邏輯如下:
- 某用戶是什么角色?
- 某角色具有什么權限?
- 通過角色對應的權限推導出用戶的權限
3.3.2、RBAC 的演化
1、用戶與權限直接關聯(lián)
????????想到權限控制,人們最先想到的一定是用戶與權限直接關聯(lián)的模式,簡單地說就是:某個用戶具有某些權限。如圖:
- 張三具有所有權限他可能是一個超級管理員.
- 李四,王五 具有添加商品和審核商品的權限有可能是一個普通業(yè)務員
????????這種模型能夠清晰的表達用戶與權限之間的關系,足夠簡單。但同時也存在問題:
- 現(xiàn)在用戶是張三、李四,王五以后隨著人員增加,每一個用戶都需要重新授權
- 操作人員的他的權限發(fā)生變更后,需要對每個一個用戶重新授予新的權限
2、用戶與角色關聯(lián)
????????這樣只需要維護角色和權限之間的關系就可以了. 如果業(yè)務員的權限發(fā)生變更, 只需要變動業(yè)務員角色和權限之前的關系進行維護就可以了. 用戶和權限就分離開來了. 如下圖:
3.3.3、基于 RBAC 設計權限表結構
- 一個用戶有一個或多個角色
- 一個角色包含多個用戶
- 一個角色有多種權限
- 一個權限屬于多個角色
3.3.4、基于SpringSecurity 實現(xiàn)RBAC權限管理
1、動態(tài)查詢數(shù)據(jù)庫中用戶對應的權限
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.lagou.domain.Permission;
import org.apache.ibatis.annotations.Select;
import java.util.List;
public interface PermissionMapper extends BaseMapper < Permission > {/*** 根據(jù)用戶ID查詢權限** @param id* @return*/@Select("SELECT p.* FROM t_permission p,t_role_permission rp,t_roler, t_user_role ur, t_user u " + "WHERE p.id = rp.PID AND rp.RID = r.id AND r.id = ur.RID AND ur.UID = u.id AND u.id = #{id}")List < Permission > findByUserId(Integer id);
}
2、給登錄用戶授權
// 先聲明一個權限集合, 因為構造方法里面不能傳入null
Collection < GrantedAuthority > authorities = new ArrayList < > ();
// 查詢用戶對應所有權限
List < Permission > permissions = permissionService.findByUserId(user.getId());
for(Permission permission: permissions) {// 授權authorities.add(new SimpleGrantedAuthority(permission.getPermissionTag()));
}
3、設置訪問權限
// 查詢數(shù)據(jù)庫所有權限列表
List < Permission > permissions = permissionService.list();
for(Permission permission: permissions) {//添加請求權限http.authorizeRequests().antMatchers(permission.getPermissionUrl()).hasAuthority(permission.getPermissionTag());
}
3.4、基于頁面標簽的權限控制
????????在jsp頁面或者thymeleaf模板頁面中我們可以使用spring security提供的權限標簽來進行權限控制.要想使用thymeleaf為SpringSecurity提供的標簽屬性,首先需要引入thymeleaf-extras-springsecurity依賴支持。
引入依賴:
<!--添加thymeleaf為SpringSecurity提供的標簽 依賴 -->
<dependency><groupId>org.thymeleaf.extras</groupId><artifactId>thymeleaf-extras-springsecurity5</artifactId><version>3.0.4.RELEASE</version>
</dependency>
聲明使用:
!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" xmlns:sec="http://www.thymeleaf.org/extras/spring-security">
3.4.1、常用SpringSecurity 標簽屬性
- sec:authorize="isAuthenticated()"
????????判斷用戶是否已經(jīng)登陸認證,引號內(nèi)的參數(shù)必須是isAuthenticated()。
- sec:authentication=“name”
????????獲得當前用戶的用戶名,引號內(nèi)的參數(shù)必須是name。
- sec:authorize=“hasRole(‘role’)”
????????判斷當前用戶是否擁有指定的權限。引號內(nèi)的參數(shù)為權限的名稱
3.4.2、標簽使用
<div class="leftnav"><div class="leftnav-title"><div sec:authorize="isAuthenticated()"><span sec:authentication="name"></span><img src="images/y.jpg" class="radius-circle rotate-hover" height="50"alt="" /></div></div><div sec:authorize="hasAuthority('user:findAll')"><h2><span class="icon-user"></span>系統(tǒng)管理</h2><ul style="display:block"><li><a href="/user/findAll" target="right"><span class="icon-caretright"></ span>用戶管理</a></li><li><a href="javascript:void(0)" onclick="toCors()" target="right"><span class="icon-caret-right"></span>跨域測試</a></li></ul></div><div sec:authorize="hasAuthority('product:findAll')"><h2><span class="icon-pencil-square-o"></span>數(shù)據(jù)管理</h2><ul><li><a href="/product/findAll" target="right"><span class="iconcaret-right"></span>商品管理</a></li></ul></div>
</div>
4、源碼剖析
4.1、過濾器鏈加載源碼
4.1.1、過濾器鏈加載流程分析
4.1.2、源碼分析
1、配置信息
# 安全過濾器自動配置
org.springframework.boot.autoconfigure.security.servlet.SecurityFilterAutoConfiguration
2、SecurityFilterAutoConfiguration類
@Configuration(proxyBeanMethods = false)
@ConditionalOnWebApplication(type = Type.SERVLET)
@EnableConfigurationProperties(SecurityProperties.class) // Security配置類
@ConditionalOnClass({AbstractSecurityWebApplicationInitializer.class,SessionCreationPolicy.class
})
@AutoConfigureAfter(SecurityAutoConfiguration.class) // 這個類加載完后去加載SecurityAutoConfiguration配置
public class SecurityFilterAutoConfiguration {//.....
}
3、SecurityAutoConfiguration類
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(DefaultAuthenticationEventPublisher.class)
@EnableConfigurationProperties(SecurityProperties.class)
@Import({SpringBootWebSecurityConfiguration.class,//web安全啟用配置WebSecurityEnablerConfiguration.class, SecurityDataConfiguration.class
})
public class SecurityAutoConfiguration {//.....
}
4、WebSecurityEnablerConfiguration類
@Configuration(proxyBeanMethods = false)
@ConditionalOnBean(WebSecurityConfigurerAdapter.class)
@ConditionalOnMissingBean(name = BeanIds.SPRING_SECURITY_FILTER_CHAIN)
@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET)
@EnableWebSecurity
public class WebSecurityEnablerConfiguration {}
@Retention(value = java.lang.annotation.RetentionPolicy.RUNTIME)
@Target(value = {java.lang.annotation.ElementType.TYPE
})
@Documented
@Import({WebSecurityConfiguration.class,SpringWebMvcImportSelector.class,OAuth2ImportSelector.class
})
@EnableGlobalAuthentication
@Configuration
public @interface EnableWebSecurity {/*** Controls debugging support for Spring Security. Default is false.* @return if true, enables debug support with Spring Security*/boolean debug() default false;
}
????????@EnableWebSecurity注解有兩個作用:
- 加載了WebSecurityConfiguration配置類, 配置安全認證策略
- 加載了AuthenticationConfiguration, 配置了認證信息
5、WebSecurityConfiguration類
// springSecurity過濾器鏈聲明
@Bean(name = AbstractSecurityWebApplicationInitializer.DEFAULT_FILTER_NAME)
public Filter springSecurityFilterChain() throws Exception {boolean hasConfigurers = webSecurityConfigurers != null && !webSecurityConfigurers.isEmpty();if(!hasConfigurers) {WebSecurityConfigurerAdapter adapter = objectObjectPostProcessor.postProcess(new WebSecurityConfigurerAdapter() {});webSecurity.apply(adapter);}return webSecurity.build(); //構建filter
}
4.2、認證流程源碼
4.2.1、認證流程分析
????????在整個過濾器鏈中, UsernamePasswordAuthenticationFilter是來處理整個用戶認證的流程的, 所以下面我們主要針對用戶認證來看下源碼是如何實現(xiàn)的?
4.2.2、源碼跟蹤
UsernamePasswordAuthenticationFilter:
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {//1.檢查是否是post請求if(postOnly && !request.getMethod().equals("POST")) {throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());}//2.獲取用戶名和密碼String username = obtainUsername(request);String password = obtainPassword(request);if(username == null) {username = "";}if(password == null) {password = "";}username = username.trim();//3.創(chuàng)建AuthenticationToken,此時是未認證的狀態(tài)UsernamePasswordAuthenticationToken authRequest = newUsernamePasswordAuthenticationToken(username, password);// Allow subclasses to set the "details" propertysetDetails(request, authRequest);//4.調(diào)用AuthenticationManager進行認證.return this.getAuthenticationManager().authenticate(authRequest);
}
UsernamePasswordAuthenticationToken:
public UsernamePasswordAuthenticationToken(Object principal, Object credentials) {super(null);this.principal = principal; //設置用戶名this.credentials = credentials; //設置密碼setAuthenticated(false); //設置認證狀態(tài)為-未認證
}
AuthenticationManager-->ProviderManager-->AbstractUserDetailsAuthenticationProvider:
public Authentication authenticate(Authentication authentication)
throws AuthenticationException {Assert.isInstanceOf(UsernamePasswordAuthenticationToken.class, authentication,() - > messages.getMessage("AbstractUserDetailsAuthenticationProvider.onlySupports", "Only UsernamePasswordAuthenticationToken is supported "));// 1.獲取用戶名String username = (authentication.getPrincipal() == null) ? "NONE_PROVIDED" : authentication.getName();// 2.嘗試從緩存中獲取boolean cacheWasUsed = true;UserDetails user = this.userCache.getUserFromCache(username);if(user == null) {cacheWasUsed = false;try {//3.檢索Useruser = retrieveUser(username,(UsernamePasswordAuthenticationToken) authentication);}//.....}try {//4. 認證前檢查user狀態(tài)preAuthenticationChecks.check(user);//5. 附加認證證檢查additionalAuthenticationChecks(user,(UsernamePasswordAuthenticationToken) authentication);}//.....//6. 認證后檢查user狀態(tài)postAuthenticationChecks.check(user);//.....// 7. 創(chuàng)建認證成功的UsernamePasswordAuthenticationToken并將認證狀態(tài)設置為truereturn createSuccessAuthentication(principalToReturn, authentication, user);
}
retrieveUser方法:
protected final UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication)
throws AuthenticationException {prepareTimingAttackProtection();try {//調(diào)用自定義UserDetailsService的loadUserByUsername的方法UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username);if(loadedUser == null) {throw new InternalAuthenticationServiceException("UserDetailsService returned null, which is an interface contract violation ");}return loadedUser;}//....
}
additionalAuthenticationChecks方法:
protected void additionalAuthenticationChecks(UserDetails userDetails, UsernamePasswordAuthenticationToken authentication)
throws AuthenticationException {//.....// 1.獲取前端密碼String presentedPassword = authentication.getCredentials().toString();// 2.與數(shù)據(jù)庫中的密碼進行比對if(!passwordEncoder.matches(presentedPassword, userDetails.getPassword())) {logger.debug("Authentication failed: password does not match stored value ");throw new BadCredentialsException(messages.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));}
}
AbstractAuthenticationProcessingFilter--doFilter方法:
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
throws IOException, ServletException {//.....Authentication authResult;try {//1.調(diào)用子類方法authResult = attemptAuthentication(request, response);//...//2.session策略驗證sessionStrategy.onAuthentication(authResult, request, response);}//....// 3.成功身份驗證successfulAuthentication(request, response, chain, authResult);
}
successfulAuthentication方法:
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult)
throws IOException, ServletException {//....// 1.將認證的用戶放入SecurityContext中SecurityContextHolder.getContext().setAuthentication(authResult);// 2.檢查是不是記住我rememberMeServices.loginSuccess(request, response, authResult);...// 3.調(diào)用自定義MyAuthenticationService的onAuthenticationSuccess方法successHandler.onAuthenticationSuccess(request, response, authResult);
}
4.3、記住我流程源碼
4.3.1、流程分析
4.3.2、源碼跟蹤
AbstractAuthenticationProcessingFilter--successfulAuthentication方法:
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult)
throws IOException, ServletException {//....// 1.將認證的用戶放入SecurityContext中SecurityContextHolder.getContext().setAuthentication(authResult);// 2.檢查是不是記住我rememberMeServices.loginSuccess(request, response, authResult);//...// 3.調(diào)用自定義MyAuthenticationService的onAuthenticationSuccess方法successHandler.onAuthenticationSuccess(request, response, authResult);
}
loginSuccess方法-->onLoginSuccess:
protected void onLoginSuccess(HttpServletRequest request, HttpServletResponse response, Authentication successfulAuthentication) {// 1.獲取用戶名String username = successfulAuthentication.getName();// 2.創(chuàng)建persistentTokenPersistentRememberMeToken persistentToken = new PersistentRememberMeToken(username, generateSeriesData(), generateTokenData(), new Date());try {// 3. 插入數(shù)據(jù)庫tokenRepository.createNewToken(persistentToken);// 4. 寫入瀏覽器cookieaddCookie(persistentToken, request, response);}catch (Exception e) {logger.error("Failed to save persistent token ", e);}
}
RememberMeAuthenticationFilter:
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
throws IOException, ServletException {HttpServletRequest request = (HttpServletRequest) req;HttpServletResponse response = (HttpServletResponse) res;if(SecurityContextHolder.getContext().getAuthentication() == null) {// 1. 檢查是否是記住我登錄. 如果是完成自動登錄Authentication rememberMeAuth = rememberMeServices.autoLogin(request, response);if(rememberMeAuth != null) {try {// 2.調(diào)用authenticationManager再次認證rememberMeAuth = authenticationManager.authenticate(rememberMeAuth);// 3.將認證的用戶在重新放入SecurityContext中SecurityContextHolder.getContext().setAuthentication(rememberMeAuth);//......}//......}//.....// 4.調(diào)用下一個過濾器chain.doFilter(request, response);}
}
autoLogin方法:
public final Authentication autoLogin(HttpServletRequest request, HttpServletResponse response) {// 1.獲取rememberMeCookieString rememberMeCookie = extractRememberMeCookie(request);// 2.檢查是否存在if(rememberMeCookie == null) {return null;}//.....UserDetails user = null;try {// 3.解碼CookieString[] cookieTokens = decodeCookie(rememberMeCookie);// 4.根據(jù)cookie完成自動登錄user = processAutoLoginCookie(cookieTokens, request, response);// 5.檢查user狀態(tài)userDetailsChecker.check(user);logger.debug("Remember-me cookie accepted");// 6.創(chuàng)建認證成功的RememberMeAuthenticationToken并將認證狀態(tài)設置為truereturn createSuccessfulAuthentication(request, user);}//.....return null;
}
processAutoLoginCookie方法:
protected UserDetails processAutoLoginCookie(String[] cookieTokens, HttpServletRequest request, HttpServletResponse response) {//....// 1.獲取系列碼和tokenfinal String presentedSeries = cookieTokens[0];final String presentedToken = cookieTokens[1];// 2.根據(jù)token去數(shù)據(jù)庫中查詢PersistentRememberMeToken token = tokenRepository.getTokenForSeries(presentedSeries);.......// 3.在創(chuàng)建一個新的tokenPersistentRememberMeToken newToken = new PersistentRememberMeToken(token.getUsername(), token.getSeries(), generateTokenData(), new Date());try {// 4.修改數(shù)據(jù)庫token信息tokenRepository.updateToken(newToken.getSeries(), newToken.getTokenValue(), newToken.getDate());// 5.寫入瀏覽器addCookie(newToken, request, response);}// 6.根據(jù)用戶名調(diào)用UserDetailsService查詢UserDetailreturn getUserDetailsService().loadUserByUsername(token.getUsername());
}
4.4、CSRF流程源碼
4.4.1、流程分析
4.4.2、源碼跟蹤
CsrfFilter:
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {request.setAttribute(HttpServletResponse.class.getName(), response);// 1.取出tokenCsrfToken csrfToken = this.tokenRepository.loadToken(request);final boolean missingToken = csrfToken == null;if(missingToken) {// 2. 如果沒有token,就重新生成tokencsrfToken = this.tokenRepository.generateToken(request);this.tokenRepository.saveToken(csrfToken, request, response);}// 3. 將csrfToken值放入request域中request.setAttribute(CsrfToken.class.getName(), csrfToken);request.setAttribute(csrfToken.getParameterName(), csrfToken);// 4. 匹配請求是否為post請求,不是則放行if(!this.requireCsrfProtectionMatcher.matches(request)) {filterChain.doFilter(request, response);return;}String actualToken = request.getHeader(csrfToken.getHeaderName());if(actualToken == null) {// 5.從request請求參數(shù)中取出csrfTokenactualToken = request.getParameter(csrfToken.getParameterName());}// 6.匹配兩個token是否相等.if(!csrfToken.getToken().equals(actualToken)) {if(this.logger.isDebugEnabled()) {this.logger.debug("Invalid CSRF token found for " + UrlUtils.buildFullRequestUrl(request));}if(missingToken) {this.accessDeniedHandler.handle(request, response, new MissingCsrfTokenException(actualToken));}else {this.accessDeniedHandler.handle(request, response, new InvalidCsrfTokenException(csrfToken, actualToken));}return;}// 7. 如果相等則放行filterChain.doFilter(request, response);
}
4.5、授權流程源碼
4.5.1、流程分析
- AffirmativeBased(基于肯定)的邏輯是: 一票通過權
- ConsensusBased(基于共識)的邏輯是: 贊成票多于反對票則表示通過,反對票多于贊成票則將拋出?AccessDeniedException
- UnanimousBased(基于一致)的邏輯: 一票否決權
4.5.2、源碼跟蹤
FilterSecurityInterceptor:
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {FilterInvocation fi = new FilterInvocation(request, response, chain);// 調(diào)用invoke(fi);
}public void invoke(FilterInvocation fi) throws IOException, ServletException {if((fi.getRequest() != null).....}else {//...//前置調(diào)用InterceptorStatusToken token = super.beforeInvocation(fi);//....// 后置調(diào)用super.afterInvocation(token, null);}
}
AbstractSecurityInterceptor的beforeInvocation方法:
protected InterceptorStatusToken beforeInvocation(Object object) {// 1. 獲取security的系統(tǒng)配置權限Collection < ConfigAttribute > attributes = this.obtainSecurityMetadataSource().getAttributes(object);//.....// 2. 獲取用戶認證的信息Authentication authenticated = authenticateIfRequired();// Attempt authorizationtry {// 3.調(diào)用決策管理器this.accessDecisionManager.decide(authenticated, object, attributes);}catch (AccessDeniedException accessDeniedException) {publishEvent(new AuthorizationFailureEvent(object, attributes, authenticated, accessDeniedException));// 4.無權限則拋出異常讓ExceptionTranslationFilter捕獲throw accessDeniedException;}//....
}
AffirmativeBased的decide方法:
public void decide(Authentication authentication, Object object, Collection < ConfigAttribute > configAttributes) throws
AccessDeniedException {int deny = 0;for(AccessDecisionVoter voter: getDecisionVoters()) {int result = voter.vote(authentication, object, configAttributes);if(logger.isDebugEnabled()) {logger.debug("Voter: " + voter + ", returned: " + result);}switch(result) {//一票通過,只要有一個投票器通過就允許訪問case AccessDecisionVoter.ACCESS_GRANTED:return;case AccessDecisionVoter.ACCESS_DENIED:deny++;break;default:break;}}if(deny > 0) {throw new AccessDeniedException(messages.getMessage("AbstractAccessDecisionManager.accessDenied", "Access isdenied"));}//...
}
ExceptionTranslationFilter:
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
throws IOException, ServletException {HttpServletRequest request = (HttpServletRequest) req;HttpServletResponse response = (HttpServletResponse) res;try {// 1.調(diào)用下一個過濾器即FilterSecurityInterceptorchain.doFilter(request, response);logger.debug("Chain processed normally");}catch (IOException ex) {throw ex;}catch (Exception ex) {// 2.捕獲FilterSecurityInterceptor并判斷異常類型Throwable[] causeChain = throwableAnalyzer.determineCauseChain(ex);RuntimeException ase = (AuthenticationException)throwableAnalyzer.getFirstThrowableOfType(AuthenticationException.class, causeChain);if(ase == null) {ase = (AccessDeniedException)throwableAnalyzer.getFirstThrowableOfType(AccessDeniedException.class, causeChain);}if(ase != null) {if(response.isCommitted()) {throw new ServletException("Unable to handle the Spring Security Exception because the response is already committed. ", ex);}// 3.如果是AccessDeniedException異常則處理Spring Security異常handleSpringSecurityException(request, response, chain, ase);}//.....}}
handleSpringSecurityException方法:
private void handleSpringSecurityException(HttpServletRequest request, HttpServletResponse response, FilterChain chain, RuntimeException exception)
throws IOException, ServletException {if(exception instanceof AuthenticationException) {//...}else if(exception instanceof AccessDeniedException) {Authentication authentication = SecurityContextHolder.getContext().getAuthentication();if(authenticationTrustResolver.isAnonymous(authentication) || authenticationTrustResolver.isRememberMe(authentication)) {//.....}else {logger.debug("Access is denied (user is not anonymous); delegating to AccessDeniedHandler ", exception);//如果是AccessDeniedException異常則調(diào)用AccessDeniedHandler的handle方法accessDeniedHandler.handle(request, response,(AccessDeniedException) exception);}}
}