Spring Security從過濾器到認證授權的源碼分析


Spring Security從過濾器到認證授權的源碼分析

​ Spring Security的實現包括認證(Authentication) 和 授權(Authorization)全部都是通過過濾器實現的,源碼分析最后都會追尋到源頭過濾器。

一、過濾器

1、WebSecurityConfigurerAdapter類

​ 一般情況下,實現認證我們需要繼承WebSecurityConfigurerAdapter類,例如,下面的SecurityConfig是一個前后端分離的SpringSecurity配置類:

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Resource
    private JwtAuthenticationEntryPoint authenticationErrorHandler;
    @Resource
    private JwtAccessDeniedHandler jwtAccessDeniedHandler;
    @Resource
    private ApplicationContext applicationContext;
    @Resource
    private JwtTokenFilter jwtTokenFilter;
    @Resource
    private CorsFilter corsFilter;

    @Bean
    GrantedAuthorityDefaults grantedAuthorityDefaults() {
        // 去除 ROLE_ 前綴
        return new GrantedAuthorityDefaults("");
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        // 密碼加密方式
        return new BCryptPasswordEncoder();
    }

    @Override
    protected void configure(HttpSecurity httpSecurity) throws Exception {
        // 搜尋匿名標記 url: @AnonymousAccess
        Map<RequestMappingInfo, HandlerMethod> handlerMethodMap = applicationContext.getBean(RequestMappingHandlerMapping.class).getHandlerMethods();
        Set<String> anonymousUrls = new HashSet<>();
        for (Map.Entry<RequestMappingInfo, HandlerMethod> infoEntry : handlerMethodMap.entrySet()) {
            HandlerMethod handlerMethod = infoEntry.getValue();
            AnonymousAccess anonymousAccess = handlerMethod.getMethodAnnotation(AnonymousAccess.class);
            if (null != anonymousAccess) {
                anonymousUrls.addAll(infoEntry.getKey().getPatternsCondition().getPatterns());
            }
        }
        httpSecurity
                // 禁用 CSRF
                .csrf().disable()
                // 授權異常
                .exceptionHandling()
                .authenticationEntryPoint(authenticationErrorHandler)
                .accessDeniedHandler(jwtAccessDeniedHandler)

                // 防止iframe 造成跨域
                .and()
                .headers()
                .frameOptions()
                .disable()

                // 不創建會話
                .and()
                .sessionManagement()
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS)

                .and()
                .authorizeRequests()
                // 靜態資源等等
                .antMatchers(
                        HttpMethod.GET,
                        "/*.html",
                        "/**/*.html",
                        "/**/*.css",
                        "/**/*.js",
                        "/**/*.map","/**/*.ttf","/**/*.woff","/**/*.woff2",
                        "/**/*.ico"
                ).permitAll()
                // swagger 文檔
                .antMatchers("/swagger-ui.html").permitAll()
                .antMatchers("/swagger-resources/**").permitAll()
                .antMatchers("/webjars/**").permitAll()
                .antMatchers("/*/api-docs").permitAll()
                // 文件
                .antMatchers("/avatar/**").permitAll()
                .antMatchers("/image/**").permitAll()
                // 阿里巴巴 druid
                .antMatchers("/druid/**").permitAll()
                // 報表
                .antMatchers("/ureport/**").permitAll()
                // 放行OPTIONS請求
                .antMatchers(HttpMethod.OPTIONS, "/**").permitAll()
                // 自定義匿名訪問所有url放行 : 允許匿名和帶權限以及登錄用戶訪問
                .antMatchers(anonymousUrls.toArray(new String[0])).permitAll()
                // 所有請求都需要認證
                .anyRequest().authenticated();

        //跨域
        httpSecurity.addFilterBefore(corsFilter,UsernamePasswordAuthenticationFilter.class);
        //用於攜帶token的認證
        httpSecurity.addFilterBefore(jwtTokenFilter,UsernamePasswordAuthenticationFilter.class);
    }
}

WebSecurityConfigurerAdapter類是Spring提供的安全配置類的基礎實現,通常情況我們都需要繼承它,當然也可以自己實現WebSecurityConfigurer接口來自定義一個實現。

WebSecurityConfigurerAdapter類是我們必須掌握的。

​ 在配置的最后加入的jwtTokenFilter是應用中自定義的過濾器,作用是JWT令牌認證。也就是我們可以將自定義的過濾器嵌入到SpringSecurity過濾鏈中,而不是加入到普通的Servlet Filter鏈中。這個時候我們自己的過濾建議實現OncePerRequestFilter接口,避免過濾器被普通過濾鏈加載而重復執行,或者不要將過濾器加入到IOC容器中,而是使用如下new的方式:

httpSecurity.addFilterBefore(new JwtTokenFilter(),UsernamePasswordAuthenticationFilter.class);

