Spring Security 核心配置解读 篇三

/ SpringSecurity / 没有评论 / 3566浏览

核心配置解读

配置介绍

回顾篇二中的 Security 安全核心配置

@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .authorizeRequests()
                .antMatchers("/", "/home").permitAll()
                .anyRequest().authenticated()
                .and()
            .formLogin()
                .loginPage("/login").permitAll()
                .and()
            .logout()
                .permitAll();
    }

    @Autowired
    public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
        auth
            .inMemoryAuthentication()
                .withUser("admin").password("admin").roles("USER");
    }
}

当配置了上述的javaconfig之后,我们的应用便具备了如下的功能:

一个 Restful 配置

动态从数据库加载权限,这边因为 Spring Security 的机制是在模块启动的时候进行加载的,如果想要动态 reload 权限,调查来看Spring 并没有提供相关的接口,需要动态 reload Spring 相关的 bean 是一种比较危险暴力的做法,需要多加注意。

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        List<String> permitAllEndpointList = Arrays.asList(AUTHENTICATION_URL,"/api/*/webjars/**","/api/*/swagger**","/api/*/swagger-resources/**","/api/*/v2/api-docs");
        //load Authorities from DB
        ResponseData<List<Authority>> authorityRules = authorityService.getAuthority();
        
        try {
            for (AuthorityModel rule : authorityRules.getData()) {
                //这里因为 Restful API url 有相同的情况,因此需要URL和方法名组合来区分,注意一种特殊情况,
                //因为是使用Ant语法来匹配URL,如果出现请求方法相同且URL是匹配子集关系时,要把最具体的URL放在
                //前面,比如同是 GET 方法,Api1:/api/user/paged 和 Api2:/api/user/{id},在Ant语法里面
                //Api2是能匹配Api1的,如果用户有Api2的权限而没有Api1的权限,在如下初始化权限时Api2初始化的
                //顺序在Api1的前面,就会导致用户即使没有Api1的权限也能访问,因此要确保数据库加载的时候Api1在Api2
                //前面
                if (HttpMethod.POST.name().equals(rule.getMethod().name())) {
                    http
                            .authorizeRequests()
                            .antMatchers(HttpMethod.POST, rule.getPattern()).hasAuthority(rule.getSystemName());
                } else if (HttpMethod.GET.name().equals(rule.getMethod().name())) {
                    http
                            .authorizeRequests()
                            .antMatchers(HttpMethod.GET, rule.getPattern()).hasAuthority(rule.getSystemName());
                } else if (HttpMethod.PUT.name().equals(rule.getMethod().name())) {
                    http
                            .authorizeRequests()
                            .antMatchers(HttpMethod.PUT, rule.getPattern()).hasAuthority(rule.getSystemName());
                } else if (HttpMethod.DELETE.name().equals(rule.getMethod().name())) {
                    http
                            .authorizeRequests()
                            .antMatchers(HttpMethod.DELETE, rule.getPattern()).hasAuthority(rule.getSystemName());
                }
            }
        } catch (Exception e) {
            log.error("", e);
        }

        http
            .csrf().disable() // We don't need CSRF for JWT based authentication
            .exceptionHandling().authenticationEntryPoint(this.authenticationEntryPoint)
                //自定义没有权限时的处理,一般根据业务封装自定义的返回结果
                .accessDeniedHandler(ajaxAccessDeniedHandler)

            .and()
                //Restful API 完全无状态,使用JWT token
                .sessionManagement()
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
            .and()
                //定义哪些API不需要认证,比如获取 Token 的接口
                .authorizeRequests()
                .antMatchers(permitAllEndpointList.toArray(new String[permitAllEndpointList.size()])).permitAll()
            .and()
                // Protected API End-points
                .authorizeRequests()
                .antMatchers(API_ROOT_URL, API_ATTACHMENT_URL).authenticated()
            .and()
                //处理跨域过滤器
                .addFilterBefore(new CustomCorsFilter(), UsernamePasswordAuthenticationFilter.class)
                //处理登录获取token的过滤器
                .addFilterBefore(buildAjaxLoginProcessingFilter(AUTHENTICATION_URL), UsernamePasswordAuthenticationFilter.class)
                //验证用户token,以及用户是否有接口访问权限的过滤器
                .addFilterBefore(buildJwtTokenAuthenticationProcessingFilter(permitAllEndpointList, Arrays.asList(API_ROOT_URL,API_ATTACHMENT_URL)), UsernamePasswordAuthenticationFilter.class);
    }

