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相關的過濾器通過HttpSecurity
的AddFilter
方法加入到過濾鏈中,加入的時機是各種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過濾器的自動配置是所有基本配置的源頭,這些關聯配置包括:SecurityAutoConfiguration
和SecurityFilterAutoConfiguration
,后者依賴前者。
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)注入SpringBootWebSecurityConfiguration
,WebSecurityEnablerConfiguration
,SecurityDataConfiguration
三個配置。重點。
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;
}
一個 AuthenticationManager
的authenticate()
方法中有三種情況:
- 返回
Authentication
(authenticated=true
),如果驗證輸入是合法的Principal
)。 - 拋出
AuthenticationException
異常,如果輸入不合法。 - 如果無法判斷,則返回
null
。
AuthenticationException
是一個運行時異常,通常被應用程序以常規的方式的處理,這取決於應用目的和代碼風格。換句話說,代碼中一般不會捕捉和處理這個異常。比如,可以使得網頁顯示認證失敗,后端返回 401 HTTP 狀態碼,響應頭中的WWW-Authenticate
有無視情況而定。
AuthenticationManager
最普遍的實現是ProviderManager
,ProviderManager
將認證委托給一系列的AuthenticationProvider
實例 。AuthenticationProvider
和 AuthenticationManager
很類似,但是它有一個額外的方法允許查詢它支持的Authentication
方式:
public interface AuthenticationProvider {
Authentication authenticate(Authentication authentication)
throws AuthenticationException;
boolean supports(Class<?> authentication);
}
supports
方法的Class authentication
參數其實是Class
類型的。一個ProviderManager
在一個應用中能支持多種不同的認證機制,通過將認證委托給一系列的AuthenticationProvider
。ProviderManager
沒有識別出的認證類型,將會被忽略。
每個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是通過UsernamePasswordAuthenticationFilter
的attemptAuthentication
方法實現認證的。它繼承自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)
);
}
}
JwtUserDto
是UserDetails
的自定義擴展。
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)
,會通過DaoAuthenticationProvider
的additionalAuthenticationChecks
方法調用第一步的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);
AccessDecisionVoter
和AccessDecisionManager
方法中的object
參數是完全泛型化的,它代表任何用戶想要訪問(web 資源或 Java 方法是最常見的兩種情況)。ConfigAttributes
也是相當泛型化的,它表示一個被裝飾的安全對象並帶有訪問權限級別的元數據。ConfigAttributes
是一個接口,僅有一個返回String
的方法,返回的字符串中包含資源所有者,解釋了訪問資源的規則。常見的ConfigAttributes
是用戶的角色(比如ROLE_ADMIN
和ROLE_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
,你需要確保上下文的傳遞。這總結起來就是將SecurityContext
用Runnable
、Callable
等包裹起來在后台執行。Spring Security 提供了一些幫助使之變得簡單,比如Runnable
和Callable
的包裝器。 要將 SecurityContext
傳遞到@Async
注解的方法,你需要編寫 AsyncConfigurer
並確保 Executor
的正確性:
@Configuration
public class ApplicationConfiguration extends AsyncConfigurerSupport {
@Override
public Executor getAsyncExecutor() {
return new DelegatingSecurityContextExecutorService(Executors.newFixedThreadPool(5));
}
}