WebFlux Security整合报错最新解决方法

作者:青山常在人不老   阅读 (2102)  |  收藏 (0)  |  点赞 (0)

摘要

Srping cloud WebFlux Security整合过程中报There is no PasswordEncoder mapped for the id "null" 的解决办法。


原文链接:WebFlux Security整合报错最新解决方法

问题

文章说明:请大家耐心从上至下看完整个文章,而不是直接走拿来主义,当然在文章的最后,我会把所有的源码分享出来,让您看整个文章的意义在于,您可以学习如何发现、查找、解决问题的思路。

我在使用SpringCloud gateway 整合 security+WebFlux时报如下错误:

java.lang.IllegalArgumentException: There is no PasswordEncoder mapped for the id "null"
	at org.springframework.security.crypto.password.DelegatingPasswordEncoder$UnmappedIdPasswordEncoder.matches(DelegatingPasswordEncoder.java:250) ~[spring-security-crypto-5.2.5.RELEASE.jar:5.2.5.RELEASE]
	Suppressed: reactor.core.publisher.FluxOnAssembly$OnAssemblyException: 
Error has been observed at the following site(s):
	|_ checkpoint ⇢ org.springframework.security.web.server.authentication.AuthenticationWebFilter [DefaultWebFilterChain]
	|_ checkpoint ⇢ org.springframework.security.web.server.authentication.AuthenticationWebFilter [DefaultWebFilterChain]
	|_ checkpoint ⇢ org.springframework.security.web.server.context.ReactorContextWebFilter [DefaultWebFilterChain]
	|_ checkpoint ⇢ org.springframework.security.web.server.header.HttpHeaderWriterWebFilter [DefaultWebFilterChain]
	|_ checkpoint ⇢ org.springframework.security.config.web.server.ServerHttpSecurity$ServerWebExchangeReactorContextWebFilter [DefaultWebFilterChain]
	|_ checkpoint ⇢ org.springframework.security.web.server.WebFilterChainProxy [DefaultWebFilterChain]
	|_ checkpoint ⇢ HTTP POST "/auth/login" [ExceptionHandlingWebHandler]
Stack trace:
		at org.springframework.security.crypto.password.DelegatingPasswordEncoder$UnmappedIdPasswordEncoder.matches(DelegatingPasswordEncoder.java:250) ~[spring-security-crypto-5.2.5.RELEASE.jar:5.2.5.RELEASE]
		at org.springframework.security.crypto.password.DelegatingPasswordEncoder.matches(DelegatingPasswordEncoder.java:198) ~[spring-security-crypto-5.2.5.RELEASE.jar:5.2.5.RELEASE]
		at org.springframework.security.authentication.AbstractUserDetailsReactiveAuthenticationManager.lambda$authenticate$2(AbstractUserDetailsReactiveAuthenticationManager.java:103) ~[spring-security-core-5.2.5.RELEASE.jar:5.2.5.RELEASE]
		at reactor.core.publisher.FluxFilter$FilterSubscriber.onNext(FluxFilter.java:93) ~[reactor-core-3.3.6.RELEASE.jar:3.3.6.RELEASE]
		at reactor.core.publisher.MonoPublishOn$PublishOnSubscriber.run(MonoPublishOn.java:178) ~[reactor-core-3.3.6.RELEASE.jar:3.3.6.RELEASE]
		at reactor.core.scheduler.SchedulerTask.call(SchedulerTask.java:68) ~[reactor-core-3.3.6.RELEASE.jar:3.3.6.RELEASE]
		at reactor.core.scheduler.SchedulerTask.call(SchedulerTask.java:28) ~[reactor-core-3.3.6.RELEASE.jar:3.3.6.RELEASE]
		at java.util.concurrent.FutureTask.run(FutureTask.java:266) ~[na:1.8.0_251]
		at java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.access$201(ScheduledThreadPoolExecutor.java:180) ~[na:1.8.0_251]
		at java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.run(ScheduledThreadPoolExecutor.java:293) ~[na:1.8.0_251]
		at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149) ~[na:1.8.0_251]
		at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624) ~[na:1.8.0_251]
		at java.lang.Thread.run(Thread.java:748) ~[na:1.8.0_251]

