spring security oauth2 是基于 spring security,所以要理解 oauth2 是需要有 spring security 的基础。oauth2 包含的主要角色有:认证服务器、资源服务器。
1、认证服务器的示例
通过一个注解就可以表明当前服务是认证服务器:
@Configuration
@EnableAuthorizationServer
进入注解 EnableAuthorizationServer,可以看到通过 @Import 引入两个重要的配置类
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import({AuthorizationServerEndpointsConfiguration.class, AuthorizationServerSecurityConfiguration.class})
public @interface EnableAuthorizationServer {}
其中
暴露接口的类:AuthorizationServerEndpointsConfiguration,如,/oauth/authorize 的 AuthorizationEndpoint、/oauth/token 的 TokenEndpoint、/oauth/check_token 的 CheckTokenEndpoint
对各种接口配置权限、设置客户端的存储方式:AuthorizationServerSecurityConfiguration。
分析AuthorizationServerEndpointsConfiguration:
@Configuration
@Import(TokenKeyEndpointRegistrar.class)
public class AuthorizationServerEndpointsConfiguration {
// 认证服务器接口的配置
private AuthorizationServerEndpointsConfigurer endpoints = new AuthorizationServerEndpointsConfigurer();
// 客户端信息服务,默认是 InMemoryClientDetailsService,一般重写为 JdbcClientDetailsService
@Autowired
private ClientDetailsService clientDetailsService;
// 默认没有实现,需要自己实现。所以注解 @Configuration、@EnableAuthorizationServer 就可以放在这个实现类上。
@Autowired
private List<AuthorizationServerConfigurer> configurers = Collections.emptyList();
@PostConstruct
public void init() {
for (AuthorizationServerConfigurer configurer : configurers) {
try {
// 给 endpoints 设置自定义的配置。
configurer.configure(endpoints);
} catch (Exception e) {
throw new IllegalStateException("Cannot configure enpdoints", e);
}
}
// 设置回配置
endpoints.setClientDetailsService(clientDetailsService);
}
// 1、 AuthorizationEndpoint 创建
// AuthorizationEndpoint 用于服务于授权请求。预设地址:/oauth/authorize。
@Bean
public AuthorizationEndpoint authorizationEndpoint() throws Exception {
AuthorizationEndpoint authorizationEndpoint = new AuthorizationEndpoint();
FrameworkEndpointHandlerMapping mapping = getEndpointsConfigurer().getFrameworkEndpointHandlerMapping();
authorizationEndpoint.setUserApprovalPage(extractPath(mapping, "/oauth/confirm_access"));
authorizationEndpoint.setProviderExceptionHandler(exceptionTranslator());
authorizationEndpoint.setErrorPage(extractPath(mapping, "/oauth/error"));
authorizationEndpoint.setTokenGranter(tokenGranter());
authorizationEndpoint.setClientDetailsService(clientDetailsService);
authorizationEndpoint.setAuthorizationCodeServices(authorizationCodeServices());
authorizationEndpoint.setOAuth2RequestFactory(oauth2RequestFactory());
authorizationEndpoint.setOAuth2RequestValidator(oauth2RequestValidator());
authorizationEndpoint.setUserApprovalHandler(userApprovalHandler());
return authorizationEndpoint;
}
// 2、 TokenEndpoint 创建
// TokenEndpoint 用于创建服务访问令牌的请求。预设地址:/oauth/token。
@Bean
public TokenEndpoint tokenEndpoint() throws Exception {
TokenEndpoint tokenEndpoint = new TokenEndpoint();
tokenEndpoint.setClientDetailsService(clientDetailsService);
tokenEndpoint.setProviderExceptionHandler(exceptionTranslator());
tokenEndpoint.setTokenGranter(tokenGranter());
tokenEndpoint.setOAuth2RequestFactory(oauth2RequestFactory());
tokenEndpoint.setOAuth2RequestValidator(oauth2RequestValidator());
tokenEndpoint.setAllowedRequestMethods(allowedTokenEndpointRequestMethods());
return tokenEndpoint;
}
// 3、 CheckTokenEndpoint 创建
// CheckTokenEndpoint 用于查看服务访问令牌的请求。预设地址:/oauth/check_token。
@Bean
public CheckTokenEndpoint checkTokenEndpoint() {
CheckTokenEndpoint endpoint = new CheckTokenEndpoint(getEndpointsConfigurer().getResourceServerTokenServices());
endpoint.setAccessTokenConverter(getEndpointsConfigurer().getAccessTokenConverter());
endpoint.setExceptionTranslator(exceptionTranslator());
return endpoint;
}
// 省略部分代码
}
上面分析到实现 AuthorizationServerConfigurer,所以需要自定义,如下:
@Slf4j
@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
@Autowired
private PasswordEncoder passwordEncoder;
@Autowired
private ClientDetailsService clientDetailsService;
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private TokenStore tokenStore;
@Autowired
private JwtAccessTokenConverter jwtAccessTokenConverter;
// 默认 InMemoryClientDetailsService
@Bean
public ClientDetailsService clientDetailsService(DataSource dataSource) {
// 使用数据库实现,就要准备 oauth 的9张表
JdbcClientDetailsService jdbcClientDetailsService = new JdbcClientDetailsService(dataSource);
jdbcClientDetailsService.setPasswordEncoder(passwordEncoder);
return jdbcClientDetailsService;
}
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.withClientDetails(clientDetailsService);
}
@Bean
public AuthorizationServerTokenServices tokenService() {
DefaultTokenServices defaultTokenServices = new DefaultTokenServices();
//客户端配置
defaultTokenServices.setClientDetailsService(clientDetailsService);
//支持令牌的刷新
defaultTokenServices.setSupportRefreshToken(true);
//令牌存储服务
defaultTokenServices.setTokenStore(tokenStore); JSONObject.toJSONString(jwtAccessTokenConverter));JSONObject.toJSONString(jwtAccessTokenConverter.getClass().getName()));
//使用令牌增强
defaultTokenServices.setTokenEnhancer(jwtAccessTokenConverter);//这里是生成token时会用到jwtAccessTokenConverter,在tokenConfig里面是验证token时用到
//设置 access_token 的过期时间,2小时。默认12小时。
defaultTokenServices.setAccessTokenValiditySeconds(60 * 60 * 2);
//设置refresh_token的过期时间,30天。默认30天。
defaultTokenServices.setRefreshTokenValiditySeconds(60 * 60 * 24 * 30);
return defaultTokenServices;
}
// AuthorizationServerEndpointsConfiguration 的 init() 调用,设置自定义的配置。
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints.authenticationManager(authenticationManager)
.tokenServices(tokenService())
.allowedTokenEndpointRequestMethods(HttpMethod.POST);
}
// 开放 /oauth/token_key 和 /oauth/check_token
@Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
security.tokenKeyAccess("permitAll()")
.checkTokenAccess("permitAll()")
.allowFormAuthenticationForClients();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
上面有用到 AuthenticationManager,而容器中不存在,所以需要注册一个,这里继承 WebSecurityConfigurerAdapter,同时拦截部分自定义接口,默认这些接口都是放开的,但是我们可以只开发部分接口(如,我们的登录接口)。
@Configuration
public class SecurityAuthorizationConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.formLogin().and().cors().disable().csrf().disable()
// 默认这些接口都是开放的,但是我们可以只开发部分接口(如,我们的登录接口)
.authorizeRequests()
// 使用 antMatchers,就会添加 AntPathRequestMatcher 路径匹配,默认是 AnyRequestMatcher(任何请求都返回 true,也就是开放)
.antMatchers("/api/mylogin").permitAll()
.antMatchers("/**").authenticated();
}
// 容器中还没有 AuthenticationManager 这个 bean,直接使用 @Bean 将父类的 AuthenticationManager 注入到容器。
@Bean
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
}
在spring security oauth2.0 中,每次访问接口都需要经历 spring security 的过滤器链FilterChainProxy,这个过滤器链也是 spring security 的精髓,理解了这些过滤器,也就将 spring security oauth2.0 掌握了大半。
还是把Import 的两个配置类分析完,分析AuthorizationServerSecurityConfiguration:
@Configuration
@Order(0)
@Import({ ClientDetailsServiceConfiguration.class, AuthorizationServerEndpointsConfiguration.class })
public class AuthorizationServerSecurityConfiguration extends WebSecurityConfigurerAdapter {
@Autowired
private List<AuthorizationServerConfigurer> configurers = Collections.emptyList();
@Autowired
private ClientDetailsService clientDetailsService;
@Autowired
private AuthorizationServerEndpointsConfiguration endpoints;
@Autowired
public void configure(ClientDetailsServiceConfigurer clientDetails) throws Exception {
for (AuthorizationServerConfigurer configurer : configurers) {
// 就是自定义 AuthorizationServerConfig 中给客户端服务设置存储方式,这里我们是 jdbc 方式
configurer.configure(clientDetails);
}
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
}
// 可以看到,这里给 /oauth/token 默认设置需要全部权限、/oauth/token_key 和 /oauth/check_token 是拒绝访问。而自定义 AuthorizationServerConfig 中可以开放/oauth/token_key 和 /oauth/check_token。
@Override
protected void configure(HttpSecurity http) throws Exception {
AuthorizationServerSecurityConfigurer configurer = new AuthorizationServerSecurityConfigurer();
FrameworkEndpointHandlerMapping handlerMapping = endpoints.oauth2EndpointHandlerMapping();
http.setSharedObject(FrameworkEndpointHandlerMapping.class, handlerMapping);
configure(configurer);
http.apply(configurer);
String tokenEndpointPath = handlerMapping.getServletPath("/oauth/token");
String tokenKeyPath = handlerMapping.getServletPath("/oauth/token_key");
String checkTokenPath = handlerMapping.getServletPath("/oauth/check_token");
if (!endpoints.getEndpointsConfigurer().isUserDetailsServiceOverride()) {
UserDetailsService userDetailsService = http.getSharedObject(UserDetailsService.class);
endpoints.getEndpointsConfigurer().userDetailsService(userDetailsService);
}
http
.authorizeRequests()
.antMatchers(tokenEndpointPath).fullyAuthenticated()
.antMatchers(tokenKeyPath).access(configurer.getTokenKeyAccess())
.antMatchers(checkTokenPath).access(configurer.getCheckTokenAccess())
.and()
.requestMatchers()
.antMatchers(tokenEndpointPath, tokenKeyPath, checkTokenPath)
.and()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.NEVER);
http.setSharedObject(ClientDetailsService.class, clientDetailsService);
}
protected void configure(AuthorizationServerSecurityConfigurer oauthServer) throws Exception {
for (AuthorizationServerConfigurer configurer : configurers) {
configurer.configure(oauthServer);
}
}
}
2、源码
下面从源码的角度分析 spring security。熟悉 spring boot 源码的都知道,要整合一个框架是通过自动配置,所以我们也查看 spring.factories,找到 spring security 自动配置类SecurityAutoConfiguration置类
@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);
}
}
@Configuration(proxyBeanMethods = false)
@ConditionalOnBean(WebSecurityConfigurerAdapter.class)
@ConditionalOnMissingBean(name = BeanIds.SPRING_SECURITY_FILTER_CHAIN)
@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET)
@EnableWebSecurity // 注意这个类,我们只看这个
public class WebSecurityEnablerConfiguration {
}
@Retention(value = java.lang.annotation.RetentionPolicy.RUNTIME)
@Target(value = { java.lang.annotation.ElementType.TYPE })
@Documented
@Import({ WebSecurityConfiguration.class, // 注意这个类,重要
SpringWebMvcImportSelector.class,
OAuth2ImportSelector.class })
@EnableGlobalAuthentication // 注意这个类,重要
@Configuration
public @interface EnableWebSecurity {
boolean debug() default false;
}
@Retention(value = java.lang.annotation.RetentionPolicy.RUNTIME)
@Target(value = { java.lang.annotation.ElementType.TYPE })
@Documented
@Import(AuthenticationConfiguration.class) // 目的就是引入 AuthenticationConfiguration
@Configuration
public @interface EnableGlobalAuthentication {
}
找到了一个重要的配置类 WebSecurityConfiguration 和 AuthenticationConfiguration。
2.1 分析 WebSecurityConfiguration
@Configuration(proxyBeanMethods = false)
public class WebSecurityConfiguration implements ImportAware, BeanClassLoaderAware {
@Bean(name = AbstractSecurityWebApplicationInitializer.DEFAULT_FILTER_NAME)
public Filter springSecurityFilterChain() throws Exception {
boolean hasConfigurers = webSecurityConfigurers != null && !webSecurityConfigurers.isEmpty();
if (!hasConfigurers) {
// 不存在 webSecurityConfigurers 就创建一个 WebSecurityConfigurerAdapter,一般我们都会实现,
// 因为 WebSecurityConfigurerAdapter 的 configure(HttpSecurity http) 拦截所有接口,所有接口都需要认证,而我们需要开发登录接口,所以需要重写
WebSecurityConfigurerAdapter adapter = objectObjectPostProcessor
.postProcess(new WebSecurityConfigurerAdapter() {});
webSecurity.apply(adapter);
}
// (重要)构建 webSecurity 的过滤器链
return webSecurity.build();
}
// 将容器中所有 SecurityConfigurer 实现到加入到 webSecurity,如我们自定义的 SecurityAuthorizationConfig
@Autowired(required = false)
public void setFilterChainProxySecurityConfigurer(
ObjectPostProcessor<Object> objectPostProcessor,@Value("#{@autowiredWebSecurityConfigurersIgnoreParents.getWebSecurityConfigurers()}")
List<SecurityConfigurer<Filter, WebSecurity>> webSecurityConfigurers) throws Exception {
webSecurity = objectPostProcessor.postProcess(new WebSecurity(objectPostProcessor));
if (debugEnabled != null) {
webSecurity.debug(debugEnabled);
}
webSecurityConfigurers.sort(AnnotationAwareOrderComparator.INSTANCE);
Integer previousOrder = null;
Object previousConfig = null;
for (SecurityConfigurer<Filter, WebSecurity> config : webSecurityConfigurers) {
Integer order = AnnotationAwareOrderComparator.lookupOrder(config);
if (previousOrder != null && previousOrder.equals(order)) {
throw new IllegalStateException(
"@Order on WebSecurityConfigurers must be unique. Order of "
+ order + " was already used on " + previousConfig + ", so it cannot be used on "
+ config + " too.");
}
previousOrder = order;
previousConfig = config;
}
for (SecurityConfigurer<Filter, WebSecurity> webSecurityConfigurer : webSecurityConfigurers) {
// 将 webSecurityConfigurers 都加入到 webSecurity
webSecurity.apply(webSecurityConfigurer);
}
this.webSecurityConfigurers = webSecurityConfigurers;
}
}
上面的属性注入肯定比内部 @Bean 要先,所以先找到所有 SecurityConfigurer,并加入到 webSecurity,默认就存在一个注入到容器的实现 AuthorizationServerSecurityConfiguration,每一个实现,都会构建一个 HttpSecurity,并且对应一个过滤器链。
注意这一行代码:webSecurity.build(),这一行代码就会去构建整个 security 的过滤器。
public final O build() throws Exception {
if (this.building.compareAndSet(false, true)) {
this.object = doBuild();
return this.object;
}
throw new AlreadyBuiltException("This object has already been built");
}
// 该方法会多次进入,WebSecurity、HttpSecurity、AuthenticationManagerBuilder等依次进入。
@Override
protected final O doBuild() throws Exception {
synchronized (configurers) {
buildState = BuildState.INITIALIZING;
beforeInit();
// 执行 init() 方法,
// 调用所有的 SecurityConfigurer 方法,其中包含 AuthorizationServerSecurityConfiguration 和 自定义的实现
// 父类 WebSecurityConfigurerAdapter 才实现 init(),会构建 HttpSecurity
init();
buildState = BuildState.CONFIGURING;
// WebSecurity 的 beforeConfigure() 空方法。
// HttpSecurity 的 beforeConfigure() 是先构建 AuthenticationManagerBuilder 认证集合相关的 ProviderManager(实际认证的工作由 AuthenticationProvider 来做)。
beforeConfigure();
// HttpSecurity 的 configure() 会加载各种过滤器(比如,CorsConfigurer 就会给 HttpSecurity 添加 CorsFilter),然后在 performBuild() 构建过滤器链
configure();
buildState = BuildState.BUILDING;
// 在 WebSecurity 的 performBuild() 方法中,会调用 HttpSecurity 的 build() 方法,然后进入 HttpSecurity 的 doBuild()。
O result = performBuild();
buildState = BuildState.BUILT;
return result;
}
}
过滤器的加载是由 HttpSecurity 添加的,比如 http.formLogin().and().cors().disable().csrf() 这一句就加入了 3 个过滤器,如 formLogin() 就会添加 UsernamePasswordAuthenticationFilter:
public FormLoginConfigurer<HttpSecurity> formLogin() throws Exception {
return getOrApply(new FormLoginConfigurer<>());
}
public FormLoginConfigurer() {
super(new UsernamePasswordAuthenticationFilter(), null);
usernameParameter("username");
passwordParameter("password");
}
下面依次看看WebSecurity、HttpSecurity、AuthenticationManagerBuilder 的几个方法
1、WebSecurity 的 performBuild() 方法
@Override
protected Filter performBuild() throws Exception {
Assert.state(
!securityFilterChainBuilders.isEmpty(),
() -> "At least one SecurityBuilder<? extends SecurityFilterChain> needs to be specified. "
+ "Typically this done by adding a @Configuration that extends WebSecurityConfigurerAdapter. "
+ "More advanced users can invoke "
+ WebSecurity.class.getSimpleName()
+ ".addSecurityFilterChainBuilder directly");
int chainSize = ignoredRequests.size() + securityFilterChainBuilders.size();
List<SecurityFilterChain> securityFilterChains = new ArrayList<>(chainSize);
for (RequestMatcher ignoredRequest : ignoredRequests) {
securityFilterChains.add(new DefaultSecurityFilterChain(ignoredRequest));
}
for (SecurityBuilder<? extends SecurityFilterChain> securityFilterChainBuilder : securityFilterChainBuilders) {
// 调用 HttpSecurity 的 build() 方法,然后进入 HttpSecurity 的 doBuild()。
// 方法的返回值就是一个过滤器链
securityFilterChains.add(securityFilterChainBuilder.build());
}
// 所以 FilterChainProxy 包含多个过滤器链
FilterChainProxy filterChainProxy = new FilterChainProxy(securityFilterChains);
if (httpFirewall != null) {
filterChainProxy.setFirewall(httpFirewall);
}
filterChainProxy.afterPropertiesSet();
Filter result = filterChainProxy;
postBuildAction.run();
return result;
}
2、HttpSecurity 的 configure() 方法
@Override
protected void beforeConfigure() throws Exception {
// 再次进入 AbstractConfiguredSecurityBuilder#build() 方法,
// 构建 AuthenticationManagerBuilder 认证集合相关的 ProviderManager(实际认证的工作由 AuthenticationProvider 来做)。
setSharedObject(AuthenticationManager.class, getAuthenticationRegistry().build());
}
3、AuthenticationManagerBuilder的 performBuild() 方法
@Override
protected ProviderManager performBuild() throws Exception {
if (!isConfigured()) {
logger.debug("No authenticationProviders and no parentAuthenticationManager defined. Returning null.");
return null;
}
// 1、创建了一个包含 authenticationProviders 参数的 ProviderManager 对象
// ProviderManager 是 AuthenticationManager 的实现类
ProviderManager providerManager = new ProviderManager(authenticationProviders, parentAuthenticationManager);
if (eraseCredentials != null) {
// 在认证后,删除认证信息
providerManager.setEraseCredentialsAfterAuthentication(eraseCredentials);
}
if (eventPublisher != null) {
// 认证成功后,发布事件
providerManager.setAuthenticationEventPublisher(eventPublisher);
}
providerManager = postProcess(providerManager);
return providerManager;
}
4、HttpSecurity 的 configure() 方法
private void configure() throws Exception {
Collection<SecurityConfigurer<O, B>> configurers = getConfigurers();
for (SecurityConfigurer<O, B> configurer : configurers) {
// 以 CorsConfigurer 为例
configurer.configure((B) this);
}
}
CorsConfigurer 的 configure() 方法
@Override
public void configure(H http) {
ApplicationContext context = http.getSharedObject(ApplicationContext.class);
// CorsConfigurer 构建一个 CorsFilter
CorsFilter corsFilter = getCorsFilter(context);
if (corsFilter == null) {
throw new IllegalStateException(
"Please configure either a " + CORS_FILTER_BEAN_NAME + " bean or a "
+ CORS_CONFIGURATION_SOURCE_BEAN_NAME + "bean.");
}
// 添加进 HttpSecurity
http.addFilter(corsFilter);
}
5、HttpSecurity 的 performBuild() 方法
@Override
protected DefaultSecurityFilterChain performBuild() {
// 先将 filters 按照 order 排序,小的先执行
filters.sort(comparator);
// 构建过滤器链
return new DefaultSecurityFilterChain(requestMatcher, filters);
}
所以一个 WebSecurity 包含多个 HttpSecurity;一个 HttpSecurity 包含一个过滤器链和一个认证管理器 AuthenticationManager;一个 AuthenticationManager 包含一个 ProviderManager,一个 ProviderManager 中真正参与认证类是一组 AuthenticationProvider。
过滤器链是什么时候执行的呢?
如果存在 SecurityFilterAutoConfiguration ,就会注入 DelegatingFilterProxyRegistrationBean
@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;
}
}
而 DelegatingFilterProxyRegistrationBean 实现 ServletContextInitializer,在 Tomcat 构建 容器 ServletWebServerApplicationContext 时,就会调用当前 DelegatingFilterProxyRegistrationBean 的 getFilter 方法。
ServletWebServerApplicationContext 类 selfInitialize() 方法
private void selfInitialize(ServletContext servletContext) throws ServletException {
prepareWebApplicationContext(servletContext);
registerApplicationScope(servletContext);
WebApplicationContextUtils.registerEnvironmentBeans(getBeanFactory(), servletContext);
for (ServletContextInitializer beans : getServletContextInitializerBeans()) {
beans.onStartup(servletContext);
}
}
ServletWebServerApplicationContext#selfInitialize() ->
RegistrationBean#onStartup() ->
AbstractFilterRegistrationBean#getDescription() ->
DelegatingFilterProxyRegistrationBean#getFilter()
就把名称为 FilterChainProxy 的过滤器链加入到 Tomcat 的 Context 容器,在客户端调用服务器接口的时候,就会经历各种过滤器。
过滤器链中到底有多少个过滤器,有默认的过滤器,也有自定义添加的过滤器。先看默认的过滤器,在 WebSecurity 的 doBuild() 方法调用 init() 方法,遍历执行 SecurityConfigurer 的 init() 方法,也就是默认的 AuthorizationServerSecurityConfiguration 和我们实现的 SecurityAuthorizationConfig,init() 在它们的父类 WebSecurityConfigurerAdapter:
public void init(final WebSecurity web) throws Exception {
// FIXME 构建 HttpSecurity
final HttpSecurity http = getHttp();
// HttpSecurity 存入到 WebSecurity 的 securityFilterChainBuilders
web.addSecurityFilterChainBuilder(http).postBuildAction(() -> {
FilterSecurityInterceptor securityInterceptor = http
.getSharedObject(FilterSecurityInterceptor.class);
web.securityInterceptor(securityInterceptor);
});
}
protected final HttpSecurity getHttp() throws Exception {
if (http != null) {
return http;
}
DefaultAuthenticationEventPublisher eventPublisher = objectPostProcessor
.postProcess(new DefaultAuthenticationEventPublisher());
localConfigureAuthenticationBldr.authenticationEventPublisher(eventPublisher);
// 认证管理器
AuthenticationManager authenticationManager = authenticationManager();
authenticationBuilder.parentAuthenticationManager(authenticationManager);
authenticationBuilder.authenticationEventPublisher(eventPublisher);
Map<Class<?>, Object> sharedObjects = createSharedObjects();
// FIXME 构建 HttpSecurity
http = new HttpSecurity(objectPostProcessor, authenticationBuilder,
sharedObjects);
if (!disableDefaults) {
// @formatter:off
// 默认添加以下 Filter,重写 configure(http); 方法,可以增加自定义的过滤器
http
// 添加配置器 CsrfConfigurer(包含过滤器 CsrfFilter)对CSRF的支持。
.csrf().and()
// 添加过滤器 WebAsyncManagerIntegrationFilter。
.addFilter(new WebAsyncManagerIntegrationFilter())
// 添加配置器 ExceptionHandlingConfigurer(包含过滤器ExceptionTranslationFilter)对异常处理的支持。
.exceptionHandling().and()
// 添加配置器 HeadersConfigurer(包含过滤器HeaderWriterFilter)支持Adds the Security HTTP headers to the response。
.headers().and()
// 添加配置器SessionManagementConfigurer(包含过滤器SessionManagementFilter)支持session管理。
.sessionManagement().and()
// 添加配置器SecurityContextConfigurer(包含过滤器 SecurityContextPersistenceFilter)支持对SecurityContextHolder的配置。
.securityContext().and()
// 添加配置器RequestCacheConfigurer(包含过滤器RequestCacheAwareFilter)支持request cache。
.requestCache().and()
// 添加配置器AnonymousConfigurer(包含过滤器AnonymousAuthenticationFilter)支持Anonymous authentication。
.anonymous().and()
// 添加配置器ServletApiConfigurer(包含过滤器SecurityContextHolderAwareRequestFilter)支持更多Servlet API。
.servletApi().and()
// 添加配置器DefaultLoginPageConfigurer(包含过滤器DefaultLoginPageGeneratingFilter,DefaultLogoutPageGeneratingFilter)支持默认的login和logout。
.apply(new DefaultLoginPageConfigurer<>()).and()
// 添加配置器LogoutConfigurer(包含过滤器LogoutFilter)支持logout。
.logout();
// @formatter:on
ClassLoader classLoader = this.context.getClassLoader();
List<AbstractHttpConfigurer> defaultHttpConfigurers =
SpringFactoriesLoader.loadFactories(AbstractHttpConfigurer.class, classLoader);
for (AbstractHttpConfigurer configurer : defaultHttpConfigurers) {
http.apply(configurer);
}
}
// FIXME HttpSecurity 拦截所有接口,即所有接口都需要认证,我们常常重写该方法,开发登录接口
configure(http);
return http;
}
AuthorizationServerSecurityConfiguration 的 configure(http); 实现如下
@Override
protected void configure(HttpSecurity http) throws Exception {
// AuthorizationServerSecurityConfigurer 会增加两个 Filter:
// ClientCredentialsTokenEndpointFilter 和 BasicAuthenticationFilter
AuthorizationServerSecurityConfigurer configurer = new AuthorizationServerSecurityConfigurer();
FrameworkEndpointHandlerMapping handlerMapping = endpoints.oauth2EndpointHandlerMapping();
http.setSharedObject(FrameworkEndpointHandlerMapping.class, handlerMapping);
configure(configurer);
http.apply(configurer);
String tokenEndpointPath = handlerMapping.getServletPath("/oauth/token");
String tokenKeyPath = handlerMapping.getServletPath("/oauth/token_key");
String checkTokenPath = handlerMapping.getServletPath("/oauth/check_token");
if (!endpoints.getEndpointsConfigurer().isUserDetailsServiceOverride()) {
UserDetailsService userDetailsService = http.getSharedObject(UserDetailsService.class);
endpoints.getEndpointsConfigurer().userDetailsService(userDetailsService);
}
// @formatter:off
http
// authorizeRequests() 给过滤器链加上最后一个过滤器 FilterSecurityInterceptor
.authorizeRequests()
.antMatchers(tokenEndpointPath).fullyAuthenticated()
.antMatchers(tokenKeyPath).access(configurer.getTokenKeyAccess())
.antMatchers(checkTokenPath).access(configurer.getCheckTokenAccess())
.and()
.requestMatchers()
.antMatchers(tokenEndpointPath, tokenKeyPath, checkTokenPath)
.and()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.NEVER);
// @formatter:on
http.setSharedObject(ClientDetailsService.class, clientDetailsService);
}
所以,默认的过滤器链有以下过滤器:
WebAsyncManagerIntegrationFilter、SecurityContextPersistenceFilter、HeaderWriterFilter、LogoutFilter、
ClientCredentialsTokenEndpointFilter(oauth 提供不用,被后面 Basic 替换)、BasicAuthenticationFilter(/oauth/token)、
DefaultLoginPageGeneratingFilter、DefaultLogoutPageGeneratingFilter、RequestCacheAwareFilter、SecurityContextHolderAwareRequestFilter、
AnonymousAuthenticationFilter、SessionManagementFilter、ExceptionTranslationFilter、FilterSecurityInterceptor
而我们自定义的实现:
@Override
protected void configure(HttpSecurity http) throws Exception {
http
// 添加 UsernamePasswordAuthenticationFilter
.formLogin()
.and().cors().disable().csrf().disable()
// 默认这些接口都是开放的,但是我们可以只开发部分接口(如,我们的登录接口)
// authorizeRequests() 给过滤器链加上最后一个过滤器 FilterSecurityInterceptor
.authorizeRequests()
// 使用 antMatchers,就会添加 AntPathRequestMatcher 路径匹配,默认是 AnyRequestMatcher(任何请求都返回 true,也就是开放)
.antMatchers("/api/mylogin").permitAll()
//.antMatchers("/oauth**").permitAll()
.antMatchers("/**").authenticated();
}
过滤器链有以下过滤器:
WebAsyncManagerIntegrationFilter、SecurityContextPersistenceFilter、HeaderWriterFilter、LogoutFilter、
UsernamePasswordAuthenticationFilter(HttpSecurity.formLogin() /login POST)、
DefaultLoginPageGeneratingFilter、DefaultLogoutPageGeneratingFilter、RequestCacheAwareFilter、SecurityContextHolderAwareRequestFilter、
AnonymousAuthenticationFilter、SessionManagementFilter、ExceptionTranslationFilter、FilterSecurityInterceptor
这里只分析 4 个过滤器:UsernamePasswordAuthenticationFilter、BasicAuthenticationFilter、ExceptionTranslationFilter、FilterSecurityInterceptor。
1、UsernamePasswordAuthenticationFilter
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) req;
HttpServletResponse response = (HttpServletResponse) res;
// 1、判断请求地址是否是 /login 和 请求方式为 POST (UsernamePasswordAuthenticationFilter 构造方法 确定的)
// 当然可以通过 loginProcessingUrl() 修改 UsernamePasswordAuthenticationFilter 指定的 /login 为其它的,如 /user/auth/login
if (!requiresAuthentication(request, response)) {
// 如果不是 /login,那么就直接放行。我们一般都有自己的接口,如 /user/auth/login,这就不和 /login 一致,那么会在 AnonymousAuthenticationFilter 创建一个匿名认证
chain.doFilter(request, response);
return;
}
if (logger.isDebugEnabled()) {
logger.debug("Request is to process authentication");
}
Authentication authResult;
try {
// 2、调用子类 UsernamePasswordAuthenticationFilter 的 attemptAuthentication() 方法进行认证
authResult = attemptAuthentication(request, response);
if (authResult == null) {
// return immediately as subclass has indicated that it hasn't completed
// authentication
return;
}
// 3、将认证成功的 Authentication 存入Session中
sessionStrategy.onAuthentication(authResult, request, response);
}
catch (InternalAuthenticationServiceException failed) {
logger.error(
"An internal error occurred while trying to authenticate the user.",
failed);
// 4、认证失败
// 调用 AuthenticationFailureHandler 的 onAuthenticationFailure() 方法进行失败处理
// (可以通过继承 AuthenticationFailureHandler 自定义失败处理逻辑,比如我们是前后端分离,需要返回 json)
unsuccessfulAuthentication(request, response, failed);
return;
}
catch (AuthenticationException failed) {
// Authentication failed
unsuccessfulAuthentication(request, response, failed);
return;
}
// Authentication success
if (continueChainBeforeSuccessfulAuthentication) {
chain.doFilter(request, response);
}
// 5、认证成功
// 调用 AuthenticationSuccessHandler 的 onAuthenticationSuccess() 进行失败处理
// (可以通过继承 AuthenticationFailureHandler 自定义失败处理逻辑,比如我们是前后端分离,需要返回 json,但是成功我们都是返回 accessToken,所以不用管)
successfulAuthentication(request, response, chain, authResult);
}
public Authentication attemptAuthentication(HttpServletRequest request,
HttpServletResponse response) throws AuthenticationException {
if (postOnly && !request.getMethod().equals("POST")) {
throw new AuthenticationServiceException(
"Authentication method not supported: " + request.getMethod());
}
String username = obtainUsername(request);
String password = obtainPassword(request);
if (username == null) {
username = "";
}
if (password == null) {
password = "";
}
username = username.trim();
// 封装 username、password,此时 token 还属于未认证状态
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);
// Allow subclasses to set the "details" property
setDetails(request, authRequest);
// 通过 认证管理器进行认证。
// AuthenticationManager 的实现是 ProviderManager
return this.getAuthenticationManager().authenticate(authRequest);
}
// ProviderManager 的 authenticate()
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
// 入参是未认证的 Token,出参是认证成功的 Token
Class<? extends Authentication> toTest = authentication.getClass();
AuthenticationException lastException = null;
AuthenticationException parentException = null;
Authentication result = null;
Authentication parentResult = null;
boolean debug = logger.isDebugEnabled();
// 1、通过 getProviders() 方法获取 AuthenticationProvider 集合
for (AuthenticationProvider provider : getProviders()) {
// Authentication 的实现都是各种类型的 Token,比如 UsernamePasswordAuthenticationToken
// 一种 token 可能对应多个 AuthenticationProvider,比如支持 UsernamePasswordAuthenticationToken 的有 AbstractLdapAuthenticationProvider、AbstractUserDetailsAuthenticationProvider
// 但是同时只会有一个支持的
if (!provider.supports(toTest)) {
continue;
}
if (debug) {
logger.debug("Authentication attempt using "
+ provider.getClass().getName());
}
try {
// FIXME 3、调用认证方法
// 看实现 DaoAuthenticationProvider(父类是 AbstractUserDetailsAuthenticationProvider)
result = provider.authenticate(authentication);
if (result != null) {
copyDetails(authentication, result);
break;
}
}
catch (AccountStatusException | InternalAuthenticationServiceException e) {
prepareException(e, authentication);
// SEC-546: Avoid polling additional providers if auth failure is due to
// invalid account status
throw e;
} catch (AuthenticationException e) {
lastException = e;
}
}
if (result == null && parent != null) {
// Allow the parent to try.
try {
// 4、前面都认证不成功,调用父类(严格意思不是调用父类,而是其他的 AuthenticationManager 实现类)认证方法
result = parentResult = parent.authenticate(authentication);
}
catch (ProviderNotFoundException e) {
// ignore as we will throw below if no other exception occurred prior to
// calling parent and the parent
// may throw ProviderNotFound even though a provider in the child already
// handled the request
}
catch (AuthenticationException e) {
lastException = parentException = e;
}
}
if (result != null) {
if (eraseCredentialsAfterAuthentication
&& (result instanceof CredentialsContainer)) {
// Authentication is complete. Remove credentials and other secret data from authentication
// 5、删除认证成功后的 密码信息,保证安全
((CredentialsContainer) result).eraseCredentials();
}
// If the parent AuthenticationManager was attempted and successful than it will publish an AuthenticationSuccessEvent
// This check prevents a duplicate AuthenticationSuccessEvent if the parent AuthenticationManager already published it
if (parentResult == null) {
eventPublisher.publishAuthenticationSuccess(result);
}
return result;
}
// Parent was null, or didn't authenticate (or throw an exception).
if (lastException == null) {
lastException = new ProviderNotFoundException(messages.getMessage(
"ProviderManager.providerNotFound",
new Object[] { toTest.getName() },
"No AuthenticationProvider found for {0}"));
}
// If the parent AuthenticationManager was attempted and failed than it will publish an AbstractAuthenticationFailureEvent
// This check prevents a duplicate AbstractAuthenticationFailureEvent if the parent AuthenticationManager already published it
if (parentException == null) {
prepareException(lastException, authentication);
}
throw lastException;
}
// AbstractUserDetailsAuthenticationProvider 的 authenticate()
public Authentication authenticate(Authentication authentication)
throws AuthenticationException {
Assert.isInstanceOf(UsernamePasswordAuthenticationToken.class, authentication,
() -> messages.getMessage(
"AbstractUserDetailsAuthenticationProvider.onlySupports",
"Only UsernamePasswordAuthenticationToken is supported"));
// Determine username
// 1、从 authentication 中获取 用户名
String username = (authentication.getPrincipal() == null) ? "NONE_PROVIDED" : authentication.getName();
boolean cacheWasUsed = true;
// 2、根据 username 从缓存中获取认证成功的 UserDetails 信息
UserDetails user = this.userCache.getUserFromCache(username);
if (user == null) {
cacheWasUsed = false;
try {
// 3、如果缓存中没有用户信息 需要 获取用户信息(由 DaoAuthenticationProvider 实现 )
user = retrieveUser(username, (UsernamePasswordAuthenticationToken) authentication);
}
catch (UsernameNotFoundException notFound) {
logger.debug("User '" + username + "' not found");
if (hideUserNotFoundExceptions) {
throw new BadCredentialsException(messages.getMessage(
"AbstractUserDetailsAuthenticationProvider.badCredentials",
"Bad credentials"));
}
else {
throw notFound;
}
}
Assert.notNull(user,
"retrieveUser returned null - a violation of the interface contract");
}
try {
// 4、前置检查账户是否锁定,过期,冻结(由 DefaultPreAuthenticationChecks 类实现)
preAuthenticationChecks.check(user);
// 5、主要是验证 获取到的用户密码与传入的用户密码是否一致
additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken) authentication);
}
catch (AuthenticationException exception) {
if (cacheWasUsed) {
// 缓存出了 bug,再查一次
// There was a problem, so try again after checking
// we're using latest data (i.e. not from the cache)
cacheWasUsed = false;
user = retrieveUser(username, (UsernamePasswordAuthenticationToken) authentication);
preAuthenticationChecks.check(user);
additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken) authentication);
}
else {
throw exception;
}
}
// 6、后置检查用户密码是否 过期
postAuthenticationChecks.check(user);
// 7、验证成功后的用户信息存入缓存
if (!cacheWasUsed) {
this.userCache.putUserInCache(user);
}
Object principalToReturn = user;
if (forcePrincipalAsString) {
principalToReturn = user.getUsername();
}
// 8、重新创建一个 authenticated 为true (即认证成功)的 UsernamePasswordAuthenticationToken 对象并返回
return createSuccessAuthentication(principalToReturn, authentication, user);
}
// DaoAuthenticationProvider 的 retrieveUser()
protected final UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
prepareTimingAttackProtection();
try {
// 通过 UserDetailsService 的 loadUserByUsername() 方法 获取用户信息
// 这个方法就是我们自己实现
// AuthorizationServerSecurityConfiguration 在设置 AuthorizationServerSecurityConfigurer 时,添加的 UserDetailsService 实现是 ClientDetailsUserDetailsService
UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username);
if (loadedUser == null) {
throw new InternalAuthenticationServiceException(
"UserDetailsService returned null, which is an interface contract violation");
}
return loadedUser;
}
catch (UsernameNotFoundException ex) {
mitigateAgainstTimingAttack(authentication);
throw ex;
}
catch (InternalAuthenticationServiceException ex) {
throw ex;
}
catch (Exception ex) {
throw new InternalAuthenticationServiceException(ex.getMessage(), ex);
}
}
// UserDetailsService 就是我们自己需要自定义的内容,封装我们自己的用户、角色。
@Service
public class JwtUserDetailsServer implements UserDetailsService {
@Resource
private UserDao userDao;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
UserDTO userByUsername = userDao.getUserByUsername(username);
List<String> permissions = userDao.findPermissionsByUserId(userByUsername.getId());
Collection<SimpleGrantedAuthority> authorities = new ArrayList<SimpleGrantedAuthority>();
permissions.forEach(p -> authorities.add(new SimpleGrantedAuthority(p)));
return new User(JSONObject.toJSONString(userByUsername), userByUsername.getPassword(), authorities);
}
}
2、BasicAuthenticationFilter 实现 Web 的 OncePerRequestFilter,所以不看 doFilter(),直接看BasicAuthenticationFilter 中的 doFilterInternal() 实现
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response, FilterChain chain)
throws IOException, ServletException {
final boolean debug = this.logger.isDebugEnabled();
try {
// 获取请求中的 token,如果是我们接口调用 /oauth/token,就是从这里取出 token,也就是 username:password
UsernamePasswordAuthenticationToken authRequest = authenticationConverter.convert(request);
if (authRequest == null) {
// 没有 token 就放行
chain.doFilter(request, response);
return;
}
String username = authRequest.getName();
if (debug) {
this.logger
.debug("Basic Authentication Authorization header found for user '"
+ username + "'");
}
if (authenticationIsRequired(username)) {
// 通过 认证管理器进行认证
// AuthenticationManager 的实现是 ProviderManager
Authentication authResult = this.authenticationManager.authenticate(authRequest);
if (debug) {
this.logger.debug("Authentication success: " + authResult);
}
// 设置到上下文
SecurityContextHolder.getContext().setAuthentication(authResult);
// 记住我的功能
this.rememberMeServices.loginSuccess(request, response, authResult);
onSuccessfulAuthentication(request, response, authResult);
}
}
catch (AuthenticationException failed) {
SecurityContextHolder.clearContext();
if (debug) {
this.logger.debug("Authentication request for failed: " + failed);
}
this.rememberMeServices.loginFail(request, response);
onUnsuccessfulAuthentication(request, response, failed);
if (this.ignoreFailure) {
chain.doFilter(request, response);
}
else {
this.authenticationEntryPoint.commence(request, response, failed);
}
return;
}
chain.doFilter(request, response);
}
BasicAuthenticationFilter 用于客户端,UserDetailsService 实现是 ClientDetailsUserDetailsService,读取的是 oauth 的表 oauth_client_details。
3、ExceptionTranslationFilter,用处就在于它捕获 AuthenticationException 和 AccessDeniedException。
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) req;
HttpServletResponse response = (HttpServletResponse) res;
try {
chain.doFilter(request, response);
logger.debug("Chain processed normally");
} catch (IOException ex) {
throw ex;
} catch (Exception ex) {
// Try to extract a SpringSecurityException from the stacktrace
Throwable[] causeChain = throwableAnalyzer.determineCauseChain(ex);
// 获取异常链中的认证异常 AuthenticationException
RuntimeException ase = (AuthenticationException) throwableAnalyzer.getFirstThrowableOfType(AuthenticationException.class, causeChain);
// 没有认证异常 AuthenticationException,那么查找访问被拒绝异常 AccessDeniedException
if (ase == null) {
ase = (AccessDeniedException) throwableAnalyzer.getFirstThrowableOfType(AccessDeniedException.class, causeChain);
}
// 如果存在认证异常 或者 查找访问被拒绝异常,那么就需要处理,如果是其它类异常,直接抛出去
if (ase != null) {
if (response.isCommitted()) {
throw new ServletException("Unable to handle the Spring Security Exception because the response is already committed.", ex);
}
handleSpringSecurityException(request, response, chain, ase);
} else {
// Rethrow ServletExceptions and RuntimeExceptions as-is
if (ex instanceof ServletException) {
throw (ServletException) ex;
} else if (ex instanceof RuntimeException) {
throw (RuntimeException) ex;
}
// Wrap other Exceptions. This shouldn't actually happen
// as we've already covered all the possibilities for doFilter
throw new RuntimeException(ex);
}
}
}
private void handleSpringSecurityException(HttpServletRequest request,
HttpServletResponse response, FilterChain chain, RuntimeException exception)
throws IOException, ServletException {
// 处理认证异常
if (exception instanceof AuthenticationException) {
logger.debug("Authentication exception occurred; redirecting to authentication entry point", exception);
// 认证异常;重定向到身份验证入口点
sendStartAuthentication(request, response, chain, (AuthenticationException) exception);
}
// 处理查找访问被拒绝异常
else if (exception instanceof AccessDeniedException) {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
// 匿名用户或者是记住我,那么重定向到身份验证入口点;
if (authenticationTrustResolver.isAnonymous(authentication) || authenticationTrustResolver.isRememberMe(authentication)) {
logger.debug(
"Access is denied (user is " + (authenticationTrustResolver.isAnonymous(authentication) ? "anonymous" : "not fully authenticated") + "); redirecting to authentication entry point",
exception);
// 认证异常;重定向到身份验证入口点
sendStartAuthentication(
request,
response,
chain,
new InsufficientAuthenticationException(
messages.getMessage(
"ExceptionTranslationFilter.insufficientAuthentication",
"Full authentication is required to access this resource")));
} else {
logger.debug(
"Access is denied (user is not anonymous); delegating to AccessDeniedHandler",
exception);
// 权限不足
// 发送 403 错误
accessDeniedHandler.handle(request, response, (AccessDeniedException) exception);
}
}
}
4、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 {
if ((fi.getRequest() != null)
&& (fi.getRequest().getAttribute(FILTER_APPLIED) != null)
&& observeOncePerRequest) {
// filter already applied to this request and user wants us to observe
// once-per-request handling, so don't re-do security checking
// 过滤器已应用于此请求(重试请求),并且用户希望我们观察每个请求一次的处理,因此不要重新进行安全检查
fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
}
else {
// first time this request being called, so perform security checking
// 第一次调用此请求,因此执行安全检查
if (fi.getRequest() != null && observeOncePerRequest) {
// 给请求设置 FILTER_APPLIED,如果发生重试,就不需要校验权限了
fi.getRequest().setAttribute(FILTER_APPLIED, Boolean.TRUE);
}
// FIXME 主要功能就是判断认证成功的用户是否有权限访问接口
// 如果是没有权限,那么会抛出无权限访问异常,在 ExceptionTranslationFilter 会专门处理该异常:返回登录页面或 403
InterceptorStatusToken token = super.beforeInvocation(fi);
try {
// 调用接口的方法
fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
}
finally {
super.finallyInvocation(token);
}
super.afterInvocation(token, null);
}
}
protected InterceptorStatusToken beforeInvocation(Object object) {
Assert.notNull(object, "Object was null");
final boolean debug = logger.isDebugEnabled();
if (!getSecureObjectClass().isAssignableFrom(object.getClass())) {
throw new IllegalArgumentException(
"Security invocation attempted for object "
+ object.getClass().getName()
+ " but AbstractSecurityInterceptor only configured to support secure objects of type: "
+ getSecureObjectClass());
}
// 1、获取访问接口的客户端存在的权限信息:read,write
// 通常该方法和 antMatchers() 和 access() 搭配使用,目的是拦截 url,需要请求的客户端需要存在的权限
// // authorizeRequests() 开启 url 拦截权限功能
// .authorizeRequests()
// // 指定哪些路径需要验证
// .antMatchers("/**")
// // 指定路径需要的权限(通用,同时满足hasAnyScope、hasAnyRole、hasAnyAuthority)
// .access("#oauth2.hasAnyScope('read,write')")
// // 指定哪些路径需要验证
// .antMatchers("/**")
// .hasAnyRole("read","write")
// // 指定哪些路径需要验证
// .antMatchers("/**")
// .hasAnyAuthority("read,write")
// SecurityMetadataSource:DefaultFilterInvocationSecurityMetadataSource
// 或者是 Controller 上的权限,需要注解 @EnableGlobalMethodSecurity 开启
// prePostEnabled = true,就可以开启以下:@PreAuthorize("hasAnyAuthority('ADMIN')")、@PreAuthorize("hasAnyRole('ADMIN')") 或 @PreAuthorize("hasAnyRole('RILE_ADMIN')")
Collection<ConfigAttribute> attributes = this.obtainSecurityMetadataSource().getAttributes(object);
// 要访问的接口没有加权限,直接返回
if (attributes == null || attributes.isEmpty()) {
if (rejectPublicInvocations) {
throw new IllegalArgumentException(
"Secure object invocation "
+ object
+ " was denied as public invocations are not allowed via this interceptor. "
+ "This indicates a configuration error because the "
+ "rejectPublicInvocations property is set to 'true'");
}
if (debug) {
logger.debug("Public object - authentication not attempted");
}
publishEvent(new PublicInvocationEvent(object));
return null; // no further work post-invocation
}
if (debug) {
logger.debug("Secure object: " + object + "; Attributes: " + attributes);
}
if (SecurityContextHolder.getContext().getAuthentication() == null) {
credentialsNotFound(messages.getMessage(
"AbstractSecurityInterceptor.authenticationNotFound",
"An Authentication object was not found in the SecurityContext"),
object, attributes);
}
// 2、获取当前访问客户端的存在的权限
Authentication authenticated = authenticateIfRequired();
// Attempt authorization
try {
// 3、默认调用 AffirmativeBased.decide() 方法, 其内部使用 AccessDecisionVoter 对象进行投票机制判权,判权失败直接抛出 AccessDeniedException 异常
this.accessDecisionManager.decide(authenticated, object, attributes);
}
catch (AccessDeniedException accessDeniedException) {
publishEvent(new AuthorizationFailureEvent(object, attributes, authenticated, accessDeniedException));
throw accessDeniedException;
}
if (debug) {
logger.debug("Authorization successful");
}
if (publishAuthorizationSuccess) {
publishEvent(new AuthorizedEvent(object, attributes, authenticated));
}
// Attempt to run as a different user
Authentication runAs = this.runAsManager.buildRunAs(authenticated, object,
attributes);
if (runAs == null) {
if (debug) {
logger.debug("RunAsManager did not change Authentication object");
}
// no further work post-invocation
return new InterceptorStatusToken(SecurityContextHolder.getContext(), false,
attributes, object);
}
else {
if (debug) {
logger.debug("Switching to RunAs Authentication: " + runAs);
}
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);
}
}
了解了以上过滤器,就基本明白了 spring security 的工作原理。
3、资源服务器的示例
spring security oauth2.0 中,标明资源服务器和认证服务器几乎一致,只是把注解 @EnableAuthorizationServer 改为 @EnableResourceServer。
但是使用 @EnableResourceServer 需要注意的是,一定要设置一种认证拦截方式,如:
.authorizeRequests().antMatchers("/**").permitAll();
因为在 AbstractConfiguredSecurityBuilder 的 doBuild() 的 configure(); 才会执行 @EnableResourceServer 引入的 ResourceServerConfiguration 在configure() 方法中构建ResourceServerSecurityConfigurer,而这个 Configure 又会尝试增加 ExpressionUrlAuthorizationConfigurer,这个时候的状态已经是 BuildState.CONFIGURING,不允许新增 Configurer 了,所以启动就抛出异常了。
资源服务器不再是继承 WebSecurityConfigurerAdapter,因为 ResourceServerConfiguration 就继承了 WebSecurityConfigurerAdapter。
@Override
protected void configure(HttpSecurity http) throws Exception {
// 构建 ResourceServerSecurityConfigurer
ResourceServerSecurityConfigurer resources = new ResourceServerSecurityConfigurer();
ResourceServerTokenServices services = resolveTokenServices();
if (services != null) {
resources.tokenServices(services);
}
else {
if (tokenStore != null) {
resources.tokenStore(tokenStore);
}
else if (endpoints != null) {
resources.tokenStore(endpoints.getEndpointsConfigurer().getTokenStore());
}
}
if (eventPublisher != null) {
resources.eventPublisher(eventPublisher);
}
// FIXME 添加资源的安全配置。
for (ResourceServerConfigurer configurer : configurers) {
configurer.configure(resources);
}
// @formatter:off
http.authenticationProvider(new AnonymousAuthenticationProvider("default"))
// N.B. exceptionHandling is duplicated in resources.configure() so that
// it works
.exceptionHandling()
.accessDeniedHandler(resources.getAccessDeniedHandler()).and()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
.csrf().disable();
// @formatter:on
// 这里将 ResourceServerSecurityConfigurer 添加到 HttpSecurity
http.apply(resources);
if (endpoints != null) {
// Assume we are in an Authorization Server
http.requestMatcher(new NotOAuthRequestMatcher(endpoints.oauth2EndpointHandlerMapping()));
}
// FIXME 添加 HTTP 的安全配置。
for (ResourceServerConfigurer configurer : configurers) {
// Delegates can add authorizeRequests() here
configurer.configure(http);
}
if (configurers.isEmpty()) {
// Add anyRequest() last as a fall back. Spring Security would
// replace an existing anyRequest() matcher with this one, so to
// avoid that we only add it if the user hasn't configured anything.
http.authorizeRequests().anyRequest().authenticated();
}
}
可以看到 ResourceServerConfigurer.configure(resources); 和 ResourceServerConfigurer.configure(http); 这两处都需要重写资源服务器的配置,所以我们继承接口的子类 ResourceServerConfigurerAdapter。
@Configuration
@EnableResourceServer
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {
@Autowired
private TokenStore tokenStore;
@Override
public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
resources.resourceId("res1") // 标记,以指示这些资源需要认证,默认 oauth2-resource ,设置为 null ,表示都不需要认证
.tokenStore(tokenStore)
.stateless(true);
}
@Override
public void configure(HttpSecurity http) throws Exception {
http.csrf().disable().cors().disable()
// 开启url 拦截权限功能
.authorizeRequests()
// 指定哪些路径需要验证
.antMatchers("/**")
//.antMatchers("/**")
.permitAll();
}
}
继续看源码,上面构建了一个 ResourceServerSecurityConfigurer,实现了 SecurityConfigurer,在 AbstractConfiguredSecurityBuilder 的 doBuild() 的 configure(); 调用,
@Override
public void configure(HttpSecurity http) throws Exception {
// 1、创建 OAuth2AuthenticationManager 对象
AuthenticationManager oauthAuthenticationManager = oauthAuthenticationManager(http);
// 2、FIXME 创建 OAuth2AuthenticationProcessingFilter 过滤器
resourcesServerFilter = new OAuth2AuthenticationProcessingFilter();
resourcesServerFilter.setAuthenticationEntryPoint(authenticationEntryPoint);
// 设置认证管理器
resourcesServerFilter.setAuthenticationManager(oauthAuthenticationManager);
if (eventPublisher != null) {
resourcesServerFilter.setAuthenticationEventPublisher(eventPublisher);
}
if (tokenExtractor != null) {
resourcesServerFilter.setTokenExtractor(tokenExtractor);
}
resourcesServerFilter = postProcess(resourcesServerFilter);
resourcesServerFilter.setStateless(stateless);
// @formatter:off
http
// authorizeRequests() 会尝试添加 ExpressionUrlAuthorizationConfigurer,添加就是抛出异常。
.authorizeRequests()
.expressionHandler(expressionHandler)
.and()
// 3、 将 OAuth2AuthenticationProcessingFilter 过滤器加载到过滤器链上
.addFilterBefore(resourcesServerFilter, AbstractPreAuthenticatedProcessingFilter.class)
.exceptionHandling()
.accessDeniedHandler(accessDeniedHandler)
.authenticationEntryPoint(authenticationEntryPoint);
// @formatter:on
}
在 AbstractPreAuthenticatedProcessingFilter 前面加了一个包含 AuthenticationManager 的过滤器OAuth2AuthenticationProcessingFilter,其实这里只是使用AbstractPreAuthenticatedProcessingFilter 的排序-1(1200-1),刚好在认证服务器过滤器链的认证过滤器位置(Basic、UsernamePassword),这里用来验证 token。
// OAuth2AuthenticationProcessingFilter 的 doFilter()
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException,
ServletException {
final boolean debug = logger.isDebugEnabled();
final HttpServletRequest request = (HttpServletRequest) req;
final HttpServletResponse response = (HttpServletResponse) res;
try {
// 1、 调用 tokenExtractor.extract() 方法,从请求中解析出 token 信息并存放到 authentication 的 principal 字段中
Authentication authentication = tokenExtractor.extract(request);
if (authentication == null) {// 没有认证信息
// 无状态(stateless = false,表示不拦截该资源,不需要认证)
// 或上下文已存在认证信息
if (stateless && isAuthenticated()) {
if (debug) {
logger.debug("Clearing security context.");
}
SecurityContextHolder.clearContext();
}
if (debug) {
logger.debug("No token in request, will continue chain.");
}
}
else {
request.setAttribute(OAuth2AuthenticationDetails.ACCESS_TOKEN_VALUE, authentication.getPrincipal());
if (authentication instanceof AbstractAuthenticationToken) {
AbstractAuthenticationToken needsDetails = (AbstractAuthenticationToken) authentication;
// 认证详情
needsDetails.setDetails(authenticationDetailsSource.buildDetails(request));
}
// 2、 调用 authenticationManager.authenticate() 进行认证
// 注意此时的 authenticationManager 是 OAuth2AuthenticationManager
Authentication authResult = authenticationManager.authenticate(authentication);
if (debug) {
logger.debug("Authentication success: " + authResult);
}
eventPublisher.publishAuthenticationSuccess(authResult);
SecurityContextHolder.getContext().setAuthentication(authResult);
}
}
catch (OAuth2Exception failed) {
SecurityContextHolder.clearContext();
if (debug) {
logger.debug("Authentication request failed: " + failed);
}
eventPublisher.publishAuthenticationFailure(new BadCredentialsException(failed.getMessage(), failed),
new PreAuthenticatedAuthenticationToken("access-token", "N/A"));
authenticationEntryPoint.commence(request, response,
new InsufficientAuthenticationException(failed.getMessage(), failed));
return;
}
chain.doFilter(request, response);
}
// OAuth2AuthenticationManager 的 authenticate()
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
if (authentication == null) {
throw new InvalidTokenException("Invalid token (token not found)");
}
// 1、从 authentication 中获取 access_token
String token = (String) authentication.getPrincipal();
// 2、调用 tokenServices.loadAuthentication() 方法,将 jwt 格式的 access_token 还原回来,这里的 tokenServices 就是我们资源服务器配置的。
OAuth2Authentication auth = tokenServices.loadAuthentication(token);
if (auth == null) {
throw new InvalidTokenException("Invalid token: " + token);
}
// 取出数据库中配置的表 oauth_client_details 字段 resource_ids 存储的资源ID
// 是否包含当前项目配置的 resourceId,默认值是 oauth2-resource
// FIXME 如果当前项目将 resourceId 设置为 null,就不需要匹配。
Collection<String> resourceIds = auth.getOAuth2Request().getResourceIds();
if (resourceId != null && resourceIds != null && !resourceIds.isEmpty() && !resourceIds.contains(resourceId)) {
throw new OAuth2AccessDeniedException("Invalid token does not contain resource id (" + resourceId + ")");
}
// 3、检测客户端信息
// 如果资源服务器没有配置 ClientDetailsService 的实现,就不需要校验;
// 如果要校验,并且 ClientDetailsService 实现是 JDBC ,那么需要连接同一个数据库,但是微服务规范是每一个服务都有独有的数据库,所以不合规范,
// 如果实现是 REDIS 等,从规范上来看还可以。
// 假设就是使用 JDBC,开始校验
checkClientDetails(auth);
if (authentication.getDetails() instanceof OAuth2AuthenticationDetails) {
OAuth2AuthenticationDetails details = (OAuth2AuthenticationDetails) authentication.getDetails();
// Guard against a cached copy of the same details
if (!details.equals(auth.getDetails())) {
// Preserve the authentication details from the one loaded by token services
details.setDecodedDetails(auth.getDetails());
}
}
// 4、设置认证成功标识并返回
auth.setDetails(authentication.getDetails());
auth.setAuthenticated(true);
return auth;
}
private void checkClientDetails(OAuth2Authentication auth) {
if (clientDetailsService != null) {
ClientDetails client;
try {
// 取出存储的配置的 客户端信息
client = clientDetailsService.loadClientByClientId(auth.getOAuth2Request().getClientId());
}
catch (ClientRegistrationException e) {
throw new OAuth2AccessDeniedException("Invalid token contains invalid client id");
}
// 对比 scope:可以配置多个scope,同时也可以在请求的时候携带多个,如果请求没携带,那么就不校验
Set<String> allowed = client.getScope();
for (String scope : auth.getOAuth2Request().getScope()) {
if (!allowed.contains(scope)) {
throw new OAuth2AccessDeniedException(
"Invalid token contains disallowed scope (" + scope + ") for this client");
}
}
}
}
至此,源码逻辑大概清晰了。
分享一个在线绘制流程图和思维导图的工具,通过链接注册有3天会员奖励。
https://www.processon.com/i/62ea16d876213176f6db97ee?full_name=java-spring-mybatis-redis