• Spring Security(4):自定义配置


    接着上节的讲,在添加了@EnableWebSecurity注解后,如果需要自定义一些配置,则需要和继承WebSecurityConfigurerAdapter后,覆盖某些方法。

    我们来看一下WebSecurityConfigurerAdapter中哪些方法可以重写,需要重写。

    (1)WebSecurity

    默认是一个空方法,一般也不会再重写。

        public void configure(WebSecurity web) throws Exception { }

    (2)HttpSecurity

    默认的父类代码默认任何request都需要认证,使用默认的login page基于表单认证,使用HTTP基本认证。

        protected void configure(HttpSecurity http) throws Exception {
            http
                .authorizeRequests()
                    .anyRequest().authenticated()
                    .and()
                .formLogin().and()
                .httpBasic();
        }

    下面是一些自定义写法。

        @Override
        protected void configure(HttpSecurity http) throws Exception {
            //@formatter:off
            http.authorizeRequests()
                // all users have access to these urls
                .antMatchers("/resources/**", "/signup", "/about").permitAll()
                // Any URL that starts with "/admin/" will be restricted to users who have the role "ROLE_ADMIN"
                .antMatchers("/admin/**").hasRole("ADMIN")
                // Any URL that starts with "/db/" requires the user to have both "ROLE_ADMIN" and "ROLE_DBA"
                .antMatchers("/db/**").access("hasRole('ADMIN') and hasRole('DBA')")
                //  Any URL that starts with "/group_a/" requires the user to have both "ROLE_ADMIN" or "ROLE_GROUP_A"
                .antMatchers("/admin/**").hasAnyRole("ADMIN", "GROUP_A")
                // Any URL that has not already been matched on only requires that the user be authenticated
                .anyRequest().authenticated()
            .and().formLogin()
                 // all users have access to custom login page
                .loginPage("/login").permitAll()
            .and().logout()
                // customize logout url
                .logoutUrl("/my/logout")
                // customize logout success url
                .logoutSuccessUrl("/my/index")
                // specify a custom LogoutSuccessHandler. If this is specified, logoutSuccessUrl() is ignored
                .logoutSuccessHandler(logoutSuccessHandler)
                // invalidate the HttpSession at the time of logout. This is true by default
                .invalidateHttpSession(true)
                // Adds a LogoutHandler. SecurityContextLogoutHandler is added as the last LogoutHandler by default
                .addLogoutHandler(logoutHandler)
                // Allows specifying the names of cookies to be removed on logout success
                .deleteCookies()
            .and().rememberMe()
                // Add remember me function and valid date.
                .key("uniqueAndSecret")
                .tokenValiditySeconds(60 * 60 * 24 * 7);
            //@formatter:on
        }

    (3)AuthenticationManagerBuilder

    默认是这样写的:

        protected void configure(AuthenticationManagerBuilder auth) throws Exception {
            this.disableLocalConfigureAuthenticationBldr = true;
        }

    由上一节分析可知,它其实默认使用DefaultPasswordEncoderAuthenticationManagerBuilder这个Builder及自动配置的UserDetails和UserDetailsService。

        protected AuthenticationManager authenticationManager() throws Exception {
            if (!authenticationManagerInitialized) {
    // [1]如果覆盖configure()方法,则disableLocalConfigureAuthenticationBldr为false
    // [2]如果是默认的configure()方法,disableLocalConfigureAuthenticationBldr还是true configure(localConfigureAuthenticationBldr);
    if (disableLocalConfigureAuthenticationBldr) { authenticationManager = authenticationConfiguration .getAuthenticationManager(); // [2] } else { authenticationManager = localConfigureAuthenticationBldr.build(); // [1] } authenticationManagerInitialized = true; } return authenticationManager; }

    如果被覆盖,虽然还是使用的DefaultPasswordEncoderAuthenticationManagerBuilder,但是我们可以使用UserDetailsManagerConfigurer(的两个子类InMemoryUserDetailsManagerConfigurer,JdbcUserDetailsManagerConfigurer)来构建UserDetailsService及UserDetails。InMemoryUserDetailsManagerConfigurer为例,下面是自定义的写法。

        @Bean
        PasswordEncoder passwordEncoder() {
            return PasswordEncoderFactories.createDelegatingPasswordEncoder();
        }
    
        @Override
        protected void configure(AuthenticationManagerBuilder auth) throws Exception {
            //@formatter:off
            // returns InMemoryUserDetailsManagerConfigurer
            PasswordEncoder encoder = new BCryptPasswordEncoder();
            auth.inMemoryAuthentication()
                // create a UserDetailsBuilder and add to userBuilders
                .withUser("user").password("{bcrypt}" + encoder.encode("pass")).roles("USER")
                // returns InMemoryUserDetailsManagerConfigurer
                .and()
                // create a UserDetailsBuilder again and add to userBuilders
    .withUser("admin").password("{bcrypt}" + encoder.encode("pass")).roles("USER", "ADMIN"); //@formatter:on }

    [注] 框架要求密码必须加密,所以这里加了有关password encode的支持。

    那么这段代码如何生成UserDetailsService及UserDetails的呢?流程如下:

    [1] 调用AuthenticationManagerBuilder的inMemoryAuthentication()方法创建InMemoryUserDetailsManagerConfigurer,调用InMemoryUserDetailsManagerConfigurer的构造器时则会创建InMemoryUserDetailsManager(即UserDetailsService的实现类),最终经过层层父类(InMemoryUserDetailsManagerConfigurer -> UserDetailsManagerConfigurer -> UserDetailsServiceConfigurer -> AbstractDaoAuthenticationConfigurer)设定到AbstractDaoAuthenticationConfigurer中。

        public InMemoryUserDetailsManagerConfigurer<AuthenticationManagerBuilder> inMemoryAuthentication()
                throws Exception {
            return apply(new InMemoryUserDetailsManagerConfigurer<>());
        }
        public InMemoryUserDetailsManagerConfigurer() {
            super(new InMemoryUserDetailsManager(new ArrayList<>()));
        }
        protected AbstractDaoAuthenticationConfigurer(U userDetailsService) {
            this.userDetailsService = userDetailsService;
            provider.setUserDetailsService(userDetailsService);
            if (userDetailsService instanceof UserDetailsPasswordService) {
                this.provider.setUserDetailsPasswordService((UserDetailsPasswordService) userDetailsService);
            }
        }

    [2] 调用AuthenticationManagerBuilder的apply()方法设定defaultUserDetailsService为[1]的InMemoryUserDetailsManager并且把[1]的InMemoryUserDetailsManagerConfigurer加到父类AbstractConfiguredSecurityBuilder的configurers list中

        private <C extends UserDetailsAwareConfigurer<AuthenticationManagerBuilder, ? extends UserDetailsService>> C apply(
                C configurer) throws Exception {
            this.defaultUserDetailsService = configurer.getUserDetailsService();
            return (C) super.apply(configurer);
        }

    [3] 调用InMemoryUserDetailsManagerConfigurer的父类UserDetailsManagerConfigurer的withUser()方法生成多个UserDetailsBuilder放在userBuilders list中

        public final UserDetailsBuilder withUser(String username) {
            UserDetailsBuilder userBuilder = new UserDetailsBuilder((C) this);
            userBuilder.username(username);
            this.userBuilders.add(userBuilder);
            return userBuilder;
        }

    [4] 当调用DefaultPasswordEncoderAuthenticationManagerBuilder的build()方法时,则会调用

        [4.1] 调用UserDetailsServiceConfigurer的configure()方法

        @Override
        public void configure(B builder) throws Exception {
            initUserDetailsService();
            super.configure(builder);
        }

        [4.2] 调用UserDetailsManagerConfigurer的initUserDetailsService()方法通过[3]的userBuilders创建User对象(UserDetails的实现类,并且从[1]中的AbstractDaoAuthenticationConfigurer获取UserDetailsService,并把UserDetails放到UserDetailsService中。

        @Override
        protected void initUserDetailsService() throws Exception {
            for (UserDetailsBuilder userBuilder : userBuilders) {
                getUserDetailsService().createUser(userBuilder.build());
            }
            for (UserDetails userDetails : this.users) {
                getUserDetailsService().createUser(userDetails);
            }
        }

    下面是一些自定义写法:

        @Override
        protected void configure(AuthenticationManagerBuilder auth) throws Exception {
            //@formatter:off
            // returns InMemoryUserDetailsManagerConfigurer
            auth.inMemoryAuthentication()
                // create a UserBuilder and add to userBuilders
                .withUser("user").password("password").roles("USER")
                // returns InMemoryUserDetailsManagerConfigurer
                .and()
                // create a UserBuilder again and add to userBuilders
                .withUser("admin").password("password").roles("USER", "ADMIN");
            //@formatter:on
        }

    (4)authenticationManagerBean()

    我们覆盖了configure(AuthenticationManagerBuilder auth)后,我们使用了AuthenticationManagerBuilder 的实现类DefaultPasswordEncoderAuthenticationManagerBuilder,通过InMemoryUserDetailsManagerConfigurer创建自己的UserDetailsService的实现类InMemoryUserDetailsManager及User,系统还会默认给我们创建AuthenticationProvider的实现类DaoAuthenticationProvider。但是我们发现,这些对象并不是Spring Bean。所以我们可以通过覆盖该方法并且声明为一个Bean,这样就可以在项目中注入并使用这个Bean了。

        @Bean(name = "myAuthenticationManager")
        @Override
        public AuthenticationManager authenticationManagerBean() throws Exception {
            return super.authenticationManagerBean();
        }

    通过父类的源码可以看到,实际上在调用时,创建了一个AuthenticationManager代理。

        public AuthenticationManager authenticationManagerBean() throws Exception {
            return new AuthenticationManagerDelegator(authenticationBuilder, context);
        }

    (5)userDetailsServiceBean()

    和(4)类似,Override this method to expose a UserDetailsService created from configure(AuthenticationManagerBuilder) as a bean. In general only thefollowing override should be done of this method:

        @Bean(name = "myUserDetailsService")
        @Override
        public UserDetailsService userDetailsServiceBean() throws Exception {
            return super.userDetailsServiceBean();
        }

    (6) UserDetailsService

    还记得第三章的UserDetailsService实现类是如何生成的吗?这里做一个简述:

    [1] AuthenticationConfiguration中创建InitializeUserDetailsBeanManagerConfigurer Bean。

    [2] build时调用InitializeUserDetailsBeanManagerConfigurer的内部类InitializeUserDetailsManagerConfigurer的configure()方法。

    [3] 在ApplicationContext中获取UserDetailsService(by type),如果没有找到自定义的UserDetailsService Bean,则UserDetailsServiceAutoConfiguration生效,会lazy load一个InMemoryUserDetailsManager;反之,则使用我们自定义的UserDetailsService Bean。

     

    在WebSecurityConfigurerAdapter中,userDetailsServiceBean()和userDetailsService()两个方法内容实际上都是一样的。都是获取当前环境中(自定义的或系统生成的InMemoryUserDetailsManager)的UserDetailsService的代理类。所以,该类一般不需要重写,如果想自定义自己的UserDetailsService,可以直接实现UserDetailsService接口,并且把该类声明为一个Spring Bean:

    @Service
    public class MyUserDetailsService implements UserDetailsService {
        @Override
        public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
            // TODO Auto-generated method stub
            return null;
        }
    }

    当然你也可以直接覆盖该方法并声明为一个Bean:

        @Bean
        @Override
        public UserDetailsService userDetailsService() {
            return (username) -> {
                AppUser user = appUserRepository.findOneByUsername(username);
                if (user == null) {
                    throw new UsernameNotFoundException("Incorrect username or password.");
                }
                return user;
            };
        }

    需要注意的是,我们也要有UserDetails的实现类供UserDetailsService处理。

  • 相关阅读:
    Common Lisp 参数传递的几种形式
    EF架构~基于EF数据层的实现
    标准架构~业务层到底是否应该关注数据持久化的方式
    Redis学习笔记~Redis主从服务器,读写分离
    Redis学习笔记~是时候为Redis实现一个仓储了,RedisRepository来了
    MVVM架构~knockoutjs系列之数组的$index和$data
    【Oracle】-【sqlplus / as sysdba登录报错问题】-新用户使用sqlplus / as sysdba登录报错
    Android---App Widget(一)
    Mit 分布式系统导论,Distributed Systems ,lab1 -lab6 总结,实验一到实验六总结
    GDAL切割重采样遥感图像
  • 原文地址:https://www.cnblogs.com/storml/p/10968797.html
Copyright © 2020-2023  润新知