2、Spring Security的過濾鏈

SpringSecurity的過濾器長什么樣子?

它在Servlet過濾鏈中加入了代理,這個代理也是一個過濾器,但是其內部又實現一系列的過濾器。

原生過濾鏈:

0=characterEncodingFilter, filterClass=org.springframework.boot.web.servlet.filter.OrderedCharacterEncodingFilter
    
1=formContentFilter, filterClass=org.springframework.boot.web.servlet.filter.OrderedFormContentFilter
    
2=requestContextFilter, filterClass=org.springframework.boot.web.servlet.filter.OrderedRequestContextFilter
    
3=springSecurityFilterChain, filterClass=org.springframework.boot.web.servlet.DelegatingFilterProxyRegistrationBean$1
    
4=webStatFilter, filterClass=com.alibaba.druid.support.http.WebStatFilter
    
5=jwtTokenFilter, filterClass=com.wood.system.security.JwtTokenFilter
    
6=corsFilter, filterClass=org.springframework.web.filter.CorsFilter
    
7=Tomcat WebSocket (JSR356) Filter, filterClass=org.apache.tomcat.websocket.server.WsFilter

其中springSecurityFilterChain是額外加入的過濾器,內部定義了Spring Security實現的過濾鏈:

0 = {WebAsyncManagerIntegrationFilter@12079} 
1 = {SecurityContextPersistenceFilter@12080} 
2 = {HeaderWriterFilter@12081} 
3 = {LogoutFilter@12082} 
4 = {CorsFilter@12083} 
5 = {JwtTokenFilter@10680} 
6 = {RequestCacheAwareFilter@12084} 
7 = {SecurityContextHolderAwareRequestFilter@12085} 
8 = {AnonymousAuthenticationFilter@12086} 
9 = {SessionManagementFilter@12087} 
10 = {ExceptionTranslationFilter@12088} 
11 = {FilterSecurityInterceptor@12089} 

3、過濾鏈里Spring Security過濾器的順序

​ 過濾器的順序有嚴格的要求,它是通過FilterComparator來實現的,代碼片段如下:

FilterComparator() {
		Step order = new Step(INITIAL_ORDER, ORDER_STEP);
		put(ChannelProcessingFilter.class, order.next());
		put(ConcurrentSessionFilter.class, order.next());
		put(WebAsyncManagerIntegrationFilter.class, order.next());
		put(SecurityContextPersistenceFilter.class, order.next());
		put(HeaderWriterFilter.class, order.next());
		put(CorsFilter.class, order.next());
		put(CsrfFilter.class, order.next());
		put(LogoutFilter.class, order.next());
		filterToOrder.put(
			"org.springframework.security.oauth2.client.web.OAuth2AuthorizationRequestRedirectFilter",
				order.next());
		filterToOrder.put(
				"org.springframework.security.saml2.provider.service.servlet.filter.Saml2WebSsoAuthenticationRequestFilter",
				order.next());
		put(X509AuthenticationFilter.class, order.next());
		put(AbstractPreAuthenticatedProcessingFilter.class, order.next());
		filterToOrder.put("org.springframework.security.cas.web.CasAuthenticationFilter",
				order.next());
		filterToOrder.put(
			"org.springframework.security.oauth2.client.web.OAuth2LoginAuthenticationFilter",
				order.next());
		filterToOrder.put(
				"org.springframework.security.saml2.provider.service.servlet.filter.Saml2WebSsoAuthenticationFilter",
				order.next());
		put(UsernamePasswordAuthenticationFilter.class, order.next());
		put(ConcurrentSessionFilter.class, order.next());
		filterToOrder.put(
				"org.springframework.security.openid.OpenIDAuthenticationFilter", order.next());
		put(DefaultLoginPageGeneratingFilter.class, order.next());
		put(DefaultLogoutPageGeneratingFilter.class, order.next());
		put(ConcurrentSessionFilter.class, order.next());
		put(DigestAuthenticationFilter.class, order.next());
		filterToOrder.put(
				"org.springframework.security.oauth2.server.resource.web.BearerTokenAuthenticationFilter", order.next());
		put(BasicAuthenticationFilter.class, order.next());
		put(RequestCacheAwareFilter.class, order.next());
		put(SecurityContextHolderAwareRequestFilter.class, order.next());
		put(JaasApiIntegrationFilter.class, order.next());
		put(RememberMeAuthenticationFilter.class, order.next());
		put(AnonymousAuthenticationFilter.class, order.next());
		filterToOrder.put(
			"org.springframework.security.oauth2.client.web.OAuth2AuthorizationCodeGrantFilter",
				order.next());
		put(SessionManagementFilter.class, order.next());
		put(ExceptionTranslationFilter.class, order.next());
		put(FilterSecurityInterceptor.class, order.next());
		put(SwitchUserFilter.class, order.next());
	}

