在上一篇博客Spring Security原理学习--核心过滤器Filter(二)中我们已经了解到Spring Security相关业务处理的Filter,这篇博客我们通过代码完整的将Spring Security登录、验证和退出的流程了解一下。
一、登录请求
1、Spring Security默认提供了一个登录页面,默认请求地址 GET:http://localhost:8080/login,当请求为GET时最终请求被DefaultLoginPageGeneratingFilter拦截进行处理,返回默认的登录页面
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) req;
HttpServletResponse response = (HttpServletResponse) res;
//登录失败
boolean loginError = isErrorPage(request);
//登出请求
boolean logoutSuccess = isLogoutSuccess(request);
//是否是登录请求
if (isLoginUrlRequest(request) || loginError || logoutSuccess) {
String loginPageHtml = generateLoginPageHtml(request, loginError,
logoutSuccess);
response.setContentType("text/html;charset=UTF-8");
response.setContentLength(loginPageHtml.length());
response.getWriter().write(loginPageHtml);
return;
}
chain.doFilter(request, response);
}
在isLoginUrlRequest(request)方法中,如果判断是/login请求则接下来生成默认的登录页面返回
private boolean isLoginUrlRequest(HttpServletRequest request) {
return matches(request, loginPageUrl);
}
private boolean matches(HttpServletRequest request, String url) {
//首先判断是不是GET请求
if (!"GET".equals(request.getMethod()) || url == null) {
return false;
}
String uri = request.getRequestURI();
int pathParamIndex = uri.indexOf(';');
if (pathParamIndex > 0) {
// strip everything after the first semi-colon
uri = uri.substring(0, pathParamIndex);
}
if (request.getQueryString() != null) {
uri += "?" + request.getQueryString();
}
if ("".equals(request.getContextPath())) {
return uri.equals(url);
}
return uri.equals(request.getContextPath() + url);
}
generateLoginPageHtml方法中生成默认登录页面
private String generateLoginPageHtml(HttpServletRequest request, boolean loginError,
boolean logoutSuccess) {
String errorMsg = "none";
if (loginError) {
HttpSession session = request.getSession(false);
if (session != null) {
AuthenticationException ex = (AuthenticationException) session
.getAttribute(WebAttributes.AUTHENTICATION_EXCEPTION);
errorMsg = ex != null ? ex.getMessage() : "none";
}
}
StringBuilder sb = new StringBuilder();
sb.append("<html><head><title>Login Page</title></head>");
if (formLoginEnabled) {
sb.append("<body onload='document.f.").append(usernameParameter)
.append(".focus();'>\n");
}
if (loginError) {
sb.append("<p><font color='red'>Your login attempt was not successful, try again.<br/><br/>Reason: ");
sb.append(errorMsg);
sb.append("</font></p>");
}
if (logoutSuccess) {
sb.append("<p><font color='green'>You have been logged out</font></p>");
}
if (formLoginEnabled) {
sb.append("<h3>Login with Username and Password</h3>");
sb.append("<form name='f' action='").append(request.getContextPath())
.append(authenticationUrl).append("' method='POST'>\n");
sb.append("<table>\n");
sb.append(" <tr><td>User:</td><td><input type='text' name='");
sb.append(usernameParameter).append("' value='").append("'></td></tr>\n");
sb.append(" <tr><td>Password:</td><td><input type='password' name='")
.append(passwordParameter).append("'/></td></tr>\n");
if (rememberMeParameter != null) {
sb.append(" <tr><td><input type='checkbox' name='")
.append(rememberMeParameter)
.append("'/></td><td>Remember me on this computer.</td></tr>\n");
}
sb.append(" <tr><td colspan='2'><input name=\"submit\" type=\"submit\" value=\"Login\"/></td></tr>\n");
renderHiddenInputs(sb, request);
sb.append("</table>\n");
sb.append("</form>");
}
if (openIdEnabled) {
sb.append("<h3>Login with OpenID Identity</h3>");
sb.append("<form name='oidf' action='").append(request.getContextPath())
.append(openIDauthenticationUrl).append("' method='POST'>\n");
sb.append("<table>\n");
sb.append(" <tr><td>Identity:</td><td><input type='text' size='30' name='");
sb.append(openIDusernameParameter).append("'/></td></tr>\n");
if (openIDrememberMeParameter != null) {
sb.append(" <tr><td><input type='checkbox' name='")
.append(openIDrememberMeParameter)
.append("'></td><td>Remember me on this computer.</td></tr>\n");
}
sb.append(" <tr><td colspan='2'><input name=\"submit\" type=\"submit\" value=\"Login\"/></td></tr>\n");
sb.append("</table>\n");
renderHiddenInputs(sb, request);
sb.append("</form>");
}
sb.append("</body></html>");
return sb.toString();
}
2、当请求地址为POST:http://localhost:8080/login时则为登录操作,POST请求会提交用户名和密码登录信息,此时由UsernamePasswordAuthenticationFilter的父类AbstractAuthenticationProcessingFilter处理,简单来说就是从请求request中获取用户名和密码进行认证操作。
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) req;
HttpServletResponse response = (HttpServletResponse) res;
//当不是登录请求 POST:/login时则直接跳过
if (!requiresAuthentication(request, response)) {
chain.doFilter(request, response);
return;
}
//如果是登录请求 POST:/login则进行验证处理
if (logger.isDebugEnabled()) {
logger.debug("Request is to process authentication");
}
Authentication authResult;
try {
//从请求中获取用户名密码进行校验
authResult = attemptAuthentication(request, response);
if (authResult == null) {
// return immediately as subclass has indicated that it hasn't completed
// authentication
return;
}
sessionStrategy.onAuthentication(authResult, request, response);
}
catch (InternalAuthenticationServiceException failed) {
logger.error(
"An internal error occurred while trying to authenticate the user.",
failed);
//校验失败则跳转到登录页面
unsuccessfulAuthentication(request, response, failed);
return;
}
catch (AuthenticationException failed) {
// Authentication failed
//校验失败则跳转到登录页面
unsuccessfulAuthentication(request, response, failed);
return;
}
// Authentication success
if (continueChainBeforeSuccessfulAuthentication) {
chain.doFilter(request, response);
}
//校验成功则跳转到成功之后页面
successfulAuthentication(request, response, chain, authResult);
}
(1)在doFilter方法中,首先会调用requiresAuthentication方法判断是不是登录请求,如果不是则直接跳过这个Filter,如果是则进行身份验证
(2)如果请求是登录操作,则接下来进行身份验证相关的操作
调用authResult = attemptAuthentication(request, response)方法进行验证,在attemptAuthentication中会从请求中获取用户名和密码构建UsernamePasswordAuthenticationToken对象,然后调用接口AuthenticationManager的authenticate进行验证返回Authentication
public Authentication attemptAuthentication(HttpServletRequest request,
HttpServletResponse response) throws AuthenticationException {
if (postOnly && !request.getMethod().equals("POST")) {
throw new AuthenticationServiceException(
"Authentication method not supported: " + request.getMethod());
}
//获取用户名密码
String username = obtainUsername(request);
String password = obtainPassword(request);
if (username == null) {
username = "";
}
if (password == null) {
password = "";
}
username = username.trim();
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(
username, password);
// Allow subclasses to set the "details" property
setDetails(request, authRequest);
//调用AuthenticationManager的实现类进行校验
return this.getAuthenticationManager().authenticate(authRequest);
}
在接口AuthenticationManager的实现类ProviderManager调用authenticate方法进行校验操作。在authenticate方法中提供了一个List<AuthenticationProvider>,开发者可以提供不同的校验方式(用户名密码、手机号密码、邮箱密码等)只要其中有一个AutenticationProvider调用authenticate方法校验通过即可,当校验不通过时会抛出AuthenticationException ,当所有的AuthenticationProvider校验不通过时,直接抛出异常由ExceptionTranslationFilter捕捉处理,跳转到登录页面。
public Authentication authenticate(Authentication authentication)
throws AuthenticationException {
Class<? extends Authentication> toTest = authentication.getClass();
AuthenticationException lastException = null;
Authentication result = null;
boolean debug = logger.isDebugEnabled();
//可以提供多个验证器,只要其中有一个校验通过接口
for (AuthenticationProvider provider : getProviders()) {
if (!provider.supports(toTest)) {
continue;
}
if (debug) {
logger.debug("Authentication attempt using "
+ provider.getClass().getName());
}
try {
//进行校验,校验不通过则直接抛出AuthenticationException
result = provider.authenticate(authentication);
if (result != null) {
copyDetails(authentication, result);
break;
}
}
catch (AccountStatusException e) {
prepareException(e, authentication);
// SEC-546: Avoid polling additional providers if auth failure is due to
// invalid account status
throw e;
}
catch (InternalAuthenticationServiceException e) {
prepareException(e, authentication);
throw e;
}
catch (AuthenticationException e) {
lastException = e;
}
}
if (result == null && parent != null) {
// Allow the parent to try.
try {
result = parent.authenticate(authentication);
}
catch (ProviderNotFoundException e) {
// ignore as we will throw below if no other exception occurred prior to
// calling parent and the parent
// may throw ProviderNotFound even though a provider in the child already
// handled the request
}
catch (AuthenticationException e) {
lastException = e;
}
}
if (result != null) {
if (eraseCredentialsAfterAuthentication
&& (result instanceof CredentialsContainer)) {
// Authentication is complete. Remove credentials and other secret data
// from authentication
((CredentialsContainer) result).eraseCredentials();
}
eventPublisher.publishAuthenticationSuccess(result);
return result;
}
// Parent was null, or didn't authenticate (or throw an exception).
//最终校验不通过则抛出异常,由ExceptionTranslationFilter捕捉处理
if (lastException == null) {
lastException = new ProviderNotFoundException(messages.getMessage(
"ProviderManager.providerNotFound",
new Object[] { toTest.getName() },
"No AuthenticationProvider found for {0}"));
}
prepareException(lastException, authentication);
throw lastException;
}
在接口AuthenticationProvider的实现类AbstractUserDetailsAuthenticationProvider中调用authenticate进行用户名密码等的校验
(1)首先从缓存userCahce中获取,如果获取到从子类DaoAuthenticationProvider的retirveUser方法中获取
(2)如果获取不到则直接抛出异常
(3)如果匹配成功调用createSuccessAuthentication方法创建Authentication返回。
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 {
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 {
preAuthenticationChecks.check(user);
//判断用户名和密码等信息是否完全匹配
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);
}
在子类DaoAuthenticationProvider中调用接口的UserDetailsService的loadUserByUsername方法根据用户名来查找用户信息(在正式项目中实现此接口来完成从数据库等中查找),如果查找不到还是直接抛出异常,由上层去处理。
protected final UserDetails retrieveUser(String username,
UsernamePasswordAuthenticationToken authentication)
throws AuthenticationException {
UserDetails loadedUser;
try {
//调用默认实现InMemoryUserDetailsManager查找用户名密码
loadedUser = this.getUserDetailsService().loadUserByUsername(username);
}
catch (UsernameNotFoundException notFound) {
if (authentication.getCredentials() != null) {
String presentedPassword = authentication.getCredentials().toString();
passwordEncoder.isPasswordValid(userNotFoundEncodedPassword,
presentedPassword, null);
}
throw notFound;
}
catch (Exception repositoryProblem) {
throw new InternalAuthenticationServiceException(
repositoryProblem.getMessage(), repositoryProblem);
}
if (loadedUser == null) {
throw new InternalAuthenticationServiceException(
"UserDetailsService returned null, which is an interface contract violation");
}
return loadedUser;
}
当找到用户信息后,还需要根据用户信息和password字段进行匹配,在additionalAuthenticationChecks中完成完整的用户名和密码认证。
protected void additionalAuthenticationChecks(UserDetails userDetails,
UsernamePasswordAuthenticationToken authentication)
throws AuthenticationException {
Object salt = null;
if (this.saltSource != null) {
salt = this.saltSource.getSalt(userDetails);
}
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();
if (!passwordEncoder.isPasswordValid(userDetails.getPassword(),
presentedPassword, salt)) {
logger.debug("Authentication failed: password does not match stored value");
throw new BadCredentialsException(messages.getMessage(
"AbstractUserDetailsAuthenticationProvider.badCredentials",
"Bad credentials"));
}
}
在UserDetailService的实现类InMemoryUserDetailsManager中根据用户名获取用户信息。
//根据用户名获取用户信息,最终生成用户名密码和权限等信息
public UserDetails loadUserByUsername(String username)
throws UsernameNotFoundException {
UserDetails user = users.get(username.toLowerCase());
if (user == null) {
throw new UsernameNotFoundException(username);
}
return new User(user.getUsername(), user.getPassword(), user.isEnabled(),
user.isAccountNonExpired(), user.isCredentialsNonExpired(),
user.isAccountNonLocked(), user.getAuthorities());
}
总结:Spring Security的用户名和密码认证主要是在UsernamePasswordAuthenticationFilter进行处理的,在这中间过程中如果认证失败则抛出异常,认证成功则返回Authentication,当然这并接下来还有一个重要的就是角色权限验证,接下来我们用一篇博客进行分析。