簡介
SpringSecurity原理(一)——初探 SpringSecurity原理(二)——認證 SpringSecurity原理(三)——授權 SpringSecurity原理(四)——過濾器 SpringSecurity原理(五)——擴展與配置
我們前面已經基本介紹了Spring Security中最重要和常用的組件與功能。
這篇文章,我們就來了解一下怎樣做一些自定義的擴展與配置。
自定義擴展
自定義Filter
自定義Filter應該是最常用的需求了,例如,爲了攔截大多數的暴力登錄,我們一般會在登錄的時候給一個驗證碼,但是UsernamePasswordAuthenticationFilter沒有提供驗證碼的校驗,所以我們就可以自定義一個Filter來處理驗證碼。
又如,對於前後端分離項目,我們更多使用Token,而不是Session,也可以通過Filter來處理Token的校驗,續期的問題。
自定義Filter的方式有很多:
- 直接實現Filter
- 繼承GenericFilterBean
- 繼承OncePerRequestFilter重寫doFilterInternal
- 繼承BasicAuthenticationFilter重寫doFilterInternal
- 繼承AbstractAuthenticationProcessingFilter重寫attemptAuthentication
- 繼承UsernamePasswordAuthenticationFilter重寫attemptAuthentication
- ……
後3個都是認證相關的Filter。
因爲涉及到轉發重定義等問題,一次請求Filter可能不止一次被調用,OncePerRequestFilter就是爲了解決這個問題,它保證一次請求繼承它的Filter只會被調用一次。
BasicAuthenticationFilter本身繼承了OncePerRequestFilter,所以不用自己處理因爲轉發等引起的Filter多次調用問題。
AbstractAuthenticationProcessingFilter添加了認證失敗,認證成功等處理,但是它沒有處理一次請求可能多次調用的問題。
對於表單認證,想偷懶,可以自己繼承UsernamePasswordAuthenticationFilter,例如,繼承UsernamePasswordAuthenticationFilter,先處理驗證碼問題,如果校驗成功,再調用UsernamePasswordAuthenticationFilter的attemptAuthentication方法。
反正自定義Filter非常靈活,根據自己的喜好選擇。
自定義了Filter如何配置呢?
最簡單的方式,自定義配置類重寫WebSecurityConfigurerAdapter的configure方法:
@Override
protected void configure(HttpSecurity http) {
http.addFilter(zzzFilter)
.addFilterAfter(aaaFilter)
.addFilterBefore(yyyFilter, UsernamePasswordAuthenticationFilter.class)
.addFilterAt(xxxFilter,UsernamePasswordAuthenticationFilter.class);
}
- addFilter是添加到最後,但並不是最終的最後,因爲後面的流程還會添加其他Filter
- addFilterAfter,添加在指定Filter之後
- addFilterBefore,添加在指定Filter之前
- addFilterAt,添加在指定Filter之前,不會覆蓋和刪除指定的Filter,感覺和addFilterBefore差不多
當然,也可以通過SecurityConfigurerAdapter的方式:
public class JwtConfigurer extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> {
@Override
public void configure(HttpSecurity http) {
JwtAuthenticationFilter filter = new JwtAuthenticationFilter();
http.addFilterBefore(filter, UsernamePasswordAuthenticationFilter.class);
}
}
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests((requests) -> requests.anyRequest().authenticated());
http.formLogin();
http.httpBasic();
http.apply(new JwtConfigurer());
}
}
自定義登出成功處理器
實現LogoutSuccessHandler接口,一般返回json數據,以便於前端給出提示信息。
public class JwtLogoutSuccessHandler implements LogoutSuccessHandler {
@Override
public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
if (authentication != null) {
new SecurityContextLogoutHandler().logout(request, response, authentication);
}
response.setContentType("application/json;charset=UTF-8");
ServletOutputStream outputStream = response.getOutputStream();
outputStream.write("jwt loginout success").getBytes("UTF-8"));
outputStream.flush();
outputStream.close();
}
}
配置方式:
protected void configure(HttpSecurity http){
http..logout()
.logoutSuccessHandler(new JwtLogoutSuccessHandler());
}
認證失敗處理器
實現AuthenticationFailureHandler接口,一般會返回json數據,然後前端根據返回數據,自己決定提示信息和跳轉。
public class LoginFailureHandler implements AuthenticationFailureHandler {
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
response.setContentType("application/json;charset=UTF-8");
ServletOutputStream outputStream = response.getOutputStream();
outputStream.write(JSONUtil.toJsonStr("登錄失敗").getBytes("UTF-8"));
outputStream.flush();
outputStream.close();
}
}
AuthenticationSuccessHandler
實現AuthenticationSuccessHandler接口,如果有生成token的邏輯可以放在這裏面。
public class LoginSuccessHandler implements AuthenticationSuccessHandler {
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
response.setContentType("application/json;charset=UTF-8");
ServletOutputStream outputStream = response.getOutputStream();
// 生成保存jwt等邏輯可以放這裏
outputStream.write(JSONUtil.toJsonStr("登錄成功").getBytes("UTF-8"));
outputStream.flush();
outputStream.close();
}
}
當然,也可以通過繼承SimpleUrlAuthenticationSuccessHandler的方式。
配置也是老方式:
@Override
protected void configure(HttpSecurity http) throws Exception {
http.formLogin()
.successHandler(loginSuccessHandler)
.failureHandler(loginFailureHandler)
}
認證異常跳轉入口
實現AuthenticationEntryPoint接口,這個接口在ExceptionTranslationFilter這個Filter截取到認證異常之後被調用,一般就是跳轉登錄頁,可以參考:LoginUrlAuthenticationEntryPoint
public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
response.setContentType("application/json;charset=UTF-8");
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
ServletOutputStream outputStream = response.getOutputStream();
outputStream.write(JSONUtil.toJsonStr("用戶名或者密碼錯誤").getBytes("UTF-8"));
outputStream.flush();
outputStream.close();
}
}
授權異常處理器
實現AccessDeniedHandler接口,這個接口在ExceptionTranslationFilter截獲到的授權異常之後被調用。
public class JwtAccessDeniedHandler implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
response.setContentType("application/json;charset=UTF-8");
response.setStatus(HttpServletResponse.SC_FORBIDDEN);
ServletOutputStream outputStream = response.getOutputStream();
outputStream.write(JSONUtil.toJsonStr("您沒有操作權限,請聯繫管理員").getBytes("UTF-8"));
outputStream.flush();
outputStream.close();
}
}
自定義認證憑證
可以實現Authentication,也可以繼承AbstractAuthenticationToken。一般不需要,除非要自定義認證器。
import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;
import java.util.Collection;
public class JwtAuthenticationToken extends AbstractAuthenticationToken {
public JwtAuthenticationToken(Collection<? extends GrantedAuthority> authorities) {
super(authorities);
}
@Override
public Object getCredentials() {
return null;
}
@Override
public Object getPrincipal() {
return null;
}
}
自定義認證器
實現AuthenticationProvider接口。
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
public class JwtAuthenticationProvider implements AuthenticationProvider {
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
return null;//認證流程
}
@Override
public boolean supports(Class<?> authentication) {//支持校驗哪種認證憑證
return authentication.isAssignableFrom(JwtAuthenticationToken.class);
}
}
自定義投票者
可以實現AccessDecisionVoter接口,也可以直接繼承WebExpressionVoter之類的,看具體需求,一般不需要,除非要自己設計新的授權體系。
public class MyExpressionVoter extends WebExpressionVoter {
@Override
public int vote(Authentication authentication, FilterInvocation fi, Collection<ConfigAttribute> attributes) {
return 1 ;//-1反對,0棄權,1同意
}
}
配置
配置WebSecurity
一般配置WebSecurity,主要是爲了忽略靜態資源校驗。
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
public void configure(WebSecurity web) {
web.ignoring().antMatchers(
"/**/*.html",
"/public/**/*.js",
"/public/**/*.css",
"/public/**/*.png",
"/**/*.gif", "/**/*.png", "/**/*.jpg", "/**/*.ico");
}
}
匹配規則使用的是:AntPathRequestMatcher
配置HttpSecurity
HttpSecurity可以配置的東西太多了,下面是一個示例,可以參考,注意很多是重複不必要的配置,只是爲了展示可以配置的內容。
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
public AccessDecisionManager accessDecisionManager(){
List<AccessDecisionVoter<? extends Object>> decisionVoters
= Arrays.asList(
new WebExpressionVoter(),
new RoleVoter(),
new AuthenticatedVoter());
return new ConsensusBased(decisionVoters);
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.
// 配置登錄頁,登錄用戶名密碼參數
formLogin().loginPage("/login")
.passwordParameter("username").passwordParameter("password")
// 配置登錄接口,defaultSuccessUrl配置登錄成功跳轉,會跳轉到來的路徑,而successForwardUrl會跳轉固定頁面
.loginProcessingUrl("/do-login").defaultSuccessUrl("/loginsucess")
// 登錄失敗處理
.failureForwardUrl("/fail").failureUrl("/failure").failureHandler(loginAuthenticationFailureHandler)
.permitAll()
// 配置特定url權限
.and().authorizeRequests().antMatchers("/admin").hasRole("admin")
// 配置鑑權管理器
.anyRequest().authenticated().accessDecisionManager(accessDecisionManager())
// 配置登出,登錄url,登出成功處理器
.and().logout().logoutUrl("/logout").logoutSuccessHandler(new JwtLogoutSuccessHandler())
// 關閉csrf
.and().csrf().disable();
// 配置自定義過濾器
http.addFilterAt(jwtAuthenticationFilter(),UsernamePasswordAuthenticationFilter.class);
// 配置鑑權失敗的處理器
http.exceptionHandling().accessDeniedHandler(new MyAccessDeniedHandler());
}
}
幾個重要的類與接口
SecurityBuilder
public interface SecurityBuilder<O> {
O build() throws Exception;
}
就是構建一個特殊對象O的抽象,例如Filter。
WebSecurity就是一個爲了創建Filter對象而設計。
HttpSecurity也是一個SecurityBuilder,不過它是爲了創建DefaultSecurityFilterChain。
SecurityConfigurer
public interface SecurityConfigurer<O, B extends SecurityBuilder<O>> {
void init(B builder) throws Exception;
void configure(B builder) throws Exception;
}
SecurityConfigurer目的主要有兩個:
- init,初始化builder,例如給一些filter設置參數
- configure,配置builder,例如創建添加新的filter
SecurityConfigurerAdapter
SecurityConfigurer的適配器,提供了init,configure的空實現,並且添加了一個CompositeObjectPostProcessor後置處理器,主要是用來處理Filter。
AbstractHttpConfigurer
最主要添加了一個disable方法,基本上很多Filter的configure類都繼承了它。
WebSecurityConfigurer
public interface WebSecurityConfigurer<T extends SecurityBuilder<Filter>> extends SecurityConfigurer<Filter, T> {
}
WebSecurityConfigurer主要是實例化了類型參數,告訴大家,我就是配置生產Filter的的SecurityBuilder。
WebSecurityConfigurerAdapter
public abstract class WebSecurityConfigurerAdapter implements WebSecurityConfigurer<WebSecurity> {
}
WebSecurityConfigurerAdapter更進一步告訴大家,我的SecurityBuilder就是WebSecurity,生產的就是Filter。
當然WebSecurityConfigurerAdapter沒有開空頭支票,還提供了很多功能,我們自己要對SpringSecurity進行配置的話,基本都是繼承WebSecurityConfigurerAdapter。
WebSecurity
WebSecurity是一個SecurityBuilder<Filter>,所以它的主要職責之一就是創建Filter,重點關注它的build方法,是繼承了AbstractSecurityBuilder的build,具體邏輯在AbstractConfiguredSecurityBuilder的doBuild方法中。
@Override
protected final O doBuild() throws Exception {
synchronized (this.configurers) {
this.buildState = BuildState.INITIALIZING;
beforeInit();
init();
this.buildState = BuildState.CONFIGURING;
beforeConfigure();
configure();
this.buildState = BuildState.BUILDING;
O result = performBuild();
this.buildState = BuildState.BUILT;
return result;
}
}
很標準的模板方法模式。
正真執行構建的邏輯在performBuild方法中,這個方法在WebSecurity創建了FilterChainProxy,在HttpSecurity中創建了DefaultSecurityFilterChain。
HttpSecurity
前面已經分析了,HttpSecurity的目的就一個創建DefaultSecurityFilterChain,注意它的performBuild方法。