然后各种找资料,发现总结出一点,那就是Spring security 5以后密码存储方式变了,在 Spring Security 5.0.x 以后,密码的一般格式为:{ID} encodedPassword ,ID 主要用于查找 PasswordEncoder 对应的编码标识符,并且encodedPassword 是所选的原始编码密码 PasswordEncoder。ID 必须书写在密码的前面,开始用{,和 结束 }。如果 ID 找不到,ID 则为null。

以下为官方的最新文档,大家可以参考:

Spring Cloud security 官方文档:https://docs.spring.io/spring-security/site/docs/5.3.2.RELEASE/reference/html5/

Spring Cloud security WebFlux 官方文档:https://docs.spring.io/spring-security/site/docs/5.3.2.RELEASE/reference/html5/#servlet-authentication

WebFlux Security 报 java.lang.IllegalArgumentException: There is no PasswordEncoder mapped for the id "null"

解决办法

既然找到了原因,那么我们就有解决的方式,接下来说下如何解决:

1、分析您的用户密码格式

首先要确定的是,您保存的用户密码是否为以下格式:

$2a$10$ZR9ULGIvRPDnl60YL4q7juTFhQlxhAWLGqJTLoN5rzkWLzOPGxjMa

注意:{bcrypt}指的是{id},在你的用户密码中必须存成这种格式

加密后的密码

当然,加密的密码是你在配置WebFlux Security使用的加密方式,我的加密方式为BCryptPasswordEncoder:

    /**
     * 密码加密工具
     * 
     * @return
     */
    @Bean
    public PasswordEncoder passwordEncoder()
    {
        return new BCryptPasswordEncoder();
    }

如果您数据库中保存的密码并非上述格式,那么,请您在用户注册或者初始化密码的时候,使用如下代码初始化密码(注意,如果您使用的并非BCryptPasswordEncoder,请参考官方文档自己修改):

BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
String password = passwordEncoder.encode("1234");//1234为明文密码
String newPwd = "{bcrypt}" + password;//密文密码
System.out.println(password);

经过上面的步骤,您可以继续尝试进行您的操作,看是否还会报There is no PasswordEncoder mapped for the id "null" 的错误,如一切正常,请忽略下方步骤。

2、改为了另一个错误

rawPassword cannot be null

  如果您在第一步中修改后,发现报错变成了rawPassword cannot be null的错误,详细报错信息如下:

java.lang.IllegalArgumentException: rawPassword cannot be null
	at org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder.matches(BCryptPasswordEncoder.java:117) ~[spring-security-crypto-5.2.5.RELEASE.jar:5.2.5.RELEASE]
	Suppressed: reactor.core.publisher.FluxOnAssembly$OnAssemblyException: 
Error has been observed at the following site(s):
	|_ checkpoint ⇢ org.springframework.security.web.server.authentication.AuthenticationWebFilter [DefaultWebFilterChain]
	|_ checkpoint ⇢ org.springframework.security.web.server.authentication.AuthenticationWebFilter [DefaultWebFilterChain]
	|_ checkpoint ⇢ org.springframework.security.web.server.context.ReactorContextWebFilter [DefaultWebFilterChain]
	|_ checkpoint ⇢ org.springframework.security.web.server.header.HttpHeaderWriterWebFilter [DefaultWebFilterChain]
	|_ checkpoint ⇢ org.springframework.security.config.web.server.ServerHttpSecurity$ServerWebExchangeReactorContextWebFilter [DefaultWebFilterChain]
	|_ checkpoint ⇢ org.springframework.security.web.server.WebFilterChainProxy [DefaultWebFilterChain]
	|_ checkpoint ⇢ HTTP POST "/auth/login" [ExceptionHandlingWebHandler]
