整合Spring Security OAuth
踩坑记录
问题:
系统报`Encoded password does not look like BCrypt`错误
问题说明:
出现这个问题,一般都是设置的客户端密码是123456
,但程序中使用的是BCryptPasswordEncoder
,BCryptPasswordEncoder
在匹配的时候会用正则验证是不是bcrypt
格式
- 程序代码:
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.inMemory().withClient("client_1")
.authorizedGrantTypes("client_credentials", "refresh_token")
.scopes("select")
.authorities("client")
// 这里使用的是明文密码
.secret("123456")
.and().withClient("client_2")
.authorizedGrantTypes("password", "refresh_token")
.scopes("select")
.authorities("client")
// 这里使用的是明文密码
.secret("123456");
}
//程序中注入的确是`BCryptPasswordEncoder`,在执行匹配的方法就会出错
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
BCryptPasswordEncoder
匹配方法
public boolean matches(CharSequence rawPassword, String encodedPassword) {
if (encodedPassword == null || encodedPassword.length() == 0) {
logger.warn("Empty encoded password");
return false;
}
//验证格式
if (!BCRYPT_PATTERN.matcher(encodedPassword).matches()) {
logger.warn("Encoded password does not look like BCrypt");
return false;
}
return BCrypt.checkpw(rawPassword.toString(), encodedPassword);
}
解决方案:
知道问题所在,更改的方式就有很多种,我们直接更改注入加密方式为明文,生产环境不推荐,也可以使用如下方式:
.secret(new BCryptPasswordEncoder().encode("123456"))
问题:
`java.lang.IllegalArgumentException: There is no PasswordEncoder mapped for the id "null"`异常
程序代码:
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.inMemory().withClient("client_1")
.authorizedGrantTypes("client_credentials", "refresh_token")
.scopes("select")
.authorities("client")
// 这里使用的是明文密码
.secret("123456")
.and().withClient("client_2")
.authorizedGrantTypes("password", "refresh_token")
.scopes("select")
.authorities("client")
// 这里使用的是明文密码
.secret("123456");
}
@Bean
public PasswordEncoder passwordEncoder() {
return PasswordEncoderFactories.createDelegatingPasswordEncoder();
}
问题说明:
Spring Security
之前的密码加密方式默认是NoOpPasswordEncoder
明文方式,当然你可以更改为BCryptPasswordEncoder
,但是如果你之前是明文格式或者其他密码加密格式的密码比如MD5
,你想改变你系统加密方式为BCryptPasswordEncoder
,这个时候会存在一个问题就是你需要重新改变你数据库所有密码的编码格式,来适配你新的编码加密方式。这个工程将是巨大的,耗时的,易出现问题的。
SpringSecurity
针对上述问题提出了一个新的密码加密结构:{id}encodedPassword
,这里面id
则代表你当前密码使用的是那种加密方式,在进行密码匹配的时候,通过前缀id
来确认密码加密格式,在进行相应的匹配操作。这样我们在更换之前的加密方式的时候,无需变动之前的加密方式。极大的降低变更带来的成本。对应的实现类是DelegatingPasswordEncoder
,我们通过分析DelegatingPasswordEncoder
源码,就能找到出现问题的原因:
DelegatingPasswordEncoder
- 继承于
PasswordEncoder
public class DelegatingPasswordEncoder implements PasswordEncoder {
//前缀左分隔符
private static final String PREFIX = "{";
//前缀右分隔符
private static final String SUFFIX = "}";
//默认加密处理key值
private final String idForEncode;
//方法encode,使用的加密方式
private final PasswordEncoder passwordEncoderForEncode;
//支持加密方式的集合 key是加密标识,对应{id},value是加密方式具体的实现类
private final Map<String, PasswordEncoder> idToPasswordEncoder;
//匹配不到,默认使用的密码匹配方式,UnmappedIdPasswordEncoder其内部类
//调用matches会抛异常
private PasswordEncoder defaultPasswordEncoderForMatches = new UnmappedIdPasswordEncoder();
- 构造方法
idForEncode
:当前系统使用的加密方式的值,对应加密密码的id
值idToPasswordEncoder
:支持的加密方式的Map
集合,key
对应的{id}
,value
对应的是实现类
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);
}
//验证id格式
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);
}
-
加密方法
encode
调用当前的加密方式并拼接前缀
{id}
返回
//调用默认的加密方法,会使用默认的加密方式进行加密
@Override
public String encode(CharSequence rawPassword) {
return PREFIX + this.idForEncode + SUFFIX + this.passwordEncoderForEncode.encode(rawPassword);
}
- 匹配方法
matches
通过传入的加密代码前缀id
,寻找相应的加密规则进行匹配判断,如果没有找到,则使用默认UnmappedIdPasswordEncoder
匹配,抛出异常IllegalArgumentException
,默认匹配可以通过方法setDefaultPasswordEncoderForMatches
进行更换,这也是我们为什么会出现IllegalArgumentException
的原因,因为我们没有设置{id}
,导致系统找不到相应的加密方式,所以会抛出该异常
// 密码匹配,会将前缀提出
@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);
}
/**
* Default {@link PasswordEncoder} that throws an exception that a id could
*/
private class UnmappedIdPasswordEncoder implements PasswordEncoder {
@Override
public String encode(CharSequence rawPassword) {
throw new UnsupportedOperationException("encode is not supported");
}
//使用默认的加密方式,会直接抛异常IllegalArgumentException("There is no PasswordEncoder mapped for the id \"" + id + "\"");可以通过setDefaultPasswordEncoderForMatches方法,设置自定义的编码格式,比如升级之前的编码格式
@Override
public boolean matches(CharSequence rawPassword,
String prefixEncodedPassword) {
String id = extractId(prefixEncodedPassword);
throw new IllegalArgumentException("There is no PasswordEncoder mapped for the id \"" + id + "\"");
}
}
PasswordEncoderFactories.createDelegatingPasswordEncoder()
方法
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());
return new DelegatingPasswordEncoder(encodingId, encoders);
}
解决方案:
- 一定要设置
id
,明确加密方式
.secret("{bcypt}"+new BCryptPasswordEncoder().encode("123456"))
- 设置
DelegatingPasswordEncoder
的默认匹配方式是明文
@Bean
public PasswordEncoder passwordEncoder() {
DelegatingPasswordEncoder delegatingPasswordEncoder = (DelegatingPasswordEncoder) PasswordEncoderFactories.createDelegatingPasswordEncoder();
delegatingPasswordEncoder.setDefaultPasswordEncoderForMatches(NoOpPasswordEncoder.getInstance());
return delegatingPasswordEncoder;
}
问题:
unsupported_grant_type
解决方案:
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints.accessTokenConverter(jwtAccessTokenConverter());
endpoints.tokenStore(jwtTokenStore());
//必须注入 AuthenticationManager
endpoints.authenticationManager(authenticationManager);
}
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
//部分代码
@Override
@Bean
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
}