1. 程式人生 > >springCloud微服務系列——OAuth2+JWT——spring-security4升級到spring-security5

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()))