FilterComparator是在HttpSecurity被初始化,並應用到各過濾器里。

4、SpringSecurity相關的過濾器是什么時候加入到過濾鏈中的?

​ Spring Security相關的過濾器通過HttpSecurityAddFilter方法加入到過濾鏈中,加入的時機是各種Security相關的Configurer初始化的時候。

​ 例如:跨域的過濾器是在CorsConfigurer里加入的。類似的配置類還有:AbstractInterceptUrlConfigurer中方法攔截過濾器,ExceptionHandlingConfigurer中的異常處理過濾器等。官方的SpringSecurity過濾器有幾個可以查看FilterComparator

有的特殊的過濾器WebAsyncManagerIntegrationFilter是在WebSecurityConfigurerAdapter里加入的。

​ 這些Configurer初始化是在HttpSecurity中開始的:

	public CsrfConfigurer<HttpSecurity> csrf() throws Exception {
		ApplicationContext context = getContext();
		return getOrApply(new CsrfConfigurer<>(context));
	}
	private <C extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity>> C getOrApply(
			C configurer) throws Exception {
		C existingConfig = (C) getConfigurer(configurer.getClass());
		if (existingConfig != null) {
			return existingConfig;
		}
		return apply(configurer);
	}

最終在AbstractConfiguredSecurityBuilder中完成配置調用:

	private void configure() throws Exception {
		Collection<SecurityConfigurer<O, B>> configurers = getConfigurers();

		for (SecurityConfigurer<O, B> configurer : configurers) {
			configurer.configure((B) this);
		}
	}
	private void init() throws Exception {
		Collection<SecurityConfigurer<O, B>> configurers = getConfigurers();

		for (SecurityConfigurer<O, B> configurer : configurers) {
			configurer.init((B) this);
		}

		for (SecurityConfigurer<O, B> configurer : configurersAddedInInitializing) {
			configurer.init((B) this);
		}
	}

二、過濾器的入口--自動配置

​ Spring Security過濾器的自動配置是所有基本配置的源頭,這些關聯配置包括:SecurityAutoConfigurationSecurityFilterAutoConfiguration,后者依賴前者。

1、SecurityAutoConfiguration

​ 作用主要完成Security相關的基礎配置導入,基本bean的生成注入。

@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(DefaultAuthenticationEventPublisher.class)
@EnableConfigurationProperties(SecurityProperties.class)
@Import({ SpringBootWebSecurityConfiguration.class, WebSecurityEnablerConfiguration.class,
		SecurityDataConfiguration.class })
public class SecurityAutoConfiguration {

	@Bean
	@ConditionalOnMissingBean(AuthenticationEventPublisher.class)
	public DefaultAuthenticationEventPublisher authenticationEventPublisher(ApplicationEventPublisher publisher) {
		return new DefaultAuthenticationEventPublisher(publisher);
	}

}

主要內容:

1)導入SecurityProperties類,此類中定義認證和授權相關過濾器的順序和缺省用戶和密碼等,該類是配置類,可以在application.yml通過spring.security進行配置。

2)注入SpringBootWebSecurityConfigurationWebSecurityEnablerConfigurationSecurityDataConfiguration三個配置。重點。

3)實例化DefaultAuthenticationEventPublisher,缺省認證事件發布器,這個暫時不是重點。

1.1、SpringBootWebSecurityConfiguration

@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(WebSecurityConfigurerAdapter.class)
@ConditionalOnMissingBean(WebSecurityConfigurerAdapter.class)
@ConditionalOnWebApplication(type = Type.SERVLET)
public class SpringBootWebSecurityConfiguration {

	@Configuration(proxyBeanMethods = false)
	@Order(SecurityProperties.BASIC_AUTH_ORDER)
	static class DefaultConfigurerAdapter extends WebSecurityConfigurerAdapter {

	}

}

主要內容:

1)確保WebSecurityConfigurerAdapter類存在。

2)WebSecurityConfigurerAdapter還沒有實例化注入容器。

3)必須是一個web應用,並且類型是Servlet

4)重點:如果滿足上述條件,注入WebSecurityConfigurerAdapter類的bean作為缺省安全認證配置,並且重新指定其生效順序為SecurityProperties.BASIC_AUTH_ORDER,這個順序值如下:

public static final int BASIC_AUTH_ORDER = Ordered.LOWEST_PRECEDENCE - 5;
int LOWEST_PRECEDENCE = Integer.MAX_VALUE;

如果沒有這個@Order(SecurityProperties.BASIC_AUTH_ORDER)指定,缺省的WebSecurityConfigurerAdapter順序為100。

注意:這里的@Order是配置類的加載順序,不是過濾器的加載順序,他們表現一樣,但是要實現的最終目的是不一樣的。配置類中,先加載的bean,如果是單例(大部分),那么可能后續相同的配置bean就無法加載,並且還有類似@ConditionalOnMissingBean這樣的注解輔助,所以結論如下:

Spring Security缺省以很低的優先級(比它更低的優先級只有5個空位)加載了WebSecurityConfigurerAdapter配置。本文開頭所說,一般我們需要繼承實現WebSecurityConfigurerAdapter配置,那么我們自己應用缺省的加載順序就是100,這樣我們應用配置就優先加載,Spring Security的DefaultConfigurerAdapter配置就無法加載。

1.2、WebSecurityEnablerConfiguration

@Configuration(proxyBeanMethods = false)
@ConditionalOnBean(WebSecurityConfigurerAdapter.class)
@ConditionalOnMissingBean(name = BeanIds.SPRING_SECURITY_FILTER_CHAIN)
@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET)
@EnableWebSecurity
public class WebSecurityEnablerConfiguration {

}

主要內容:

1)WebSecurityConfigurerAdapter類bean必須注入IOC。

2)springSecurityFilterChain過濾鏈的bean沒有創建注入IOC容器。這個過濾鏈就是Spring Security的過濾鏈。

3)重點:@EnableWebSecurity,加載默認Web自動安全配置。這個注解我們一般在繼承實現WebSecurityConfigurerAdapter類的時候都會加入,如果忘記加了,那么spring security初始化時就會自動加載缺省配置。

1.2.1 @EnableWebSecurity
@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;
}

主要內容:

1)加載WebSecurityConfiguration。如下精簡代碼:

@Configuration(proxyBeanMethods = false)
public class WebSecurityConfiguration implements ImportAware, BeanClassLoaderAware {
	private WebSecurity webSecurity;
	
	@Bean
	@DependsOn(AbstractSecurityWebApplicationInitializer.DEFAULT_FILTER_NAME)
	public SecurityExpressionHandler<FilterInvocation> webSecurityExpressionHandler() {
		return webSecurity.getExpressionHandler();
	}


	@Bean(name = AbstractSecurityWebApplicationInitializer.DEFAULT_FILTER_NAME)
	public Filter springSecurityFilterChain() throws Exception {
		...
		return webSecurity.build();
	}

	@Bean
	@DependsOn(AbstractSecurityWebApplicationInitializer.DEFAULT_FILTER_NAME)
	public WebInvocationPrivilegeEvaluator privilegeEvaluator() {
		return webSecurity.getPrivilegeEvaluator();
	}	
}

里面有重要的三個bean:

webSecurityExpressionHandler 安全表達式處理器

springSecurityFilterChain Security過濾鏈,也就是整個Spring Security核心的過濾鏈bean在這里創建。

WebInvocationPrivilegeEvaluator web方法調用評估器

2)加載SpringWebMvcImportSelector。作用是加載DispatcherServlet類WebMvcSecurityConfiguration類,這兩個類不屬於主線任務。

3)加載OAuth2ImportSelector。作用是加載oAuth2、WebFlux相關的類,先略過。

4)加載@EnableGlobalAuthentication。重點。

@Retention(value = java.lang.annotation.RetentionPolicy.RUNTIME)
@Target(value = { java.lang.annotation.ElementType.TYPE })
@Documented
@Import(AuthenticationConfiguration.class)
@Configuration
public @interface EnableGlobalAuthentication {
}

主要導入AuthenticationConfiguration類,這個配置類主要注入了AuthenticationManagerBuilder,通過它,我們就可以拿到AuthenticationManager,然后進行自動或者手動的認證。

1.3、SecurityDataConfiguration

這個是與Spring Data的安全集成,略過。

2、SecurityFilterAutoConfiguration

​ Spring Security過濾鏈的配置與注冊。

@Configuration(proxyBeanMethods = false)
@ConditionalOnWebApplication(type = Type.SERVLET)
@EnableConfigurationProperties(SecurityProperties.class)
@ConditionalOnClass({ AbstractSecurityWebApplicationInitializer.class, SessionCreationPolicy.class })
@AutoConfigureAfter(SecurityAutoConfiguration.class)
public class SecurityFilterAutoConfiguration {

	private static final String DEFAULT_FILTER_NAME = AbstractSecurityWebApplicationInitializer.DEFAULT_FILTER_NAME;

	@Bean
	@ConditionalOnBean(name = DEFAULT_FILTER_NAME)
	public DelegatingFilterProxyRegistrationBean securityFilterChainRegistration(
			SecurityProperties securityProperties) {
		DelegatingFilterProxyRegistrationBean registration = new DelegatingFilterProxyRegistrationBean(
				DEFAULT_FILTER_NAME);
		registration.setOrder(securityProperties.getFilter().getOrder());
		registration.setDispatcherTypes(getDispatcherTypes(securityProperties));
		return registration;
	}

    ... ...

}

主要內容:

