利用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编码方式一致
例如:
- 程序中配置为bcrypt,则数据库中client_secret字段保存的字符串也应用secret明文做一次bcrypt编码。
- 如确要存储明文,则将PasswordEncoder指定为NoOpPasswordEncoder。(由于NoOpPasswordEncoder已定义为deprecated,不建议采用这种方式)
- 为了更灵活的配置,使用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的方式
- 直接定义一个类型为PasswordEncoder的Bean,例如在WebSecurityConfig类中执行:
@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Bean
public PasswordEncoder myPasswordEncoder() {
return MyPasswordEncoderFactories.createDelegatingPasswordEncoder();
}
}
这种方式的问题是用户密码验证也会使用相同的encoder.
- 用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在最新版本升级后,默认把之前的明文密码方式给去掉了
}