跳到主要内容

14、Spring Security 实战 - PasswordEncoderFactories 设计模式之简单工厂模式

设计模式之工厂模式可以参考下面两篇文章:

[读书| 设计模式之禅 - 工厂方法模式][_ _ -]

[王争| 设计模式之美 - 工厂模式:我为什么说没事不要随便用工厂模式创建对象?][_ _ - 1]

1. PasswordEncoderFactories 演进分析

我们根据代表不同密码加密算法的encodeId(比如:bcrypt,ldap,pbkdf2,noop等),来选择不同的加密算法(比如:BCryptPasswordEncode,rLdapShaPasswordEncoder,NoOpPasswordEncoder等)对密码进行加密,如果我们使用传统的方式如何写:

public class PasswordEncoderResource {
   
     
    public PasswordEncoder load(){
   
     
       String encodeId = getEncodeId();
       if("bcrypt".equalsIgnoreCase(encodeId)){
   
     
           return new BCryptPasswordEncoder();
       }else if("ldap".equalsIgnoreCase(encodeId)){
   
     
           return new LdapShaPasswordEncoder();
       }else if("noop".equalsIgnoreCase(encodeId)){
   
     
           return  NoOpPasswordEncoder.getInstance();
       }else if("pbkdf2".equalsIgnoreCase(encodeId)){
   
     
           return new Pbkdf2PasswordEncoder();
       }else if("scrypt".equalsIgnoreCase(encodeId)){
   
     
           return new SCryptPasswordEncoder();
       }else if("SHA-1".equalsIgnoreCase(encodeId)){
   
     
           return new MessageDigestPasswordEncoder("SHA-1");
       }else if("SHA-256".equalsIgnoreCase(encodeId)){
   
     
           return new MessageDigestPasswordEncoder("SHA-256");
       }else if("sha256".equalsIgnoreCase(encodeId)){
   
     
           return new StandardPasswordEncoder();
       }else if("argon2".equalsIgnoreCase(encodeId)){
   
     
           return new Argon2PasswordEncoder();
       }
       return new BCryptPasswordEncoder();
    }

    private String getEncodeId() {
   
     
        return "bcrypt";
    }
}

为了让代码逻辑更加清晰,可读性更好,我们要善于将功能独立的代码块封装成函数,我们可以将代码中涉及 PasswordEncoder 创建的部分逻辑剥离出来,抽象成 createPasswordEncoder(String encodeId) 函数。重构之后的代码如下所示:

public class PasswordEncoderResource {
   
     
    public PasswordEncoder load(){
   
     
        String encodeId = getEncodeId();
        return createPasswordEncoder(encodeId);
    }

    private PasswordEncoder createPasswordEncoder(String encodeId) {
   
     
        if("bcrypt".equalsIgnoreCase(encodeId)){
   
     
            return new BCryptPasswordEncoder();
        }else if("ldap".equalsIgnoreCase(encodeId)){
   
     
            return new LdapShaPasswordEncoder();
        }else if("noop".equalsIgnoreCase(encodeId)){
   
     
            return  NoOpPasswordEncoder.getInstance();
        }else if("pbkdf2".equalsIgnoreCase(encodeId)){
   
     
            return new Pbkdf2PasswordEncoder();
        }else if("scrypt".equalsIgnoreCase(encodeId)){
   
     
            return new SCryptPasswordEncoder();
        }else if("SHA-1".equalsIgnoreCase(encodeId)){
   
     
            return new MessageDigestPasswordEncoder("SHA-1");
        }else if("SHA-256".equalsIgnoreCase(encodeId)){
   
     
            return new MessageDigestPasswordEncoder("SHA-256");
        }else if("sha256".equalsIgnoreCase(encodeId)){
   
     
            return new StandardPasswordEncoder();
        }else if("argon2".equalsIgnoreCase(encodeId)){
   
     
            return new Argon2PasswordEncoder();
        }
        return new BCryptPasswordEncoder();
    }

    private String getEncodeId() {
   
     
        return "bcrypt";
    }
}

