Spring Security从过滤器到认证授权的源码分析

Spring Security从过滤器到认证授权的源码分析

​ Spring Security的实现包括认证(Authentication) 和 授权(Authorization)全部都是通过过滤器实现的,源码分析最后都会追寻到源头过滤器。

一、过滤器

1、WebSecurityConfigurerAdapter类

​ 一般情况下,实现认证我们需要继承WebSecurityConfigurerAdapter类,例如,下面的SecurityConfig是一个前后端分离的SpringSecurity配置类:

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

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

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

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

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

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

                // 不创建会话
                .and()
                .sessionManagement()
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS)

                .and()
                .authorizeRequests()
                // 静态资源等等
                .antMatchers(
                        HttpMethod.GET,
                        "/*.html",
                        "/**/*.html",
                        "/**/*.css",
                        "/**/*.js",
                        "/**/*.map","/**/*.ttf","/**/*.woff","/**/*.woff2",
                        "/**/*.ico"
                ).permitAll()
                // swagger 文档
                .antMatchers("/swagger-ui.html").permitAll()
                .antMatchers("/swagger-resources/**").permitAll()
                .antMatchers("/webjars/**").permitAll()
                .antMatchers("/*/api-docs").permitAll()
                // 文件
                .antMatchers("/avatar/**").permitAll()
                .antMatchers("/image/**").permitAll()
                // 阿里巴巴 druid
                .antMatchers("/druid/**").permitAll()
                // 报表
                .antMatchers("/ureport/**").permitAll()
                // 放行OPTIONS请求
                .antMatchers(HttpMethod.OPTIONS, "/**").permitAll()
                // 自定义匿名访问所有url放行 : 允许匿名和带权限以及登录用户访问
                .antMatchers(anonymousUrls.toArray(new String[0])).permitAll()
                // 所有请求都需要认证
                .anyRequest().authenticated();

        //跨域
        httpSecurity.addFilterBefore(corsFilter,UsernamePasswordAuthenticationFilter.class);
        //用于携带token的认证
        httpSecurity.addFilterBefore(jwtTokenFilter,UsernamePasswordAuthenticationFilter.class);
    }
}

WebSecurityConfigurerAdapter类是Spring提供的安全配置类的基础实现,通常情况我们都需要继承它,当然也可以自己实现WebSecurityConfigurer接口来自定义一个实现。

WebSecurityConfigurerAdapter类是我们必须掌握的。

​ 在配置的最后加入的jwtTokenFilter是应用中自定义的过滤器,作用是JWT令牌认证。也就是我们可以将自定义的过滤器嵌入到SpringSecurity过滤链中,而不是加入到普通的Servlet Filter链中。这个时候我们自己的过滤建议实现OncePerRequestFilter接口,避免过滤器被普通过滤链加载而重复执行,或者不要将过滤器加入到IOC容器中,而是使用如下new的方式:

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

2、Spring Security的过滤链

SpringSecurity的过滤器长什么样子?

它在Servlet过滤链中加入了代理,这个代理也是一个过滤器,但是其内部又实现一系列的过滤器。

原生过滤链:

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

其中springSecurityFilterChain是额外加入的过滤器,内部定义了Spring Security实现的过滤链:

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

3、过滤链里Spring Security过滤器的顺序

​ 过滤器的顺序有严格的要求,它是通过FilterComparator来实现的,代码片段如下:

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

FilterComparator是在HttpSecurity被初始化,并应用到各过滤器里。

4、SpringSecurity相关的过滤器是什么时候加入到过滤链中的?

​ Spring Security相关的过滤器通过HttpSecurityAddFilter方法加入到过滤链中,加入的时机是各种Security相关的Configurer初始化的时候。

​ 例如:跨域的过滤器是在CorsConfigurer里加入的。类似的配置类还有:AbstractInterceptUrlConfigurer中方法拦截过滤器,ExceptionHandlingConfigurer中的异常处理过滤器等。官方的SpringSecurity过滤器有几个可以查看FilterComparator

有的特殊的过滤器WebAsyncManagerIntegrationFilter是在WebSecurityConfigurerAdapter里加入的。

​ 这些Configurer初始化是在HttpSecurity中开始的:

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

最终在AbstractConfiguredSecurityBuilder中完成配置调用:

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

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

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

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

