目录
一、简介
二、问题
三、源码分析
四、解决方案
spring boot2和spring cloud Finchley版本使用的是spring-security5,在升级的过程中OAuth2+JWT遇到一些问题,这里记录一下。环境如下:
spring boot 2.0.3
spring cloud Finchley
spring security 4.2.4 升级到 5.0.6
调用/oauth/token的时候,出现警告Encoded password does not look like BCrypt,直接被loginPage配置的controller拦截,无法生成jwt
调用/oauth/token之前,spring会检查配置的clientSecret是否正确,该检查调用DaoAuthenticationProvider的additionalAuthenticationChecks方法
我们先看一下4.2.4中的该方法
@SuppressWarnings("deprecation")
protected void additionalAuthenticationChecks(UserDetails userDetails,
UsernamePasswordAuthenticationToken authentication)
throws AuthenticationException {
Object salt = null;
if (this.saltSource != null) {
salt = this.saltSource.getSalt(userDetails);
}
if (authentication.getCredentials() == null) {
logger.debug("Authentication failed: no credentials provided");
throw new BadCredentialsException(messages.getMessage(
"AbstractUserDetailsAuthenticationProvider.badCredentials",
"Bad credentials"));
}
String presentedPassword = authentication.getCredentials().toString();
if (!passwordEncoder.isPasswordValid(userDetails.getPassword(),
presentedPassword, salt)) {
logger.debug("Authentication failed: password does not match stored value");
throw new BadCredentialsException(messages.getMessage(
"AbstractUserDetailsAuthenticationProvider.badCredentials",
"Bad credentials"));
}
}
关键的代码为
if (!passwordEncoder.isPasswordValid(userDetails.getPassword(),
presentedPassword, salt)) {
logger.debug("Authentication failed: password does not match stored value");
throw new BadCredentialsException(messages.getMessage(
"AbstractUserDetailsAuthenticationProvider.badCredentials",
"Bad credentials"));
}
userDetails.getPassword()获得的是clientSecret的明文
具体的逻辑为PlaintextPasswordEncoder的isPasswordValid方法
public boolean isPasswordValid(String encPass, String rawPass, Object salt) {
String pass1 = encPass + "";
// Strict delimiters is false because pass2 never persisted anywhere
// and we want to avoid unnecessary exceptions as a result (the
// authentication will fail as the encodePassword never allows them)
String pass2 = mergePasswordAndSalt(rawPass, salt, false);
if (ignorePasswordCase) {
// Note: per String javadoc to get correct results for Locale insensitive, use
// English
pass1 = pass1.toLowerCase(Locale.ENGLISH);
pass2 = pass2.toLowerCase(Locale.ENGLISH);
}
return PasswordEncoderUtils.equals(pass1, pass2);
}
关键代码为PasswordEncoderUtils.equals(pass1, pass2),该方法用指定的PasswordEncoder对两个明文进行比较
我们再看一下 5.0.6中DaoAuthenticationProvider的additionalAuthenticationChecks方法
@SuppressWarnings("deprecation")
protected void additionalAuthenticationChecks(UserDetails userDetails,
UsernamePasswordAuthenticationToken authentication)
throws AuthenticationException {
if (authentication.getCredentials() == null) {
logger.debug("Authentication failed: no credentials provided");
throw new BadCredentialsException(messages.getMessage(
"AbstractUserDetailsAuthenticationProvider.badCredentials",
"Bad credentials"));
}
String presentedPassword = authentication.getCredentials().toString();
if (!passwordEncoder.matches(presentedPassword, userDetails.getPassword())) {
logger.debug("Authentication failed: password does not match stored value");
throw new BadCredentialsException(messages.getMessage(
"AbstractUserDetailsAuthenticationProvider.badCredentials",
"Bad credentials"));
}
}
我们发现,它的校验逻辑变成了我们指定的passwordEncoder的matches方法
if (!passwordEncoder.matches(presentedPassword, userDetails.getPassword())) {
logger.debug("Authentication failed: password does not match stored value");
throw new BadCredentialsException(messages.getMessage(
"AbstractUserDetailsAuthenticationProvider.badCredentials",
"Bad credentials"));
}
同样userDetails.getPassword()方法返回的是明文
我们指定的是BCryptPasswordEncoder,所以我们看一下BCryptPasswordEncoder的matches方法
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);
}
关键代码为
if (!BCRYPT_PATTERN.matcher(encodedPassword).matches()) {
logger.warn("Encoded password does not look like BCrypt");
return false;
}
BCRYPT_PATTERN是加密后密码的正则表达式,很明显,我们这里的userDetails.getPassword()方法需要获得暗文。问题就出在这里,按照以前的配置,这里的encodedPassword是userDetails.getPassword()获得的明文,所以这里返回false。
解决方案就是在配置的时候配置clientSecret的暗文,而不是明文
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
ClientDetailsServiceBuilder clientDetailsServiceBuilder = clients.inMemory();
OAuth2ClientProperties[] oauth2ClientProperties = securityProperties.getOauth2().getClients();
for (OAuth2ClientProperties clientProperties : oauth2ClientProperties) {
clientDetailsServiceBuilder.withClient(clientProperties.getClientId())
.secret(passwordEncoder.encode(clientProperties.getClientSecret()))
.accessTokenValiditySeconds(clientProperties.getAccessTokenValiditySeconds())
.refreshTokenValiditySeconds(clientProperties.getRefreshTokenValidtySecnods())
.authorizedGrantTypes(clientProperties.getAuthorizedGrantTypes())
.scopes(clientProperties.getScopes());
}
注意这里的secret(passwordEncoder.encode(clientProperties.getClientSecret()))