利用Spring Cloud Security开发OAuth服务器的PasswordEncoder问题

  • PasswordEncoder怎么起作用
  • 客户端Secret验证问题
  • 指定PasswordEncoder的方式


PasswordEncoder怎么起作用

PasswordEncoder是Spring Security定义的一个接口,用于抽象密码验证过程。它的核心方法是matches。

public interface PasswordEncoder {
    String encode(CharSequence var1);

    boolean matches(CharSequence var1, String var2);

    default boolean upgradeEncoding(String encodedPassword) {
        return false;
    }
}

matches有两个参数,前面一个是待验证密码,后一个是存储中的正确密码。由于存储中的密码通常以哈希Hash方式存储,因此可以叫前一个参数为裸密码(rawPassword),后一个参数叫编码密码(encodedPassword)。

一般来讲,matches的过程就是先将第一个参数散列编码,再和第二个参数进行比较。但由于不同的散列编码有一定规则,matches也会检查encodedPassword的合法性。

例如 BCryptPasswordEncoder,见以下源码:

public boolean matches(CharSequence rawPassword, String encodedPassword) {
        if (rawPassword == null) {
            throw new IllegalArgumentException("rawPassword cannot be null");
        } else if (encodedPassword != null && encodedPassword.length() != 0) {
            if (!this.BCRYPT_PATTERN.matcher(encodedPassword).matches()) {
                this.logger.warn("Encoded password does not look like BCrypt");
                return false;
            } else {
                return BCrypt.checkpw(rawPassword.toString(), encodedPassword);
            }
        } else {
            this.logger.warn("Empty encoded password");
            return false;
        }
    }

BCryptPasswordEncoder会验证encodedPassword是否符合BCrypt的规则。

客户端Secret验证问题

OAuth2是客户端和用户的双认证。
无论何种方式,OAuth2认证中心会首先对客户端进行验证,验证的依据是client_id和client_secret两个参数。这一对参数可以看作是客户端的用户名和密码。而spring的验证服务器验证机制也是利用了spring security的密码验证,也就是PasswordEncoder。

在oauth/token这个endpoint收到请求时,spring会调用程序配置的PasswordEncoder Bean去验证secret。

通常这个配置会在WebSecurityConfig类中指定。如果指定为BCryptPasswordEncoder,程序存储的Secret必须按BCrypt编码方式编码。否则就会报“Encoded password does not look like BCrypt”错误。这事实上与客户端传过来的是否明文并无不关系。

解决的办法就是使存储中保存着的客户端Secret与程序中配置的PasswordEncoder编码方式一致

例如:

  1. 程序中配置为bcrypt,则数据库中client_secret字段保存的字符串也应用secret明文做一次bcrypt编码。
  2. 如确要存储明文,则将PasswordEncoder指定为NoOpPasswordEncoder。(由于NoOpPasswordEncoder已定义为deprecated,不建议采用这种方式)
  3. 为了更灵活的配置,使用DelegatingPasswordEncoder(直接用PasswordEncoderFactories.createDelegatingPasswordEncoder()生成)。这个Encoder的编码方式是将散列方式用花括号括着放在散列串前面,从而兼容所有散列方式。同时也支持明文。

Spring Seurity预先定义的PasswordEncoderFactories的源码:

public final class PasswordEncoderFactories {
    private PasswordEncoderFactories() {
    }

    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);
    }
}

从上面代码看,这个办法至少支持11中散列编码方式。

指定PasswordEncoder的方式

  1. 直接定义一个类型为PasswordEncoder的Bean,例如在WebSecurityConfig类中执行:
@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    @Bean
    public PasswordEncoder myPasswordEncoder() {
        return MyPasswordEncoderFactories.createDelegatingPasswordEncoder();
    }
}

这种方式的问题是用户密码验证也会使用相同的encoder.

  1. 用AuthorizationServerConfigurerAdapter指定
@Override
    public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
        security.allowFormAuthenticationForClients();   //让/oauth/token支持client_id以及client_secret作登录认证
        security.checkTokenAccess("permitAll()");       //开启/oauth/check_token端口无权限访问
        security.tokenKeyAccess("isAuthenticated()");     // 开启/oauth/token_key验证端口验证权限(isAuthenticated() permitAll())才能访问
        security.passwordEncoder(NoOpPasswordEncoder.getInstance()); //因为springsecurity在最新版本升级后,默认把之前的明文密码方式给去掉了
    }