1)導入SecurityProperties配置

2)確保在SecurityAutoConfiguration之后執行

3)注入DelegatingFilterProxyRegistrationBean,它就是springSecurityFilterChain過濾鏈的委托對象。在前面提到普通的Servlet filter過濾鏈中

3=springSecurityFilterChain, filterClass=org.springframework.boot.web.servlet.DelegatingFilterProxyRegistrationBean$1

springSecurityFilterChain是以代理方式實現,這是為了spring security內部需要走一遍自己的過濾器,在我之前的應用里是11個過濾器。

4)DelegatingFilterProxyRegistrationBean在生成的時候設置了過濾器的順序為-100。

public static final int DEFAULT_FILTER_ORDER = OrderedFilter.REQUEST_WRAPPER_FILTER_MAX_ORDER - 100;
int REQUEST_WRAPPER_FILTER_MAX_ORDER = 0;

三、認證(Authentication)

1、認證接口

​ 用於認證的主要接口是AuthenticationManager,一般我們可以使用AuthenticationManagerBuilder獲取它的默認實現,它只有一個方法:

public interface AuthenticationManager {
  Authentication authenticate(Authentication authentication)
    throws AuthenticationException;
}

一個 AuthenticationManagerauthenticate()方法中有三種情況:

  1. 返回 Authentication (authenticated=true),如果驗證輸入是合法的Principal)。
  2. 拋出AuthenticationException異常,如果輸入不合法。
  3. 如果無法判斷,則返回null

AuthenticationException是一個運行時異常,通常被應用程序以常規的方式的處理,這取決於應用目的和代碼風格。換句話說,代碼中一般不會捕捉和處理這個異常。比如,可以使得網頁顯示認證失敗,后端返回 401 HTTP 狀態碼,響應頭中的WWW-Authenticate 有無視情況而定。

AuthenticationManager最普遍的實現是ProviderManagerProviderManager將認證委托給一系列的AuthenticationProvider實例 。AuthenticationProviderAuthenticationManager 很類似,但是它有一個額外的方法允許查詢它支持的Authentication方式:

public interface AuthenticationProvider {
	Authentication authenticate(Authentication authentication)
			throws AuthenticationException;

	boolean supports(Class<?> authentication);
}

supports方法的Class authentication 參數其實是Class類型的。一個ProviderManager在一個應用中能支持多種不同的認證機制,通過將認證委托給一系列的AuthenticationProviderProviderManager沒有識別出的認證類型,將會被忽略。

每個ProviderManager可以有一個父類,如果所有AuthenticationProvider都返回null,那么就交給父類去認證。如果父類也不可用,則拋出AuthenticationException異常。

