• spring security认证源码分析之账户验证


    当我用spring security,首先就有下面三个问题让我很疑惑:
    1、spring security到底是在哪个环节验证用户密码的?
    2、为什么post一段json形式的口令不能登录?比如 {"username": "test","password": "123"}
    3、为什么代码实现层没有校验密码的地方?

    经过仔细分析spring security的源码和执行逻辑后,搞清楚了问题的答案。见下文。
    spring security的后台代码的执行逻辑主要是通过UsernamePasswordAuthenticationFilter过滤器来实现。

    1、过滤器拦截
    UsernamePasswordAuthenticationFilter类的attemptAuthentication方法:
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
            if (this.postOnly && !request.getMethod().equals("POST")) {
                throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
            } else {
                String username = this.obtainUsername(request);
                String password = this.obtainPassword(request);
                if (username == null) {
                    username = "";
                }
    
                if (password == null) {
                    password = "";
                }
    
                username = username.trim();
                UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);
                this.setDetails(request, authRequest);
                return this.getAuthenticationManager().authenticate(authRequest);  // 1
            }
        }

    通过这个方法得到客户端post过来的账号和密码,存入Authentication

    上面代码中,标红的authenticate是认证的关键方法,标注为 // 1 用数字表示顺序
    这个方法实际上会跳转到下面ProviderManager的authenticate方法:
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
            Class<? extends Authentication> toTest = authentication.getClass();
            AuthenticationException lastException = null;
            Authentication result = null;
            boolean debug = logger.isDebugEnabled();
            Iterator var6 = this.getProviders().iterator();
    
            while(var6.hasNext()) {
                AuthenticationProvider provider = (AuthenticationProvider)var6.next();
                if (provider.supports(toTest)) {
                    if (debug) {
                        logger.debug("Authentication attempt using " + provider.getClass().getName());
                    }
    
                    try {
                        result = provider.authenticate(authentication);   // 3   // 8
                        if (result != null) {
                            this.copyDetails(authentication, result);
                            break;
                        }
                    } catch (AccountStatusException var11) {
                        this.prepareException(var11, authentication);
                        throw var11;
                    } catch (InternalAuthenticationServiceException var12) {
                        this.prepareException(var12, authentication);
                        throw var12;
                    } catch (AuthenticationException var13) {
                        lastException = var13;
                    }
                }
            }
    
            if (result == null && this.parent != null) {
                try {
                    result = this.parent.authenticate(authentication);  // 2
                } catch (ProviderNotFoundException var9) {
                    ;
                } catch (AuthenticationException var10) {
                    lastException = var10;
                }
            }
    
            if (result != null) {
                if (this.eraseCredentialsAfterAuthentication && result instanceof CredentialsContainer) {
                    ((CredentialsContainer)result).eraseCredentials();
                }
    
                this.eventPublisher.publishAuthenticationSuccess(result);   // 9
                return result;
            } else {
                if (lastException == null) {
                    lastException = new ProviderNotFoundException(this.messages.getMessage("ProviderManager.providerNotFound", new Object[]{toTest.getName()}, "No AuthenticationProvider found for {0}"));
                }
    
                this.prepareException((AuthenticationException)lastException, authentication);
                throw lastException;
            }
        }
    
    

    其中又调用了provider类的auth方法:
    result = provider.authenticate(authentication); // 3   // 7


    3、鉴权
    AbstractUserDetailsAuthenticationProvider类的authenticate方法
     public Authentication authenticate(Authentication authentication) throws AuthenticationException {   // 4    
            Assert.isInstanceOf(UsernamePasswordAuthenticationToken.class, authentication, 
    this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.onlySupports",
    "Only UsernamePasswordAuthenticationToken is supported")); String username = authentication.getPrincipal() == null ? "NONE_PROVIDED" : authentication.getName(); boolean cacheWasUsed = true; UserDetails user = this.userCache.getUserFromCache(username); if (user == null) { cacheWasUsed = false; try { user = this.retrieveUser(username, (UsernamePasswordAuthenticationToken)authentication); // 5 } catch (UsernameNotFoundException var6) { this.logger.debug("User '" + username + "' not found"); if (this.hideUserNotFoundExceptions) { throw new BadCredentialsException(this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials")); } throw var6; } Assert.notNull(user, "retrieveUser returned null - a violation of the interface contract"); } try { this.preAuthenticationChecks.check(user); this.additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken)authentication); // 6 验证密码关键步骤 } catch (AuthenticationException var7) { if (!cacheWasUsed) { throw var7; } cacheWasUsed = false; user = this.retrieveUser(username, (UsernamePasswordAuthenticationToken)authentication); this.preAuthenticationChecks.check(user); this.additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken)authentication); } this.postAuthenticationChecks.check(user); if (!cacheWasUsed) { this.userCache.putUserInCache(user); } Object principalToReturn = user; if (this.forcePrincipalAsString) { principalToReturn = user.getUsername(); } return this.createSuccessAuthentication(principalToReturn, authentication, user); // 7 }

     这一步就是验证账户密码的关键的步骤,

    Authetication里面存了客户端的发过来的用户和密码,
    服务端数据库的用户和密码存入UserDetails
    先从缓存里面拿,如果有,就返回,
    UserDetails user = this.userCache.getUserFromCache(username);

    如果缓存里面没有原始的账户和密码,就查数据库:
    user = this.retrieveUser(username, (UsernamePasswordAuthenticationToken)authentication);

    校验

    真正校验账户和密码是在这一步:

    DaoAuthenticationProvider
    protected void additionalAuthenticationChecks(UserDetails userDetails, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {  //6
            Object salt = null;
            if (this.saltSource != null) {
                salt = this.saltSource.getSalt(userDetails);
            }
    
            if (authentication.getCredentials() == null) {
                this.logger.debug("Authentication failed: no credentials provided");
                throw new BadCredentialsException(this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
            } else {
                String presentedPassword = authentication.getCredentials().toString();
                if (!this.passwordEncoder.isPasswordValid(userDetails.getPassword(), presentedPassword, salt)) {
                    this.logger.debug("Authentication failed: password does not match stored value");
                    throw new BadCredentialsException(this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
                }
            }
        }

    数据库的原始密码就是:
    String presentedPassword = authentication.getCredentials().toString();

    最后到了AbstractAuthenticationProcessingFilter
    public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
            HttpServletRequest request = (HttpServletRequest)req;
            HttpServletResponse response = (HttpServletResponse)res;
            if (!this.requiresAuthentication(request, response)) {
                chain.doFilter(request, response);
            } else {
                if (this.logger.isDebugEnabled()) {
                    this.logger.debug("Request is to process authentication");
                }
    
                Authentication authResult;
                try {
                    authResult = this.attemptAuthentication(request, response);   // 10
                    if (authResult == null) {
                        return;
                    }
    
                    this.sessionStrategy.onAuthentication(authResult, request, response);
                } catch (InternalAuthenticationServiceException var8) {
                    this.logger.error("An internal error occurred while trying to authenticate the user.", var8);
                    this.unsuccessfulAuthentication(request, response, var8);
                    return;
                } catch (AuthenticationException var9) {
                    this.unsuccessfulAuthentication(request, response, var9);
                    return;
                }
    
                if (this.continueChainBeforeSuccessfulAuthentication) {
                    chain.doFilter(request, response);
                }
    
                this.successfulAuthentication(request, response, chain, authResult);  // 11
            }
        }
    到了第11步,spring security的逻辑就走完了,接下来是其他的filter的跳出逻辑了,略过。


    这11步就是包含了完整spring security的验证逻辑。

    测试用例

    值得注意的是,spring securiy有三种方式进行测试
    shell命令(测试登录):
    curl 
    -X POST 
    -H "Content-Type:application/x-www-form-urlencoded" 
    --data "username=thy&password=1231" 
    http://localhost:8081/api/login 

    或者:
    curl 
    -X POST 
    -H "Content-Type:application/x-www-form-urlencoded" 
    http://localhost:8081/api/login?username=thy&password=1231


    或者(shell执行,注意&前面有转义符)
    curl -X POST http://localhost:8081/api/login?username=thy&password=1231
    
    
  • 相关阅读:
    jQuery实现图片前进后退
    jQuery写日历
    python列表模拟栈
    python 列表去重
    Linux的文件系统
    新建vss数据库
    关于业务用例和系统用例
    从零开始使用Linux命令
    svn的安装与配置
    数塔 动态规划
  • 原文地址:https://www.cnblogs.com/geektcp/p/12283662.html
Copyright © 2020-2023  润新知