为了让类的职责更加单一、代码更加清晰,我们还可以进一步将 createPasswordEncoder(String encodeId) 函数剥离到一个独立的类中,让这个类只负责对象的创建。而这个类就是简单工厂模式类:

public class PasswordEncoderResource {
   
     
    public PasswordEncoder load(){
   
     
        String encodeId = getEncodeId();
        return PasswordEncoderFactory.createPasswordEncoder(encodeId);
    }

    private String getEncodeId() {
   
     
        return "bcrypt";
    }
}

public class PasswordEncoderFactory {
   
     
    public static PasswordEncoder createPasswordEncoder(String encodeId) {
   
     
        if("bcrypt".equalsIgnoreCase(encodeId)){
   
     
            return new BCryptPasswordEncoder();
        }else if("ldap".equalsIgnoreCase(encodeId)){
   
     
            return new LdapShaPasswordEncoder();
        }else if("noop".equalsIgnoreCase(encodeId)){
   
     
            return  NoOpPasswordEncoder.getInstance();
        }else if("pbkdf2".equalsIgnoreCase(encodeId)){
   
     
            return new Pbkdf2PasswordEncoder();
        }else if("scrypt".equalsIgnoreCase(encodeId)){
   
     
            return new SCryptPasswordEncoder();
        }else if("SHA-1".equalsIgnoreCase(encodeId)){
   
     
            return new MessageDigestPasswordEncoder("SHA-1");
        }else if("SHA-256".equalsIgnoreCase(encodeId)){
   
     
            return new MessageDigestPasswordEncoder("SHA-256");
        }else if("sha256".equalsIgnoreCase(encodeId)){
   
     
            return new StandardPasswordEncoder();
        }else if("argon2".equalsIgnoreCase(encodeId)){
   
     
            return new Argon2PasswordEncoder();
        }
        return new BCryptPasswordEncoder();
    }
}

我们每次调用 PasswordEncoderFactory#createPasswordEncoder 的时候,都要创建一个新的 PasswordEncoder,实际上,如果 parser 可以复用,为了节省内存和对象创建的时间,我们可以将 PasswordEncoder 事先创建好缓存起来。当调用 PasswordEncoderFactory#createPasswordEncoder 函数的时候,我们从缓存中取出 PasswordEncoder 对象直接使用。

通过定义一个Map容器,容纳所有产生的对象,如果在Map容器中已经有的对象,则直接取出返回;如果没有,则根据需要的类型产生一个对象并放入到Map容器中,以方便下次调用。

public class PasswordEncoderFactory {
   
     
    
    public static final Map<String, PasswordEncoder> encoders = new HashMap<>();
    
    static {
   
     
        encoders.put("bcrypt", new BCryptPasswordEncoder());
        encoders.put("ldap", new org.springframework.security.crypto.password.LdapShaPasswordEncoder());
        encoders.put("MD4", new org.springframework.security.crypto.password.Md4PasswordEncoder());
        encoders.put("MD5", new org.springframework.security.crypto.password.MessageDigestPasswordEncoder("MD5"));
        encoders.put("noop", org.springframework.security.crypto.password.NoOpPasswordEncoder.getInstance());
        encoders.put("pbkdf2", new Pbkdf2PasswordEncoder());
        encoders.put("scrypt", new SCryptPasswordEncoder());
        encoders.put("SHA-1", new org.springframework.security.crypto.password.MessageDigestPasswordEncoder("SHA-1"));
        encoders.put("SHA-256", new org.springframework.security.crypto.password.MessageDigestPasswordEncoder("SHA-256"));
        encoders.put("sha256", new org.springframework.security.crypto.password.StandardPasswordEncoder());
        encoders.put("argon2", new Argon2PasswordEncoder());
    }
    
    public static PasswordEncoder createPasswordEncoder(String encodeId) {
   
     
        if(!StringUtils.hasText(encodeId)){
   
     
            return null;
        }
        return encoders.get(encodeId);
    }
}

