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"


注解暴露一个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处理。

@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)进行

微信公众号欢迎关注.jpg

你可能感兴趣的:(SpringBoot2.0之Security-Oauth2配置踩坑+源码分析)