有時應用的資源會有邏輯分組(比如所有網站資源都匹配URL/api/**),並且每個組都有自己的AuthenticationManager,通常是一個ProviderManager,它們之間有共同的父類認證器。那么父類就是一種全局資源,充當所有認證器的 fallback。

2、自定義AuthenticationManager

Spring Security 提供了一些配置方式幫助你快速的配置通用的AuthenticationManager。最常見的是AuthenticationManagerBuilder,它可以使用內存方式(in-memory)、JDBC 或 LDAP、或自定義的UserDetailService來認證用戶。下面是設置全局認證器的例子:

@Configuration
public class ApplicationSecurity extends WebSecurityConfigurerAdapter {

   ... // web stuff here

  @Autowired
  public initialize(AuthenticationManagerBuilder builder, DataSource dataSource) {
    builder.jdbcAuthentication().dataSource(dataSource).withUser("dave")
      .password("secret").roles("USER");
  }

}

雖然這個例子僅僅設計一個 web 應用,但是AuthenticationManagerBuilder的用處大為廣闊(詳細情況請看[Web 安全](#Web 安全)是如何實現的)。請注意AuthenticationManagerBuilder是通過@AutoWired注入到被@Bean注解的一個方法中的,這使得它成為一個全局AuthenticationManager。相反的,如果我們這樣寫:

@Configuration
public class ApplicationSecurity extends WebSecurityConfigurerAdapter {

  @Autowired
  DataSource dataSource;

   ... // web stuff here

  @Override
  public configure(AuthenticationManagerBuilder builder) {
    builder.jdbcAuthentication().dataSource(dataSource).withUser("dave")
      .password("secret").roles("USER");
  }

}

重寫configure(AuthenticationManagerBuilder builder)方法,那么AuthenticationManagerBuilder僅會構造一個“本地”的AuthenticationManager,只是全局認證器的一個子實現。在 Spring Boot 應用中你可以使用@Autowired注入全局的AuthenticationManager,但是你不能注入“本地”的,除非你自己公開暴露它。

Spring Boot 提供默認的全局AuthenticationManager,除非你提供自己的全局AuthenticationManager。不用擔心,默認的已經足夠安全了,除非你真的需要一個自定義的全局AuthenticationManager。一般的,你只需只用“本地”的AuthenticationManagerBuilder來配置,而不需要擔心全局的。

3、認證與過濾器

​ 默認情況下,Spring Security是通過UsernamePasswordAuthenticationFilterattemptAuthentication方法實現認證的。它繼承自AbstractAuthenticationProcessingFilter,這個過濾器在AbstractAuthenticationFilterConfigurer配置類中加入到過濾鏈中。在attemptAuthentication方法中通過AuthenticationManager接口實現進行認證。

		UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(
				username, password);

		// Allow subclasses to set the "details" property
		setDetails(request, authRequest);

		return this.getAuthenticationManager().authenticate(authRequest);

​ 在實際開發中,特別是前后端分離的項目中,大部分時候我們都需要自定義認證,例如:前后端分離+Jwt令牌認證改造,我們需要做三件事:

1)實現UserDetailsService接口,重寫loadUserByUsername業務邏輯,重新封裝返回的對象。

@Service("userDetailsService")
public class UserDetailsServiceImpl implements UserDetailsService {

    @Resource
    private SysUserService userService;

    @Override
    public JwtUserDto loadUserByUsername(String username) {
        SysUser sysUser = userService.queryByUserName(username);      
        UserDto userDto = new UserDto(sysUser);
        userDto.setAvatar(avatar);           
        return new JwtUserDto(
                    userDto,
                    userService.getGrantedAuthoritiesByUserId(sysUser)
            );
    }
}

JwtUserDtoUserDetails的自定義擴展。

2)實現一個自定義的過濾器JwtTokenFilter,建議實現OncePerRequestFilter接口,在doFilterInternal方法里實現對令牌的認證,如果有合法令牌,則設置SecurityContext,然后繼續走過濾鏈。

 SecurityContextHolder.getContext().setAuthentication(authentication);

將過濾器加入到springSecurityFilterChain

httpSecurity.addFilterBefore(jwtTokenFilter,UsernamePasswordAuthenticationFilter.class);

注意:需要將WebSecurityConfigurerAdapter的實現中去掉formLogin()認證方式。這個時候過濾鏈中UsernamePasswordAuthenticationFilter過濾器將被刪除。

3)手動調用認證,並手動設置SecurityContext,並生成令牌返回前端,下次前端訪問帶着令牌就會進入第二步的JwtTokenFilter進行令牌的認證和安全上下文的設置。精簡代碼如下:

    @AnonymousAccess
    @PostMapping(value = "/login")
    public Result<Object> login(@Validated @RequestBody AuthUserDto authUser, HttpServletRequest request) {

        // 查詢驗證碼
     	... ...
        String password;
        // 前端密碼解密
       ... ...
        UsernamePasswordAuthenticationToken authenticationToken =
                new UsernamePasswordAuthenticationToken(authUser.getUsername(), password);
       ... ...
        Authentication authentication;
        try {
            authentication = authenticationManagerBuilder.getObject().authenticate(authenticationToken);
        } catch (AuthenticationException e) {
            log.warn("登錄失敗:{},username {},ip {}", e.getMessage(), authUser.getUsername(), request.getRemoteHost());
            return new Result<>(false, StatusCode.LOGIN_ERROR, "登錄失敗", null);
        }

        SecurityContextHolder.getContext().setAuthentication(authentication);

        // 生成令牌
        String token = tokenProvider.createToken(authentication);
        final JwtUserDto jwtUserDto = (JwtUserDto) authentication.getPrincipal();
       
        // 返回 token 與 用戶信息
        Map<String, Object> authInfo = new HashMap<String, Object>(2) {{
            put("token", properties.getTokenStartWith() + token);
            put("user", jwtUserDto);
        }};
            
        return new Result<>(authInfo);
    }

手動調用authenticationManagerBuilder.getObject().authenticate(authenticationToken),會通過DaoAuthenticationProvideradditionalAuthenticationChecks方法調用第一步的userDetailsService取出UserDetails對象進行密碼驗證。

四、授權(Authorization)

1、授權接口

​ 一旦認證成功,我們就可以進行授權了,它核心的策略就是AccessDecisionManager。它提供三個方法並且全部委托給AccessDecisionVoter,這有點像ProviderManager將認證委托給AuthenticationProvider

一個AccessDecisionVoter考慮一個Authentication(代表一個Principal)和一個被ConfigAttributes裝飾的安全對象:

boolean supports(ConfigAttribute attribute);

boolean supports(Class<?> clazz);

int vote(Authentication authentication, S object,
        Collection<ConfigAttribute> attributes);

AccessDecisionVoterAccessDecisionManager方法中的object參數是完全泛型化的,它代表任何用戶想要訪問(web 資源或 Java 方法是最常見的兩種情況)。ConfigAttributes也是相當泛型化的,它表示一個被裝飾的安全對象並帶有訪問權限級別的元數據。ConfigAttributes是一個接口,僅有一個返回String的方法,返回的字符串中包含資源所有者,解釋了訪問資源的規則。常見的ConfigAttributes是用戶的角色(比如ROLE_ADMINROLE_AUDIT),它們通常有一定的格式(比如以ROLE_作為前綴)或者是可計算的表達式。

大部分人使用默認的AccessDecisionManager,即AffirmativeBased(如果沒有 voters 返回那么該訪問將被授權)。任何自定義的行為最好放在 voter 中,無論添加一個新的 voter 還是修改已有的 voter。

使用 Spring Expression Language(SpEL)表達式的ConfigAttributes是很常見的,比如isFullyAuthenticated() && hasRole('FOO')。解析表達式和加載表達式由AccessDecisionVoter實現。要擴展可處理的表達式的范圍,需要自定義SecurityExpressionRoot,優勢也需要SecurityExpressionHandler

2、Method 安全

Spring Security 在支持 web 安全的同時,也提供了對 Java 方法執行的訪問規則。對於 Spring Security 來說,方法只是一種不同類型的“資源”而已。對用戶來說,訪問規則在ConfigAttribute中有相同的格式(比如 角色 或者 表達式),但在代碼中有不同的配置。第一步就是啟用方法安全,比如你可以在應用的啟動類上進行配置:

@SpringBootApplication
@EnableGlobalMethodSecurity(securedEnabled = true)
public class SampleSecureApplication {
}

之后,便可以在方法上直接使用注解:

@Service
public class MyService {

  @Secured("ROLE_USER")
  public String secure() {
    return "Hello Security";
  }

}

這個例子是一個有安全方法的服務。如果 Spring 創建了MyService Bean,那么它將被代理,調用者必須在方法調用之前通過一個安全攔截器。如果訪問被拒絕,調用者會拋出一個AccessDeniedException而不是執行這個方法的結果。

還有其他可用於強制執行安全約束的方法注解,特別是@PreAuthorize@PostAuthorize, 它們允許你在其中寫 SpEL 表達式並可以引用方法的參數和返回值。

提示:把 web 安全和方法安全放在一起並不突兀。過濾鏈提供了用戶體驗特性,比如認證和重定向到登錄界面。而方法安全在更細粒度級別上提供了保護。

3、授權與過濾器

​ 在Spring Security過濾鏈中最后一個環節通過FilterSecurityInterceptor進行了方法攔截,對需要授權的方法進行驗證。核心代碼如下:

FilterSecurityInterceptor中的

	public void doFilter(ServletRequest request, ServletResponse response,
			FilterChain chain) throws IOException, ServletException {
		FilterInvocation fi = new FilterInvocation(request, response, chain);
		invoke(fi);
	}
	public void invoke(FilterInvocation fi) throws IOException, ServletException {
		... ...

			InterceptorStatusToken token = super.beforeInvocation(fi);
		... ...
			super.afterInvocation(token, null);
		}
	}

攔截的具體實現在其父類AbstractSecurityInterceptor中完成。精簡代碼如下:

protected InterceptorStatusToken beforeInvocation(Object object) {
		... ...

      	Collection<ConfigAttribute> attributes = this.obtainSecurityMetadataSource()
				.getAttributes(object);
		// Attempt authorization
		try {
			this.accessDecisionManager.decide(authenticated, object, attributes);
		}
		catch (AccessDeniedException accessDeniedException) {
			publishEvent(new AuthorizationFailureEvent(object, attributes, authenticated,
					accessDeniedException));

			throw accessDeniedException;
		}
		
		if (runAs == null) {		
			// no further work post-invocation
			return new InterceptorStatusToken(SecurityContextHolder.getContext(), false,
					attributes, object);
		}
		else {			
			SecurityContext origCtx = SecurityContextHolder.getContext();
			SecurityContextHolder.setContext(SecurityContextHolder.createEmptyContext());
			SecurityContextHolder.getContext().setAuthentication(runAs);

			// need to revert to token.Authenticated post-invocation
			return new InterceptorStatusToken(origCtx, true, attributes, object);
		}
	}

最終調用的是AccessDecisionManager的實現類(一般是AffirmativeBased)來進行授權,而AffirmativeBased將授權委托給AccessDecisionVoter接口。

4、項目中自定義授權

在實際項目中可能經常用到的自定義授權如下:


    @PutMapping
    @PreAuthorize("@perms.check('user:update')")
    public Result<SysUser> update(@RequestBody SysUser sysUser) {
        return new Result<>(true, StatusCode.OK, "修改完成", sysUserService.update(sysUser));
    }

update方法需要用戶帶有user:update權限才能訪問,應用通過@perm.check方法檢查用戶是否具備權限。

@perm是SpEL對IOC容器中的bean進行引用。

@PreAuthorize是Spring Security自帶的權限校驗注解。

@Service(value = "perms")
public class PermissionCheck {   
    public final static String ADMIN = "1001";
    public Boolean check(String ...permissions){
        // 獲取當前用戶的所有權限
        List<String> perms = SecurityContextHolder.getContext().getAuthentication().getAuthorities()
                .stream().map(GrantedAuthority::getAuthority).collect(Collectors.toList());
        // 判斷當前用戶的所有權限是否包含接口上定義的權限
        return perms.contains(ADMIN) || Arrays.stream(permissions).anyMatch(perms::contains);
    }
}

注解中@PreAuthorize是授權的關鍵步驟,它是如何實現的?

實際上@preAuthorize的實現由接口AccessDecisionVoter的實現類PreInvocationAuthorizationAdviceVoter完成授權投票。

授權過程如下:

FilterSecurityInterceptor -> doFilter -> invoke -> super.beforeInvocation ->
AbstractSecurityInterceptor -> beforeInvocation -> accessDecisionManager.decide ->
AffirmativeBased -> decide -> 
AccessDecisionVoter -> vote ->
PreInvocationAuthorizationAdviceVoter -> vote -> preAdvice.before -> 
ExpressionBasedPreInvocationAdvice -> before -> ExpressionUtils.evaluateAsBoolean

1)FilterSecurityInterceptor過濾器攔截方法doFilter,包裝請求和過濾鏈。

2)invoke做一些簡單校驗,例如:是否帶有@preAuthorize注解等,看是否要進入攔截流程,如果符合過濾條件,則調用父類的攔截方法super.beforeInvocation

3)AbstractSecurityInterceptor調用AccessDecisionManager接口的實現類AffirmativeBased的decide方法進行授權判斷,如果失敗則拋出accessDeniedException異常,流程結束。

4)由於授權可能有多種實現,AffirmativeBased將授權判斷委托給AccessDecisionVoter接口。

5)根據授權類型,最終是AccessDecisionVoter的實現類PreInvocationAuthorizationAdviceVoter符合授權判斷,通過preAdvice.before進行vote投票判斷。

6)最終由表達式預調用處理器ExpressionBasedPreInvocationAdvice進行表達式計算,並返回BOOLEAN類型結果。

五、Spring Security 和線程

1、線程綁定

Spring Security是線程綁定的,因為它需要保證當前的已認證的用戶(authenticated principal)對下流的消費者可用。基本構建塊是SecurityContext,它可能包含Authentication(當一個用戶登陸后,authenticated肯定是 true)。你總是可以從SecurityContextHolder中的靜態方法得到SecurityContext,它內部使用了ThreadLocal進行管理。

SecurityContext context = SecurityContextHolder.getContext();
Authentication authentication = context.getAuthentication();
assert(authentication.isAuthenticated);

這種操作並不常見,但是它可能對你有幫助。比如,你需要寫一個自定義的認證過濾器(盡管如此,Spring Security 中還有一些基類可用於避免使用SecurityContextHolder的地方)。

如果需要訪問 web endpoint 中經過身份驗證的用戶,則可以在@RequestMapping中使用方法參數注解。例如:

@RequestMapping("/foo")
public String foo(@AuthenticationPrincipal User user) {
  ... // do stuff with user
}

這個注解相當於從SecurityContext中獲得當前Authentication,並調用getPrincipal()方法賦值給方法參數。Authentication中的Principal取決與用來認證的AuthenticationManager,所以這對於獲得對用戶數據類型的安全引用來說是一個有用的小技巧。

如果使用了 Spring Security,那么在HttpServletRequest中的Principal將是Authentication類型,因此你也可以直接使用它:

@RequestMapping("/foo")
public String foo(Principal principal) {
  Authentication authentication = (Authentication) principal;
  User = (User) authentication.getPrincipal();
  ... // do stuff with user
}

如果你需要編寫在沒有使用 Spring Security 的情況下的代碼,那么這會很有用(你需要在加載Authentication類時更加謹慎)。

2、異步執行安全方法

因為SecurityContext是線程綁定的,所以如果你想在后台執行安全方法,比如使用@Async,你需要確保上下文的傳遞。這總結起來就是將SecurityContextRunnableCallable等包裹起來在后台執行。Spring Security 提供了一些幫助使之變得簡單,比如RunnableCallable的包裝器。 要將 SecurityContext 傳遞到@Async注解的方法,你需要編寫 AsyncConfigurer 並確保 Executor 的正確性:

@Configuration
public class ApplicationConfiguration extends AsyncConfigurerSupport {

  @Override
  public Executor getAsyncExecutor() {
    return new DelegatingSecurityContextExecutorService(Executors.newFixedThreadPool(5));
  }

}


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM