【Spring Security】 密码编码流程

Metadata

title: 【Spring Security】 密码编码流程
date: 2023-02-02 14:01
tags:
  - 行动阶段/完成
  - 主题场景/组件
  - 笔记空间/KnowladgeSpace/ProgramSpace/ModuleSpace
  - 细化主题/Module/SpringSecurity
categories:
  - SpringSecurity
keywords:
  - SpringSecurity
description: 【Spring Security】 密码编码流程

【Spring Security】 密码编码流程

流程分析

1. 注入密码解析器

    /**
     * 注入密码解析器到IOC中
     */
    @Bean
    PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

2. DaoAuthenticationProvider

DaoAuthenticationProvider 有一个属性 PasswordEncoder,就是用来校验数据库密码的。创建 DaoAuthenticationProvider 的时候,会设置 PasswordEncoder 委派对象。

public DaoAuthenticationProvider() {
        this.setPasswordEncoder(PasswordEncoderFactories.createDelegatingPasswordEncoder());
    }

使用 PasswordEncoderFactories 工厂创建委派的 DelegatingPasswordEncoder,首先会将所有 security 支持的加密解析器放入一个 Map 中。

public static PasswordEncoder createDelegatingPasswordEncoder() {
        String encodingId = "bcrypt";
        Map<String, PasswordEncoder> encoders = new HashMap();
        encoders.put(encodingId, new BCryptPasswordEncoder());
        encoders.put("ldap", new LdapShaPasswordEncoder());
        encoders.put("MD4", new Md4PasswordEncoder());
        encoders.put("MD5", new MessageDigestPasswordEncoder("MD5"));
        encoders.put("noop", NoOpPasswordEncoder.getInstance());
        encoders.put("pbkdf2", new Pbkdf2PasswordEncoder());
        encoders.put("scrypt", new SCryptPasswordEncoder());
        encoders.put("SHA-1", new MessageDigestPasswordEncoder("SHA-1"));
        encoders.put("SHA-256", new MessageDigestPasswordEncoder("SHA-256"));
        encoders.put("sha256", new StandardPasswordEncoder());
        encoders.put("argon2", new Argon2PasswordEncoder());
        return new DelegatingPasswordEncoder(encodingId, encoders);
    }

3. DelegatingPasswordEncoder

接下来使用 DelegatingPasswordEncoder 的构造方法创建对象。该对象维护了所有了密码解析器。

public DelegatingPasswordEncoder(String idForEncode, Map<String, PasswordEncoder> idToPasswordEncoder) {
        if (idForEncode == null) {
            throw new IllegalArgumentException("idForEncode cannot be null");
        }
        if (!idToPasswordEncoder.containsKey(idForEncode)) {
            throw new IllegalArgumentException(
                    "idForEncode " + idForEncode + "is not found in idToPasswordEncoder " + idToPasswordEncoder);
        }
        for (String id : idToPasswordEncoder.keySet()) {
            if (id == null) {
                continue;
            }
            if (id.contains(PREFIX)) {
                throw new IllegalArgumentException("id " + id + " cannot contain " + PREFIX);
            }
            if (id.contains(SUFFIX)) {
                throw new IllegalArgumentException("id " + id + " cannot contain " + SUFFIX);
            }
        }
        // 密码解析器的ID bcrypt
        this.idForEncode = idForEncode;
        // 密码解析器 
        this.passwordEncoderForEncode = idToPasswordEncoder.get(idForEncode);
        // 所有的密码解析器
        this.idToPasswordEncoder = new HashMap<>(idToPasswordEncoder);
    }

5. InitializeUserDetailsBeanManagerConfigurer

之后进入 InitializeUserDetailsBeanManagerConfigurer 类的 configure 方法进行相关配置。这里主要获取数据库查询认证时需要的一些 bean 对象,比如 UserDetailsService、PasswordEncoder、UserDetailsPasswordService,并把这个对象赋值给 DaoAuthenticationProvider,此时我们注入的密码解析器就和验证管理器相关联了。

@Order(InitializeUserDetailsBeanManagerConfigurer.DEFAULT_ORDER)
class InitializeUserDetailsBeanManagerConfigurer extends GlobalAuthenticationConfigurerAdapter {

    static final int DEFAULT_ORDER = Ordered.LOWEST_PRECEDENCE - 5000;

    private final ApplicationContext context;

    /**
     * @param context
     */
    InitializeUserDetailsBeanManagerConfigurer(ApplicationContext context) {
        this.context = context;
    }

    @Override
    public void init(AuthenticationManagerBuilder auth) throws Exception {
        auth.apply(new InitializeUserDetailsManagerConfigurer());
    }

    class InitializeUserDetailsManagerConfigurer extends GlobalAuthenticationConfigurerAdapter {

        @Override
        public void configure(AuthenticationManagerBuilder auth) throws Exception {
            if (auth.isConfigured()) {
                return;
            }
            // 获取UserDetailsService Bean
            UserDetailsService userDetailsService = getBeanOrNull(UserDetailsService.class);
            if (userDetailsService == null) {
                return;
            }
            // 获取PasswordEncoder Bean
            PasswordEncoder passwordEncoder = getBeanOrNull(PasswordEncoder.class);
            UserDetailsPasswordService passwordManager = getBeanOrNull(UserDetailsPasswordService.class);
            DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
            provider.setUserDetailsService(userDetailsService);
            if (passwordEncoder != null) {
                provider.setPasswordEncoder(passwordEncoder);
            }
            if (passwordManager != null) {
                provider.setUserDetailsPasswordService(passwordManager);
            }
            provider.afterPropertiesSet();
            auth.authenticationProvider(provider);
        }

        /**
         * @return a bean of the requested class if there's just a single registered
         * component, null otherwise.
         * 获取Bean 
         */
        private <T> T getBeanOrNull(Class<T> type) {
            String[] beanNames = InitializeUserDetailsBeanManagerConfigurer.this.context.getBeanNamesForType(type);
            if (beanNames.length != 1) {
                return null;
            }
            return InitializeUserDetailsBeanManagerConfigurer.this.context.getBean(beanNames[0], type);
        }

    }

}

6. 密码校验

加载完成后,用户输入账号密码进行登录,最后密码校验调用的是 DaoAuthenticationProvider 的 additionalAuthenticationChecks 方法。第五步中我们获取到了注入的密码解析器,并交给了 DaoAuthenticationProvider,那么这些就可以调用注入的密码解析器,并使用 matches 方法校验数据库查询出来的和输入的是否匹配,密码错误时,抛出异常,正确则返回认证信息。流程结束。

protected void additionalAuthenticationChecks(UserDetails userDetails, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
        if (authentication.getCredentials() == null) {
            this.logger.debug("Failed to authenticate since no credentials provided");
            throw new BadCredentialsException(this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
        } else {
            String presentedPassword = authentication.getCredentials().toString();
            if (!this.passwordEncoder.matches(presentedPassword, userDetails.getPassword())) {
                this.logger.debug("Failed to authenticate since password does not match stored value");
                throw new BadCredentialsException(this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
            }
        }
    }