具体解释见代码上的注释

@EnableWebSecurity

我们自己定义的配置类 WebSecurityConfig 加上了 @EnableWebSecurity 注解,同时继承了 WebSecurityConfigurerAdapter。你可能会在想谁的作用大一点,毫无疑问 @EnableWebSecurity 起到决定性的配置作用,它其实是个组合注解。

@Retention(value = java.lang.annotation.RetentionPolicy.RUNTIME)
@Target(value = { java.lang.annotation.ElementType.TYPE })
@Documented
@Import({ WebSecurityConfiguration.class,// <2>
		SpringWebMvcImportSelector.class })// <1>
@EnableGlobalAuthentication // <3>
@Configuration
public @interface EnableWebSecurity {
	boolean debug() default false;
}
@Import(AuthenticationConfiguration.class)
@Configuration
public @interface EnableGlobalAuthentication {
}

注意点同样在 @Import 之中,它实际上激活了 AuthenticationConfiguration 这样的一个配置类,用来配置认证相关的核心类。

也就是说:@EnableWebSecurity完成的工作便是加载了WebSecurityConfigurationAuthenticationConfiguration 这两个核心配置类,也就此将 spring security 的职责划分为了配置安全信息,配置认证信息两部分。

WebSecurityConfiguration

在这个配置类中,有一个非常重要的Bean被注册了。

@Configuration
public class WebSecurityConfiguration {
	//DEFAULT_FILTER_NAME = "springSecurityFilterChain"
	@Bean(name = AbstractSecurityWebApplicationInitializer.DEFAULT_FILTER_NAME)
    public Filter springSecurityFilterChain() throws Exception {
    	...
    }
 }

在未使用springboot之前,大多数人都应该对springSecurityFilterChain这个名词不会陌生,他是spring security的核心过滤器,是整个认证的入口。在曾经的XML配置中,想要启用spring security,需要在web.xml中进行如下配置:


<!-- Spring Security -->
   <filter>
       <filter-name>springSecurityFilterChain</filter-name>
       <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
   </filter>

   <filter-mapping>
       <filter-name>springSecurityFilterChain</filter-name>
       <url-pattern>/*</url-pattern>
   </filter-mapping>

而在springboot集成之后,这样的XMLjava配置取代。WebSecurityConfiguration中完成了声明springSecurityFilterChain的作用,并且最终交给DelegatingFilterProxy这个代理类,负责拦截请求(注意DelegatingFilterProxy这个类不是spring security包中的,而是存在于web包中,spring使用了代理模式来实现安全过滤的解耦)。

AuthenticationConfiguration

@Configuration
@Import(ObjectPostProcessorConfiguration.class)
public class AuthenticationConfiguration {
  	@Bean
	public AuthenticationManagerBuilder authenticationManagerBuilder(
			ObjectPostProcessor<Object> objectPostProcessor) {
		return new AuthenticationManagerBuilder(objectPostProcessor);
	}
  	public AuthenticationManager getAuthenticationManager() throws Exception {
    	...
    }
}

AuthenticationConfiguration 的主要任务,便是负责生成全局的身份认证管理者 AuthenticationManager。还记得在《初识 Spring Security 篇一》中,介绍了 Spring Security 的认证体系,AuthenticationManager 便是最核心的身份认证管理器。

WebSecurityConfigurerAdapter

适配器模式在 spring 中被广泛的使用,在配置中使用 Adapter 的好处便是,我们可以选择性的配置想要修改的那一部分配置,而不用覆盖其他不相关的配置。WebSecurityConfigurerAdapter 中我们可以选择自己想要修改的内容,来进行重写,而其提供了三个 configure 重载方法,是我们主要关心的:

    /**WebSecurityConfigurerAdapter**/

	protected void configure(AuthenticationManagerBuilder auth) throws Exception {
		this.disableLocalConfigureAuthenticationBldr = true;
	}
    ...
    	/**
	 * Override this method to configure {@link WebSecurity}. For example, if you wish to
	 * ignore certain requests.
	 */
	public void configure(WebSecurity web) throws Exception {
	}
	/**
	 * Override this method to configure the {@link HttpSecurity}. Typically subclasses
	 * should not invoke this method by calling super as it may override their
	 * configuration. The default configuration is:
	 *
	 * <pre>
	 * http.authorizeRequests().anyRequest().authenticated().and().formLogin().and().httpBasic();
	 * </pre>
	 *
	 * @param http the {@link HttpSecurity} to modify
	 * @throws Exception if an error occurs
	 */
	// @formatter:off
	protected void configure(HttpSecurity http) throws Exception {
		logger.debug("Using default configure(HttpSecurity). If subclassed this will potentially override subclass configure(HttpSecurity).");

		http
			.authorizeRequests()
				.anyRequest().authenticated()
				.and()
			.formLogin().and()
			.httpBasic();
	}

