springCloud微服務系列——OAuth2+JWT——spring-security4升級到spring-security5
目錄
一、簡介
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()))