springCloud微服务系列OAuth2+JWTspring-security4升级到spring-security5

2022-06-15 08:39:55

目录

一、简介

二、问题

三、源码分析

四、解决方案


一、简介

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

  • 作者:guduyishuai
  • 原文链接:https://blog.csdn.net/guduyishuai/article/details/82115990
    更新时间:2022-06-15 08:39:55