由参数就可以知道,分别是对 AuthenticationManagerBuilderWebSecurityHttpSecurity进行个性化的配置。

HttpSecurity常用配置

@Configuration
@EnableWebSecurity
public class CustomWebSecurityConfig extends WebSecurityConfigurerAdapter {
  
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .authorizeRequests()
                .antMatchers("/resources/**", "/signup", "/about").permitAll()
                .antMatchers("/admin/**").hasRole("ADMIN")
                .antMatchers("/db/**").access("hasRole('ADMIN') and hasRole('DBA')")
                .anyRequest().authenticated()
                .and()
            .formLogin()
                .usernameParameter("username")
                .passwordParameter("password")
                .failureForwardUrl("/login?error")
                .loginPage("/login")
                .permitAll()
                .and()
            .logout()
                .logoutUrl("/logout")
                .logoutSuccessUrl("/index")
                .permitAll()
                .and()
            .httpBasic()
                .disable();
    }
}

上述是一个使用 Java Configuration 配置 HttpSecurity 的典型配置,其中 http 作为根开始配置,每一个 and() 对应了一个模块的配置(等同于xml配置中的结束标签),并且 and() 返回了 HttpSecurity 本身,于是可以连续进行配置。他们配置的含义也非常容易通过变量本身来推测

他们分别代表了 http 请求相关的安全配置,这些配置项无一例外的返回了 Configurer 类,而所有的 http 相关配置可以通过查看HttpSecurity的主要方法得知:

	public LogoutConfigurer<HttpSecurity> logout() throws Exception {
		return getOrApply(new LogoutConfigurer<HttpSecurity>());
	}
    ......
    	public CsrfConfigurer<HttpSecurity> csrf() throws Exception {
		ApplicationContext context = getContext();
		return getOrApply(new CsrfConfigurer<HttpSecurity>(context));
	}

需要对 http 协议有一定的了解才能完全掌握所有的配置,不过,springbootspring security的自动配置已经足够使用了。其中每一项 Configurer(e.g.FormLoginConfigurer,CsrfConfigurer)都是 HttpConfigurer 的细化配置项。

WebSecurityBuilder

@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    public void configure(WebSecurity web) throws Exception {
        web
            .ignoring()
            .antMatchers("/resources/**");
    }
}

以笔者的经验,这个配置中并不会出现太多的配置信息。

AuthenticationManagerBuilder

@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth
            .inMemoryAuthentication()
            .withUser("admin").password("admin").roles("USER");
    }
}

想要在 WebSecurityConfigurerAdapter 中进行认证相关的配置,可以使用 configure(AuthenticationManagerBuilder auth) 暴露一个 AuthenticationManager 的建造器:AuthenticationManagerBuilder 。如上所示,我们便完成了内存中用户的配置。

细心的朋友会发现,在前面的文章中我们配置内存中的用户时,似乎不是这么配置的,而是:

@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    @Autowired
    public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
        auth
            .inMemoryAuthentication()
                .withUser("admin").password("admin").roles("USER");
    }
}

如果你的应用只有唯一一个 WebSecurityConfigurerAdapter ,那么他们之间的差距可以被忽略,从方法名可以看出两者的区别:使用@Autowired注入的 AuthenticationManagerBuilder 是全局的身份认证器,作用域可以跨越多个WebSecurityConfigurerAdapter,以及影响到基于Method的安全控制;而 protected configure()的方式则类似于一个匿名内部类,它的作用域局限于一个WebSecurityConfigurerAdapter内部。关于这一点的区别,可以参考我曾经提出的issuespring-security#issues4571。官方文档中,也给出了配置多个WebSecurityConfigurerAdapter的场景以及demo,将在该系列的后续文章中解读

原文链接:https://www.cnkirito.moe/2017/09/20/spring-security-3/