1. 程式人生 > >springboot情操陶冶-web配置(八)

springboot情操陶冶-web配置(八)

本文關注應用的安全方面,涉及校驗以及授權方面,以springboot自帶的security板塊作為講解的內容

例項

建議使用者可直接路由至博主的先前部落格spring security整合cas方案。本文則針對相關的原始碼作下簡單的分析,方便筆者以及讀者更深入的瞭解spring的security板塊

@EnableWebSecurity

這個註解很精髓,基本上可以作為security的入口,筆者貼一下它的原始碼

@Retention(value = java.lang.annotation.RetentionPolicy.RUNTIME)
@Target(value = { java.lang.annotation.ElementType.TYPE })
@Documented
@Import({ WebSecurityConfiguration.class,
        SpringWebMvcImportSelector.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;
}

可以分為三個部分來分析,
SpringWebMvcImportSelector-支援mvc的引數安全校驗,替代了@EnableWebMvcSecurity註解
WebSecurityConfiguration-Web的安全配置
@EnableGlobalAuthentication-支援公共的認證校驗

SpringWebMvcImportSelector

首先先看下其如何整合mvc的安全校驗,其是一個ImportSelector介面,觀察下其複寫的方法

public String[] selectImports(AnnotationMetadata importingClassMetadata) {
        boolean webmvcPresent = ClassUtils.isPresent(
                "org.springframework.web.servlet.DispatcherServlet",
                getClass().getClassLoader());
        return webmvcPresent
                ? new String[] {
                        "org.springframework.security.config.annotation.web.configuration.WebMvcSecurityConfiguration" }
                : new String[] {};
    }

由上述程式碼可知,在classpath環境中存在mvc的關鍵類DispatcherServlet時便會引入WebMvcSecurityConfiguration類,那麼此類又配置了什麼東西呢?
裡面的程式碼很簡單,但關鍵是其是WebMvcConfigurer介面的實現類,根據之前的文章提到,該介面主要是用於配置MVC的相關功能,比如引數處理器、返回值處理器、異常處理器等等。

而該類只是擴充套件了相應的引數處理器,我們可以看下原始碼

    @Override
    @SuppressWarnings("deprecation")
    public void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) {
        // 支援@AuthenticationPrinciple引數註解校驗
        AuthenticationPrincipalArgumentResolver authenticationPrincipalResolver = new AuthenticationPrincipalArgumentResolver();
        authenticationPrincipalResolver.setBeanResolver(beanResolver);
        argumentResolvers.add(authenticationPrincipalResolver);
        // 廢棄
        argumentResolvers
                .add(new org.springframework.security.web.bind.support.AuthenticationPrincipalArgumentResolver());
        // csrf token引數
        argumentResolvers.add(new CsrfTokenArgumentResolver());
    }

針對@AuthenticationPrinciple註解的引數校驗,本文不展開了,這裡作下歸納

  1. 帶有@AuthenticationPrinciple註解的引數其值會從SecurityContext的上下文讀取相應的Authentication校驗資訊
  2. 有一個要求,被該註解修飾的引數須同SecurityContext的上下文存放的Authentication資訊為同一介面,否則則會返回null。如果設定了errorOnInvalidType屬性為true,則會拋異常
  3. 綜上所述,該註解主要是方便將校驗通過的Token用於引數賦值,其它的作用也不是很大

@EnableGlobalAuthentication

再來分析下springboot-security的公共認證校驗是什麼概念,貼下原始碼

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

OK,直接進入相應的AuthenticationConfiguration類進行具體的分析


1.其引入了ObjectPostProcessorConfiguration配置用於建立AutowireBeanFactoryObjectPostProcessor類,作用應該是通過Spring上下文例項相應的實體類並註冊到bean工廠中

    @Bean
    public ObjectPostProcessor<Object> objectPostProcessor(
            AutowireCapableBeanFactory beanFactory) {
        return new AutowireBeanFactoryObjectPostProcessor(beanFactory);
    }

