整合Spring Security OAuth 踩坑记录

本文解决SpringSecurity OAuth2中常见的密码加密问题,包括BCrypt匹配失败与不明编码ID异常,提供代码示例及配置解决方案。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

整合Spring Security OAuth 踩坑记录

参考官方文档:https://docs.spring.io/spring-security/site/docs/5.3.2.BUILD-SNAPSHOT/reference/html5/#authentication-password-storage

问题:

系统报`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();
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值