Stack trace:
		at org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder.matches(BCryptPasswordEncoder.java:117) ~[spring-security-crypto-5.2.5.RELEASE.jar:5.2.5.RELEASE]
		at org.springframework.security.crypto.password.DelegatingPasswordEncoder.matches(DelegatingPasswordEncoder.java:201) ~[spring-security-crypto-5.2.5.RELEASE.jar:5.2.5.RELEASE]
		at org.springframework.security.authentication.AbstractUserDetailsReactiveAuthenticationManager.lambda$authenticate$2(AbstractUserDetailsReactiveAuthenticationManager.java:103) ~[spring-security-core-5.2.5.RELEASE.jar:5.2.5.RELEASE]
		at reactor.core.publisher.FluxFilter$FilterSubscriber.onNext(FluxFilter.java:93) ~[reactor-core-3.3.6.RELEASE.jar:3.3.6.RELEASE]
		at reactor.core.publisher.MonoPublishOn$PublishOnSubscriber.run(MonoPublishOn.java:178) ~[reactor-core-3.3.6.RELEASE.jar:3.3.6.RELEASE]
		at reactor.core.scheduler.SchedulerTask.call(SchedulerTask.java:68) ~[reactor-core-3.3.6.RELEASE.jar:3.3.6.RELEASE]
		at reactor.core.scheduler.SchedulerTask.call(SchedulerTask.java:28) ~[reactor-core-3.3.6.RELEASE.jar:3.3.6.RELEASE]
		at java.util.concurrent.FutureTask.run(FutureTask.java:266) ~[na:1.8.0_251]
		at java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.access$201(ScheduledThreadPoolExecutor.java:180) ~[na:1.8.0_251]
		at java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.run(ScheduledThreadPoolExecutor.java:293) ~[na:1.8.0_251]
		at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149) ~[na:1.8.0_251]
		at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624) ~[na:1.8.0_251]
		at java.lang.Thread.run(Thread.java:748) ~[na:1.8.0_251]

那么恭喜您,咱们已经解决了一个问题,但是又遇到了新问题,接下来可以进入下一个错误排查的阶段了

这个就涉及到Spring security 校验的流程了

1、前端发送请求数据,被AbstractUserDetailsReactiveAuthenticationManager拦截

2、它拿到发送的数据后,用前端传递的用户名调用自定义的用户查询service :SecurityUserDetailsService去查询用户

3、获取到这两个用户后,AbstractUserDetailsReactiveAuthenticationManager将会调用BCryptPasswordEncoder去比对这两个密码是否一致

我们看下它的源码

	@Override
	public Mono<Authentication> authenticate(Authentication authentication) {
		final String username = authentication.getName();
		final String presentedPassword = (String) authentication.getCredentials();
		return retrieveUser(username)
				.doOnNext(this.preAuthenticationChecks::check)
				.publishOn(this.scheduler)
				.filter(u -> this.passwordEncoder.matches(presentedPassword, u.getPassword()))
				.switchIfEmpty(Mono.defer(() -> Mono.error(new BadCredentialsException("Invalid Credentials"))))
				.flatMap(u -> {
					boolean upgradeEncoding = this.userDetailsPasswordService != null
							&& this.passwordEncoder.upgradeEncoding(u.getPassword());
					if (upgradeEncoding) {
						String newPassword = this.passwordEncoder.encode(presentedPassword);
						return this.userDetailsPasswordService.updatePassword(u, newPassword);
					}
					return Mono.just(u);
				})
				.doOnNext(this.postAuthenticationChecks::check)
				.map(u -> new UsernamePasswordAuthenticationToken(u, u.getPassword(), u.getAuthorities()) );
	}

以及比对方法

	public boolean matches(CharSequence rawPassword, String encodedPassword) {
		if (rawPassword == null) {
			throw new IllegalArgumentException("rawPassword cannot be null");
		}

		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);
	}

正如我所说的那样,它没有获取到前端传递的用户密码,那么我们就知道了,前端传递的参数有问题,去检查下前端传递的参数是否为:username、password(注意,除非你在配置类中自定义了这两个参数,否则前端传值必须为这两个)。修改正确后,再次调用,没问题了。

分类   Spring boot 开发
字数   9242

博客标签    WebFlux Security 整合  

评论