2.建立基於密碼機制的認證管理器Bean,型別為DefaultPasswordEncoderAuthenticationManagerBuilder

    @Bean
    public AuthenticationManagerBuilder authenticationManagerBuilder(
            ObjectPostProcessor<Object> objectPostProcessor, ApplicationContext context) {
        // 密碼加密器
        LazyPasswordEncoder defaultPasswordEncoder = new LazyPasswordEncoder(context);
        // 認證事件傳播器
        AuthenticationEventPublisher authenticationEventPublisher = getBeanOrNull(context, AuthenticationEventPublisher.class);
        // 預設的認證管理器
        DefaultPasswordEncoderAuthenticationManagerBuilder result = new DefaultPasswordEncoderAuthenticationManagerBuilder(objectPostProcessor, defaultPasswordEncoder);
        if (authenticationEventPublisher != null) {
            result.authenticationEventPublisher(authenticationEventPublisher);
        }
        return result;
    }

上述的密碼加密器支援多種方式的加密,比如bcrypt(預設)/ladp/md5/sha-1等,感興趣的讀者可自行閱讀。使用者也可多用此Bean作額外的擴充套件,例如官方建議的如下程式碼

@Configuration
@EnableGlobalAuthentication
public class MyGlobalAuthenticationConfiguration {

    @Autowired
    public void configureGlobal(AuthenticationManagerBuilder auth) {
        auth.inMemoryAuthentication().withUser("user").password("password").roles("USER")
                .and().withUser("admin").password("password").roles("ADMIN,USER");
    }
 }

3.建立基於UserDetails的認證器,用於管理使用者的授權資訊

    @Bean
    public static InitializeUserDetailsBeanManagerConfigurer initializeUserDetailsBeanManagerConfigurer(ApplicationContext context) {
        return new InitializeUserDetailsBeanManagerConfigurer(context);
    }

其會建立基於Datasource源的DaoAuthenticationProvider許可權校驗器,前提是ApplicationContext上下文存在UserDetailsServiceBean物件,否則會不建立。如果使用者想基於資料庫或者其他資料來源的可嘗試複寫UserDetailsService介面

@Configuration
public class DaoUserDetailsServiceConfig {

    /**
     * load user info by dao
     *
     * @see org.springframework.security.authentication.dao.DaoAuthenticationProvider
     */
    @Configuration
    public static class DefaultUserDetailsService implements UserDetailsService {

        private static final String DEFAULT_PASS = "defaultPass";

        // admin authority
        private Collection<? extends GrantedAuthority> adminAuthority;

        @Resource
        private PasswordEncoder defaultPasswordEncoder;

        public DefaultUserDetailsService() {
            SimpleGrantedAuthority authority = new SimpleGrantedAuthority("ROLE_ADMIN");
            List<GrantedAuthority> authorities = new ArrayList<>();
            authorities.add(authority);

            adminAuthority = Collections.unmodifiableList(authorities);
        }

        @Override
        public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
            User userdetails = new User(username, defaultPasswordEncoder.encode(DEFAULT_PASS), adminAuthority);

            return userdetails;
        }

        @Bean
        public PasswordEncoder daoPasswordEncoder() {
            PasswordEncoder passwordEncoder = new BCryptPasswordEncoder();

            return passwordEncoder;
        }
    }
}

4.建立AuthenticationProvider認證器,用於使用者資訊的校驗

    @Bean
    public static InitializeAuthenticationProviderBeanManagerConfigurer initializeAuthenticationProviderBeanManagerConfigurer(ApplicationContext context) {
        return new InitializeAuthenticationProviderBeanManagerConfigurer(context);
    }

同第三點,只是它就配置簡單的AuthenticationProvider至相應的AuthenticationManagerBuilderBean中

所以綜上所述,@EnableGlobalAuthentication註解的主要目的是配置認證管理器,裡面包含了加密器以及相應的認證器

WebSecurityConfiguration

web方面的安全配置,筆者也根據載入的順序來進行分析


1.獲取WebSecurityConfigurer介面bean集合的AutowiredWebSecurityConfigurersIgnoreParents

    @Bean
    public static AutowiredWebSecurityConfigurersIgnoreParents autowiredWebSecurityConfigurersIgnoreParents(
            ConfigurableListableBeanFactory beanFactory) {
        return new AutowiredWebSecurityConfigurersIgnoreParents(beanFactory);
    }

此Bean用於獲取所有註冊在bean工廠上的WebSecurityConfigurer介面,使用者也一般通過此介面的抽象類WebSecurityConfigurerAdapter來進行相應的擴充套件


2.設定Security的Filter過濾鏈配置,提前為建立過濾鏈作準備

    @Autowired(required = false)
    public void setFilterChainProxySecurityConfigurer(
            ObjectPostProcessor<Object> objectPostProcessor,
            @Value("#{@autowiredWebSecurityConfigurersIgnoreParents.getWebSecurityConfigurers()}") List<SecurityConfigurer<Filter, WebSecurity>> webSecurityConfigurers)
            throws Exception {
        // WebSecurity建立
        webSecurity = objectPostProcessor
                .postProcess(new WebSecurity(objectPostProcessor));
        if (debugEnabled != null) {
            webSecurity.debug(debugEnabled);
        }
        
        // 根據@Order屬性排序
        Collections.sort(webSecurityConfigurers, AnnotationAwareOrderComparator.INSTANCE);

        Integer previousOrder = null;
        Object previousConfig = null;
        // 校驗Order對應的值,不允許相同,否則會丟擲異常
        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;
        }
        // 對排序過的SecurityConfigurer依次放入WebSecurity物件中
        for (SecurityConfigurer<Filter, WebSecurity> webSecurityConfigurer : webSecurityConfigurers) {
            webSecurity.apply(webSecurityConfigurer);
        }
        this.webSecurityConfigurers = webSecurityConfigurers;
    }

這裡便提一下,我們在繼承WebSecurityConfigurerAdapter抽象類的時候,記得在其頭上加上@Order屬性,並且保證值唯一


3.建立Security過濾鏈

    @Bean(name = AbstractSecurityWebApplicationInitializer.DEFAULT_FILTER_NAME)
    public Filter springSecurityFilterChain() throws Exception {
        // 如果使用者沒有配置WebSecurityConfigurer介面,則建立一個空的
        boolean hasConfigurers = webSecurityConfigurers != null
                && !webSecurityConfigurers.isEmpty();
        if (!hasConfigurers) {
            WebSecurityConfigurerAdapter adapter = objectObjectPostProcessor
                    .postProcess(new WebSecurityConfigurerAdapter() {
                    });
            webSecurity.apply(adapter);
        }
        // create Filter
        return webSecurity.build();
    }

看來Filter攔截器的配置是通過WebSecurity這個類來完成的,限於裡面的程式碼過於複雜,本文就不展開了,感興趣的讀者可以重點關注下此類。由此可以得出Springboot的安全校驗是通過過濾鏈的設計方式來完成的


4.URI許可權校驗Bean,其依賴於第三點的配置

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

5.安全校驗表示式驗證Bean,其也依賴於第三點的配置,應該是與第四點搭配使用

    @Bean
    @DependsOn(AbstractSecurityWebApplicationInitializer.DEFAULT_FILTER_NAME)
    public SecurityExpressionHandler<FilterInvocation> webSecurityExpressionHandler() {
        return webSecurity.getExpressionHandler();
    }

小結

Springboot整合的Security板塊內容很多,本文也展示不完,不過值得關注的是以下幾個方面
1)WebSecurity的個性化配置類,一般是複寫抽象介面WebSecurityConfigurerAdapter,再加上@EnableWebSecurity註解便可

2)AuthenticationManagerBuilder認證校驗器,重點關注其中的密碼校驗器,用於密碼的加密解密,預設使用bcrypt方式。如果使用者想通過其他資料來源獲取使用者資訊,可以關注UserDetailsService介面

3)WebSecurity類,此類是Springboot Security模組的核心類,具體的過濾鏈配置均是由此類得到的。讀者以及筆者應該對此加以關注