二、过滤器的入口--自动配置

​ Spring Security过滤器的自动配置是所有基本配置的源头,这些关联配置包括:SecurityAutoConfigurationSecurityFilterAutoConfiguration,后者依赖前者。

1、SecurityAutoConfiguration

​ 作用主要完成Security相关的基础配置导入,基本bean的生成注入。

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

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

}

主要内容:

1)导入SecurityProperties类,此类中定义认证和授权相关过滤器的顺序和缺省用户和密码等,该类是配置类,可以在application.yml通过spring.security进行配置。

2)注入SpringBootWebSecurityConfigurationWebSecurityEnablerConfigurationSecurityDataConfiguration三个配置。重点。

3)实例化DefaultAuthenticationEventPublisher,缺省认证事件发布器,这个暂时不是重点。

1.1、SpringBootWebSecurityConfiguration

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

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

	}

}

主要内容:

1)确保WebSecurityConfigurerAdapter类存在。

2)WebSecurityConfigurerAdapter还没有实例化注入容器。

3)必须是一个web应用,并且类型是Servlet

4)重点:如果满足上述条件,注入WebSecurityConfigurerAdapter类的bean作为缺省安全认证配置,并且重新指定其生效顺序为SecurityProperties.BASIC_AUTH_ORDER,这个顺序值如下:

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

如果没有这个@Order(SecurityProperties.BASIC_AUTH_ORDER)指定,缺省的WebSecurityConfigurerAdapter顺序为100。

注意:这里的@Order是配置类的加载顺序,不是过滤器的加载顺序,他们表现一样,但是要实现的最终目的是不一样的。配置类中,先加载的bean,如果是单例(大部分),那么可能后续相同的配置bean就无法加载,并且还有类似@ConditionalOnMissingBean这样的注解辅助,所以结论如下:

Spring Security缺省以很低的优先级(比它更低的优先级只有5个空位)加载了WebSecurityConfigurerAdapter配置。本文开头所说,一般我们需要继承实现WebSecurityConfigurerAdapter配置,那么我们自己应用缺省的加载顺序就是100,这样我们应用配置就优先加载,Spring Security的DefaultConfigurerAdapter配置就无法加载。

1.2、WebSecurityEnablerConfiguration

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

}

主要内容:

1)WebSecurityConfigurerAdapter类bean必须注入IOC。

2)springSecurityFilterChain过滤链的bean没有创建注入IOC容器。这个过滤链就是Spring Security的过滤链。

3)重点:@EnableWebSecurity,加载默认Web自动安全配置。这个注解我们一般在继承实现WebSecurityConfigurerAdapter类的时候都会加入,如果忘记加了,那么spring security初始化时就会自动加载缺省配置。

1.2.1 @EnableWebSecurity
@Retention(value = java.lang.annotation.RetentionPolicy.RUNTIME)
@Target(value = { java.lang.annotation.ElementType.TYPE })
@Documented
@Import({ WebSecurityConfiguration.class,
		SpringWebMvcImportSelector.class,
		OAuth2ImportSelector.class })
@EnableGlobalAuthentication
@Configuration
public @interface EnableWebSecurity {

	/**
	 * Controls debugging support for Spring Security. Default is false.
	 * @return if true, enables debug support with Spring Security
	 */
	boolean debug() default false;
}

主要内容:

1)加载WebSecurityConfiguration。如下精简代码:

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


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

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

里面有重要的三个bean:

webSecurityExpressionHandler 安全表达式处理器

springSecurityFilterChain Security过滤链,也就是整个Spring Security核心的过滤链bean在这里创建。

WebInvocationPrivilegeEvaluator web方法调用评估器

2)加载SpringWebMvcImportSelector。作用是加载DispatcherServlet类WebMvcSecurityConfiguration类,这两个类不属于主线任务。

3)加载OAuth2ImportSelector。作用是加载oAuth2、WebFlux相关的类,先略过。

4)加载@EnableGlobalAuthentication。重点。

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

主要导入AuthenticationConfiguration类,这个配置类主要注入了AuthenticationManagerBuilder,通过它,我们就可以拿到AuthenticationManager,然后进行自动或者手动的认证。

1.3、SecurityDataConfiguration

这个是与Spring Data的安全集成,略过。

