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

posted @ 2020-02-08 15:49  狂奔的骆驼  阅读(403)  评论(0编辑  收藏  举报