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相关的过滤器通过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));
}
}