2、SecurityFilterAutoConfiguration

​ Spring Security过滤链的配置与注册。

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

	private static final String DEFAULT_FILTER_NAME = AbstractSecurityWebApplicationInitializer.DEFAULT_FILTER_NAME;

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

    ... ...

}

主要内容:

1)导入SecurityProperties配置

2)确保在SecurityAutoConfiguration之后执行

3)注入DelegatingFilterProxyRegistrationBean,它就是springSecurityFilterChain过滤链的委托对象。在前面提到普通的Servlet filter过滤链中

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

springSecurityFilterChain是以代理方式实现,这是为了spring security内部需要走一遍自己的过滤器,在我之前的应用里是11个过滤器。

4)DelegatingFilterProxyRegistrationBean在生成的时候设置了过滤器的顺序为-100。

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

三、认证(Authentication)

1、认证接口

​ 用于认证的主要接口是AuthenticationManager,一般我们可以使用AuthenticationManagerBuilder获取它的默认实现,它只有一个方法:

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

一个 AuthenticationManagerauthenticate()方法中有三种情况:

  1. 返回 Authentication (authenticated=true),如果验证输入是合法的Principal)。
  2. 抛出AuthenticationException异常,如果输入不合法。
  3. 如果无法判断,则返回null

AuthenticationException是一个运行时异常,通常被应用程序以常规的方式的处理,这取决于应用目的和代码风格。换句话说,代码中一般不会捕捉和处理这个异常。比如,可以使得网页显示认证失败,后端返回 401 HTTP 状态码,响应头中的WWW-Authenticate 有无视情况而定。

AuthenticationManager最普遍的实现是ProviderManagerProviderManager将认证委托给一系列的AuthenticationProvider实例 。AuthenticationProviderAuthenticationManager 很类似,但是它有一个额外的方法允许查询它支持的Authentication方式:

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

	boolean supports(Class<?> authentication);
}

supports方法的Class authentication 参数其实是Class类型的。一个ProviderManager在一个应用中能支持多种不同的认证机制,通过将认证委托给一系列的AuthenticationProviderProviderManager没有识别出的认证类型,将会被忽略。

每个ProviderManager可以有一个父类,如果所有AuthenticationProvider都返回null,那么就交给父类去认证。如果父类也不可用,则抛出AuthenticationException异常。

