SpringBoot2.0之Security-Oauth2配置踩坑+源码分析

最近使用SpringBoot2.0新版构建项目,新版的SpringBoot相关依赖的jar很多包结构做了变更,相关依赖也有很多不同,本人负责公司的基础服务,相关登录认证,资源认证采用了开源的 Spring-Security-Oauth2来构建,但是构建过程中会遇到很多坑,所以做此记录。

坑一:

Spring boot 2.0.X引用的security 依赖是 spring security 5.X版本,此版本需要提供一个PasswordEncorder的实例,否则后台汇报错误:

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

: \color{red}{解决方式:}
注解暴露一个PasswordEncorder实例

@Bean
    public BCryptPasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

坑二:

采内存配置clientIdsecret时,请求{{url}}/oauth/token获取token接口时:
WARN [http-nio-8020-exec-2 ] o.s.s.c.b.BCryptPasswordEncoder:90 - [ ] Encoded password does not look like BCrypt
通过debug断点查看首先进行验证的是配置的clientIdsecret,认证的过滤器为BasicAuthenticationFilter阅读源码可知该过滤器主要是对头部header配置的Authorization : Basic XXXX头部信息进行认证。核心代码为:

@Override
	protected void doFilterInternal(HttpServletRequest request,
			HttpServletResponse response, FilterChain chain)
					throws IOException, ServletException {
		final boolean debug = this.logger.isDebugEnabled();
        //获取头部信息Authorization的basic认证信息
		String header = request.getHeader("Authorization");

		if (header == null || !header.toLowerCase().startsWith("basic ")) {
			chain.doFilter(request, response);
			return;
		}

		try {
           //获取头部信息Authorization的basic认证信息(尽心base64解码)
			String[] tokens = extractAndDecodeHeader(header, request);
			assert tokens.length == 2;

			String username = tokens[0];

			if (debug) {
				this.logger
						.debug("Basic Authentication Authorization header found for user '"
								+ username + "'");
			}

			if (authenticationIsRequired(username)) {
				UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(
						username, tokens[1]);
				authRequest.setDetails(
						this.authenticationDetailsSource.buildDetails(request));
              //认证头部信息,通过authenticationManager选择合适的provider尽心认证,失败则抛出异常AuthenticationException
				Authentication authResult = this.authenticationManager
						.authenticate(authRequest);

				if (debug) {
					this.logger.debug("Authentication success: " + authResult);
				}

				SecurityContextHolder.getContext().setAuthentication(authResult);

				this.rememberMeServices.loginSuccess(request, response, authResult);

				onSuccessfulAuthentication(request, response, authResult);
			}

		}
        //根据抛出的异常信息,做不同处理
		catch (AuthenticationException failed) {
			SecurityContextHolder.clearContext();

			if (debug) {
				this.logger.debug("Authentication request for failed: " + failed);
			}

			this.rememberMeServices.loginFail(request, response);

			onUnsuccessfulAuthentication(request, response, failed);

			if (this.ignoreFailure) {
				chain.doFilter(request, response);
			}
			else {
				this.authenticationEntryPoint.commence(request, response, failed);
			}

			return;
		}

		chain.doFilter(request, response);
	}

Basic认证authenticationManager使用的是DaoAuthenticationProvider中父类抽象类AbstractUserDetailsAuthenticationProviderauthenticate认证方法,获取basic认证信息,主要认证核心代码为:

public Authentication authenticate(Authentication authentication)
			throws AuthenticationException {
		Assert.isInstanceOf(UsernamePasswordAuthenticationToken.class, authentication,
				messages.getMessage(
						"AbstractUserDetailsAuthenticationProvider.onlySupports",
						"Only UsernamePasswordAuthenticationToken is supported"));

		// Determine username
		String username = (authentication.getPrincipal() == null) ? "NONE_PROVIDED"
				: authentication.getName();

		boolean cacheWasUsed = true;
		UserDetails user = this.userCache.getUserFromCache(username);

		if (user == null) {
			cacheWasUsed = false;

			try {
                //获取basic认证用户信息即根据clientId获取ClientDetails信息(secret,scope,authorizedGrantTypes.....)
				user = retrieveUser(username,
						(UsernamePasswordAuthenticationToken) authentication);
			}
			catch (UsernameNotFoundException notFound) {
				logger.debug("User '" + username + "' not found");

				if (hideUserNotFoundExceptions) {
					throw new BadCredentialsException(messages.getMessage(
							"AbstractUserDetailsAuthenticationProvider.badCredentials",
							"Bad credentials"));
				}
				else {
					throw notFound;
				}
			}

			Assert.notNull(user,
					"retrieveUser returned null - a violation of the interface contract");
		}

		try {
            //检查是否被lock,是否过期等
			preAuthenticationChecks.check(user);
           //检查clientId和secret是否匹配
			additionalAuthenticationChecks(user,
					(UsernamePasswordAuthenticationToken) authentication);
		}
		catch (AuthenticationException exception) {
			if (cacheWasUsed) {
				// There was a problem, so try again after checking
				// we're using latest data (i.e. not from the cache)
				cacheWasUsed = false;
				user = retrieveUser(username,
						(UsernamePasswordAuthenticationToken) authentication);
				preAuthenticationChecks.check(user);
				additionalAuthenticationChecks(user,
						(UsernamePasswordAuthenticationToken) authentication);
			}
			else {
				throw exception;
			}
		}

		postAuthenticationChecks.check(user);

		if (!cacheWasUsed) {
			this.userCache.putUserInCache(user);
		}

		Object principalToReturn = user;

		if (forcePrincipalAsString) {
			principalToReturn = user.getUsername();
		}

		return createSuccessAuthentication(principalToReturn, authentication, user);
	}

其中检测clientIdsecret是否匹配的核心代码为:

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();
         //此处采用配置的passwordEncoder编码并检查secret是否匹配
		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"));
		}
	}

if (!passwordEncoder.matches(presentedPassword, userDetails.getPassword()))核心认证secret是否配置的代码。
所以我们在memory模式下配置ClientDetailsServiceConfigurer
需要将secret进行passwordEncoder进行encoder处理。
: \color{red}{解决方式:}

@Override
   public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
       clients.inMemory().withClient(clientId).secret(passwordEncoder.encode(secret)).authorizedGrantTypes("password", "refresh_token").scopes("read,write")
           .accessTokenValiditySeconds((int) TimeUnit.DAYS.toSeconds(1)).refreshTokenValiditySeconds((int) TimeUnit.DAYS.toSeconds(7));
   }

passwordEncoder.encode(secret)进行

在这里插入图片描述

发布了41 篇原创文章 · 获赞 14 · 访问量 1万+

猜你喜欢

转载自blog.csdn.net/Yunwei_Zheng/article/details/104018460