2. PasswordEncoderFactories 源码分析

1. PasswordEncoderFactories
public class PasswordEncoderFactories {
   
     

   /**
    * 创建DelegatingPasswordEncoder的实例
    */
	@SuppressWarnings("deprecation")
	public static PasswordEncoder createDelegatingPasswordEncoder() {
   
     
        
		String encodingId = "bcrypt";
		Map<String, PasswordEncoder> encoders = new HashMap<>();
		encoders.put(encodingId, new BCryptPasswordEncoder());
		encoders.put("ldap", new org.springframework.security.crypto.password.LdapShaPasswordEncoder());
		encoders.put("MD4", new org.springframework.security.crypto.password.Md4PasswordEncoder());
		encoders.put("MD5", new org.springframework.security.crypto.password.MessageDigestPasswordEncoder("MD5"));
		encoders.put("noop", org.springframework.security.crypto.password.NoOpPasswordEncoder.getInstance());
		encoders.put("pbkdf2", new Pbkdf2PasswordEncoder());
		encoders.put("scrypt", new SCryptPasswordEncoder());
		encoders.put("SHA-1", new org.springframework.security.crypto.password.MessageDigestPasswordEncoder("SHA-1"));
		encoders.put("SHA-256", new org.springframework.security.crypto.password.MessageDigestPasswordEncoder("SHA-256"));
		encoders.put("sha256", new org.springframework.security.crypto.password.StandardPasswordEncoder());
		encoders.put("argon2", new Argon2PasswordEncoder());

        // DelegatingPasswordEncoder默认使用的是BCryptPasswordEncoder加密
		return new DelegatingPasswordEncoder(encodingId, encoders);
	}

	private PasswordEncoderFactories() {
   
     }
}

DelegatingPasswordEncoder 是一个代理类,主要用来代理上面介绍的不同加密方式,它允许系统中存在不同的加密方案,很方便的完成对加密方案的升级。如果是通过PasswordEncoderFactories#createDelegatingPasswordEncoder方法创建的DelegatingPasswordEncoder实例时,默认其实使用的还是BCryptPasswordEncoder

@Configuration
@EnableWebSecurity
public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter {
   
     

    @Override
    protected void configure(HttpSecurity http) throws Exception {
   
     
        http
                .formLogin()
                .and()
                .csrf()
                .disable()
                .authorizeRequests()
                .antMatchers("/**")
                .authenticated();
    }

    @Bean
    public UserDetailsService users() {
   
     
        // {MD5}value必须大写,value值必须是32位小写
        // admin
        UserDetails admin = User.builder()
                //.passwordEncoder(encoder::encode)
                .username("admin").password(
                        "{MD5}e10adc3949ba59abbe56e057f20f883e"
                ).roles("admin").build();

        // lisi
        UserDetails hengboy = User.builder()
                .username("lisi")
                .password("{bcrypt}$2a$10$iMz8sMVMiOgRgXRuREF/f.ChT/rpu2ZtitfkT5CkDbZpZlFhLxO3y")
                .roles("admin")
                .build();

        // zhangsan
        UserDetails yuqiyu = User.builder().username("zhangsan")
                //.password("{noop}123456")
                .password("{pbkdf2}cc409867e39f011f6332bbb6634f58e98d07be7fceefb4cc27e62501594d6ed0b271a25fd9f7fc2e")
                .roles("user").build();

        return new InMemoryUserDetailsManager(admin, zhangsan, lisi);
    }
}

2. 客户端 WebSecurityConfigurerAdapter
public abstract class WebSecurityConfigurerAdapter implements WebSecurityConfigurer<WebSecurity> {
   
     
    
    static class LazyPasswordEncoder implements PasswordEncoder {
   
     

        private PasswordEncoder passwordEncoder;
        
        // ...