有时应用的资源会有逻辑分组(比如所有网站资源都匹配URL/api/**),并且每个组都有自己的AuthenticationManager,通常是一个ProviderManager,它们之间有共同的父类认证器。那么父类就是一种全局资源,充当所有认证器的 fallback。

2、自定义AuthenticationManager

Spring Security 提供了一些配置方式帮助你快速的配置通用的AuthenticationManager。最常见的是AuthenticationManagerBuilder,它可以使用内存方式(in-memory)、JDBC 或 LDAP、或自定义的UserDetailService来认证用户。下面是设置全局认证器的例子:

@Configuration
public class ApplicationSecurity extends WebSecurityConfigurerAdapter {

   ... // web stuff here

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

}

虽然这个例子仅仅设计一个 web 应用,但是AuthenticationManagerBuilder的用处大为广阔(详细情况请看[Web 安全](#Web 安全)是如何实现的)。请注意AuthenticationManagerBuilder是通过@AutoWired注入到被@Bean注解的一个方法中的,这使得它成为一个全局AuthenticationManager。相反的,如果我们这样写:

@Configuration
public class ApplicationSecurity extends WebSecurityConfigurerAdapter {

  @Autowired
  DataSource dataSource;

   ... // web stuff here

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

}

重写configure(AuthenticationManagerBuilder builder)方法,那么AuthenticationManagerBuilder仅会构造一个“本地”的AuthenticationManager,只是全局认证器的一个子实现。在 Spring Boot 应用中你可以使用@Autowired注入全局的AuthenticationManager,但是你不能注入“本地”的,除非你自己公开暴露它。

Spring Boot 提供默认的全局AuthenticationManager,除非你提供自己的全局AuthenticationManager。不用担心,默认的已经足够安全了,除非你真的需要一个自定义的全局AuthenticationManager。一般的,你只需只用“本地”的AuthenticationManagerBuilder来配置,而不需要担心全局的。

3、认证与过滤器

​ 默认情况下,Spring Security是通过UsernamePasswordAuthenticationFilterattemptAuthentication方法实现认证的。它继承自AbstractAuthenticationProcessingFilter,这个过滤器在AbstractAuthenticationFilterConfigurer配置类中加入到过滤链中。在attemptAuthentication方法中通过AuthenticationManager接口实现进行认证。

		UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(
				username, password);

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

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

​ 在实际开发中,特别是前后端分离的项目中,大部分时候我们都需要自定义认证,例如:前后端分离+Jwt令牌认证改造,我们需要做三件事:

1)实现UserDetailsService接口,重写loadUserByUsername业务逻辑,重新封装返回的对象。

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

    @Resource
    private SysUserService userService;

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

JwtUserDtoUserDetails的自定义扩展。

2)实现一个自定义的过滤器JwtTokenFilter,建议实现OncePerRequestFilter接口,在doFilterInternal方法里实现对令牌的认证,如果有合法令牌,则设置SecurityContext,然后继续走过滤链。

 SecurityContextHolder.getContext().setAuthentication(authentication);

将过滤器加入到springSecurityFilterChain

httpSecurity.addFilterBefore(jwtTokenFilter,UsernamePasswordAuthenticationFilter.class);

注意:需要将WebSecurityConfigurerAdapter的实现中去掉formLogin()认证方式。这个时候过滤链中UsernamePasswordAuthenticationFilter过滤器将被删除。

3)手动调用认证,并手动设置SecurityContext,并生成令牌返回前端,下次前端访问带着令牌就会进入第二步的JwtTokenFilter进行令牌的认证和安全上下文的设置。精简代码如下:

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

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

        SecurityContextHolder.getContext().setAuthentication(authentication);

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

手动调用authenticationManagerBuilder.getObject().authenticate(authenticationToken),会通过DaoAuthenticationProvideradditionalAuthenticationChecks方法调用第一步的userDetailsService取出UserDetails对象进行密码验证。

四、授权(Authorization)

1、授权接口

​ 一旦认证成功,我们就可以进行授权了,它核心的策略就是AccessDecisionManager。它提供三个方法并且全部委托给AccessDecisionVoter,这有点像ProviderManager将认证委托给AuthenticationProvider

一个AccessDecisionVoter考虑一个Authentication(代表一个Principal)和一个被ConfigAttributes装饰的安全对象:

boolean supports(ConfigAttribute attribute);

boolean supports(Class<?> clazz);

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

AccessDecisionVoterAccessDecisionManager方法中的object参数是完全泛型化的,它代表任何用户想要访问(web 资源或 Java 方法是最常见的两种情况)。ConfigAttributes也是相当泛型化的,它表示一个被装饰的安全对象并带有访问权限级别的元数据。ConfigAttributes是一个接口,仅有一个返回String的方法,返回的字符串中包含资源所有者,解释了访问资源的规则。常见的ConfigAttributes是用户的角色(比如ROLE_ADMINROLE_AUDIT),它们通常有一定的格式(比如以ROLE_作为前缀)或者是可计算的表达式。

大部分人使用默认的AccessDecisionManager,即AffirmativeBased(如果没有 voters 返回那么该访问将被授权)。任何自定义的行为最好放在 voter 中,无论添加一个新的 voter 还是修改已有的 voter。

使用 Spring Expression Language(SpEL)表达式的ConfigAttributes是很常见的,比如isFullyAuthenticated() && hasRole('FOO')。解析表达式和加载表达式由AccessDecisionVoter实现。要扩展可处理的表达式的范围,需要自定义SecurityExpressionRoot,优势也需要SecurityExpressionHandler

2、Method 安全

Spring Security 在支持 web 安全的同时,也提供了对 Java 方法执行的访问规则。对于 Spring Security 来说,方法只是一种不同类型的“资源”而已。对用户来说,访问规则在ConfigAttribute中有相同的格式(比如 角色 或者 表达式),但在代码中有不同的配置。第一步就是启用方法安全,比如你可以在应用的启动类上进行配置:

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

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

@Service
public class MyService {

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

}

这个例子是一个有安全方法的服务。如果 Spring 创建了MyService Bean,那么它将被代理,调用者必须在方法调用之前通过一个安全拦截器。如果访问被拒绝,调用者会抛出一个AccessDeniedException而不是执行这个方法的结果。

还有其他可用于强制执行安全约束的方法注解,特别是@PreAuthorize@PostAuthorize, 它们允许你在其中写 SpEL 表达式并可以引用方法的参数和返回值。

提示:把 web 安全和方法安全放在一起并不突兀。过滤链提供了用户体验特性,比如认证和重定向到登录界面。而方法安全在更细粒度级别上提供了保护。

3、授权与过滤器

​ 在Spring Security过滤链中最后一个环节通过FilterSecurityInterceptor进行了方法拦截,对需要授权的方法进行验证。核心代码如下:

FilterSecurityInterceptor中的

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

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

拦截的具体实现在其父类AbstractSecurityInterceptor中完成。精简代码如下:

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

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

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

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

最终调用的是AccessDecisionManager的实现类(一般是AffirmativeBased)来进行授权,而AffirmativeBased将授权委托给AccessDecisionVoter接口。

4、项目中自定义授权

在实际项目中可能经常用到的自定义授权如下:


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

update方法需要用户带有user:update权限才能访问,应用通过@perm.check方法检查用户是否具备权限。

@perm是SpEL对IOC容器中的bean进行引用。

@PreAuthorize是Spring Security自带的权限校验注解。

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

注解中@PreAuthorize是授权的关键步骤,它是如何实现的?

实际上@preAuthorize的实现由接口AccessDecisionVoter的实现类PreInvocationAuthorizationAdviceVoter完成授权投票。

授权过程如下:

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

1)FilterSecurityInterceptor过滤器拦截方法doFilter,包装请求和过滤链。

2)invoke做一些简单校验,例如:是否带有@preAuthorize注解等,看是否要进入拦截流程,如果符合过滤条件,则调用父类的拦截方法super.beforeInvocation

3)AbstractSecurityInterceptor调用AccessDecisionManager接口的实现类AffirmativeBased的decide方法进行授权判断,如果失败则抛出accessDeniedException异常,流程结束。

4)由于授权可能有多种实现,AffirmativeBased将授权判断委托给AccessDecisionVoter接口。

5)根据授权类型,最终是AccessDecisionVoter的实现类PreInvocationAuthorizationAdviceVoter符合授权判断,通过preAdvice.before进行vote投票判断。

6)最终由表达式预调用处理器ExpressionBasedPreInvocationAdvice进行表达式计算,并返回BOOLEAN类型结果。

五、Spring Security 和线程

1、线程绑定

Spring Security是线程绑定的,因为它需要保证当前的已认证的用户(authenticated principal)对下流的消费者可用。基本构建块是SecurityContext,它可能包含Authentication(当一个用户登陆后,authenticated肯定是 true)。你总是可以从SecurityContextHolder中的静态方法得到SecurityContext,它内部使用了ThreadLocal进行管理。

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

这种操作并不常见,但是它可能对你有帮助。比如,你需要写一个自定义的认证过滤器(尽管如此,Spring Security 中还有一些基类可用于避免使用SecurityContextHolder的地方)。

如果需要访问 web endpoint 中经过身份验证的用户,则可以在@RequestMapping中使用方法参数注解。例如:

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

这个注解相当于从SecurityContext中获得当前Authentication,并调用getPrincipal()方法赋值给方法参数。Authentication中的Principal取决与用来认证的AuthenticationManager,所以这对于获得对用户数据类型的安全引用来说是一个有用的小技巧。

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

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

如果你需要编写在没有使用 Spring Security 的情况下的代码,那么这会很有用(你需要在加载Authentication类时更加谨慎)。

2、异步执行安全方法

因为SecurityContext是线程绑定的,所以如果你想在后台执行安全方法,比如使用@Async,你需要确保上下文的传递。这总结起来就是将SecurityContextRunnableCallable等包裹起来在后台执行。Spring Security 提供了一些帮助使之变得简单,比如RunnableCallable的包装器。 要将 SecurityContext 传递到@Async注解的方法,你需要编写 AsyncConfigurer 并确保 Executor 的正确性:

@Configuration
public class ApplicationConfiguration extends AsyncConfigurerSupport {

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

}
posted @ 2020-11-22 20:04  我是属车的  阅读(1618)  评论(0编辑  收藏  举报