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
-------------------------
技术不在于多么高超先进巧妙,而在于要有现实价值!!!
技术不在于多么高超先进巧妙,而在于要有现实价值!!!