        private PasswordEncoder getPasswordEncoder() {
   
     
            if (this.passwordEncoder != null) {
   
     
                return this.passwordEncoder;
            }
            PasswordEncoder passwordEncoder = getBeanOrNull(PasswordEncoder.class);
            if (passwordEncoder == null) {
   
     
                passwordEncoder = PasswordEncoderFactories.createDelegatingPasswordEncoder();
            }
            this.passwordEncoder = passwordEncoder;
            return passwordEncoder;
        }
        
        // ....

    }
}

3. DelegatingPasswordEncoder
public class DelegatingPasswordEncoder implements PasswordEncoder {
   
     

   private static final String PREFIX = "{";

   private static final String SUFFIX = "}";

   private final String idForEncode;

   private final PasswordEncoder passwordEncoderForEncode;

   private final Map<String, PasswordEncoder> idToPasswordEncoder;

   private PasswordEncoder defaultPasswordEncoderForMatches = new UnmappedIdPasswordEncoder();

   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);
         }
      }
      this.idForEncode = idForEncode;
      this.passwordEncoderForEncode = idToPasswordEncoder.get(idForEncode);
      this.idToPasswordEncoder = new HashMap<>(idToPasswordEncoder);
   }
   public void setDefaultPasswordEncoderForMatches(PasswordEncoder defaultPasswordEncoderForMatches) {
   
     
      if (defaultPasswordEncoderForMatches == null) {
   
     
         throw new IllegalArgumentException("defaultPasswordEncoderForMatches cannot be null");
      }
      this.defaultPasswordEncoderForMatches = defaultPasswordEncoderForMatches;
   }

   @Override
   public String encode(CharSequence rawPassword) {
   
     
      return PREFIX + this.idForEncode + SUFFIX + this.passwordEncoderForEncode.encode(rawPassword);
   }

   @Override
   public boolean matches(CharSequence rawPassword, String prefixEncodedPassword) {
   
     
      if (rawPassword == null && prefixEncodedPassword == null) {
   
     
         return true;
      }
      String id = extractId(prefixEncodedPassword);
      PasswordEncoder delegate = this.idToPasswordEncoder.get(id);
      if (delegate == null) {
   
     
         return this.defaultPasswordEncoderForMatches.matches(rawPassword, prefixEncodedPassword);
      }
      String encodedPassword = extractEncodedPassword(prefixEncodedPassword);
      return delegate.matches(rawPassword, encodedPassword);
   }

   private String extractId(String prefixEncodedPassword) {
   
     
      if (prefixEncodedPassword == null) {
   
     
         return null;
      }
      int start = prefixEncodedPassword.indexOf(PREFIX);
      if (start != 0) {
   
     
         return null;
      }
      int end = prefixEncodedPassword.indexOf(SUFFIX, start);
      if (end < 0) {
   
     
         return null;
      }
      return prefixEncodedPassword.substring(start + 1, end);
   }

   @Override
   public boolean upgradeEncoding(String prefixEncodedPassword) {
   
     
      String id = extractId(prefixEncodedPassword);
      if (!this.idForEncode.equalsIgnoreCase(id)) {
   
     
         return true;
      }
      else {
   
     
         String encodedPassword = extractEncodedPassword(prefixEncodedPassword);
         return this.idToPasswordEncoder.get(id).upgradeEncoding(encodedPassword);
      }
   }

   private String extractEncodedPassword(String prefixEncodedPassword) {
   
     
      int start = prefixEncodedPassword.indexOf(SUFFIX);
      return prefixEncodedPassword.substring(start + 1);
   }

   /**
    * Default {@link PasswordEncoder} that throws an exception telling that a suitable
    * {@link PasswordEncoder} for the id could not be found.
    */
   private class UnmappedIdPasswordEncoder implements PasswordEncoder {
   
     

      @Override
      public String encode(CharSequence rawPassword) {
   
     
         throw new UnsupportedOperationException("encode is not supported");
      }

      @Override
      public boolean matches(CharSequence rawPassword, String prefixEncodedPassword) {
   
     
         String id = extractId(prefixEncodedPassword);
         throw new IllegalArgumentException("There is no PasswordEncoder mapped for the id \"" + id + "\"");
      }

   }

}