3.流程分析
3.1 流程分析
-
client请求 /hello 接口,在引入spring security之后会先经过一系列过滤器(默认15个)。
-
在请求到达FilterSecurityInterceptor(检查认证授权)时,发现请求并未认证,请求被拦截下来,并抛出AccessDeniedException异常。
-
抛出 AccessDeniedException的异常会被ExceptionTranslationFilter捕获,这个 Filter中会调用 LoginUrlAuthenticationEntryPoint#commence 方法给客户端返回
302(请求重定向),要求客户端进行重定向到 /login页面。 -
客户端再次发送 /login 请求。
-
/login请求会再次被拦截器中DefaultLoginPageGeneratingFilter拦截到,并在拦截器中返回生成登录页面(纯代码拼接而成的页面)。
就是通过这种方式,Spring Security 默认过滤器中生成了登录页面,并返回!
3.2登陆页面
DefaultLoginPageGeneratingFilter.java
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//
package org.springframework.security.web.authentication.ui;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.Collections;
import java.util.Iterator;
import java.util.Map;
import java.util.function.Function;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.authentication.rememberme.AbstractRememberMeServices;
import org.springframework.util.Assert;
import org.springframework.web.filter.GenericFilterBean;
import org.springframework.web.util.HtmlUtils;
public class DefaultLoginPageGeneratingFilter extends GenericFilterBean {
public static final String DEFAULT_LOGIN_PAGE_URL = "/login";
public static final String ERROR_PARAMETER_NAME = "error";
private String loginPageUrl;
private String logoutSuccessUrl;
private String failureUrl;
private boolean formLoginEnabled;
private boolean openIdEnabled;
private boolean oauth2LoginEnabled;
private boolean saml2LoginEnabled;
private String authenticationUrl;
private String usernameParameter;
private String passwordParameter;
private String rememberMeParameter;
private String openIDauthenticationUrl;
private String openIDusernameParameter;
private String openIDrememberMeParameter;
private Map<String, String> oauth2AuthenticationUrlToClientName;
private Map<String, String> saml2AuthenticationUrlToProviderName;
private Function<HttpServletRequest, Map<String, String>> resolveHiddenInputs = (request) -> {
return Collections.emptyMap();
};
public DefaultLoginPageGeneratingFilter() {
}
public DefaultLoginPageGeneratingFilter(AbstractAuthenticationProcessingFilter filter) {
if (filter instanceof UsernamePasswordAuthenticationFilter) {
this.init((UsernamePasswordAuthenticationFilter)filter, (AbstractAuthenticationProcessingFilter)null);
} else {
this.init((UsernamePasswordAuthenticationFilter)null, filter);
}
}
public DefaultLoginPageGeneratingFilter(UsernamePasswordAuthenticationFilter authFilter, AbstractAuthenticationProcessingFilter openIDFilter) {
this.init(authFilter, openIDFilter);
}
private void init(UsernamePasswordAuthenticationFilter authFilter, AbstractAuthenticationProcessingFilter openIDFilter) {
this.loginPageUrl = "/login";
this.logoutSuccessUrl = "/login?logout";
this.failureUrl = "/login?error";
if (authFilter != null) {
this.initAuthFilter(authFilter);
}
if (openIDFilter != null) {
this.initOpenIdFilter(openIDFilter);
}
}
private void initAuthFilter(UsernamePasswordAuthenticationFilter authFilter) {
this.formLoginEnabled = true;
this.usernameParameter = authFilter.getUsernameParameter();
this.passwordParameter = authFilter.getPasswordParameter();
if (authFilter.getRememberMeServices() instanceof AbstractRememberMeServices) {
this.rememberMeParameter = ((AbstractRememberMeServices)authFilter.getRememberMeServices()).getParameter();
}
}
private void initOpenIdFilter(AbstractAuthenticationProcessingFilter openIDFilter) {
this.openIdEnabled = true;
this.openIDusernameParameter = "openid_identifier";
if (openIDFilter.getRememberMeServices() instanceof AbstractRememberMeServices) {
this.openIDrememberMeParameter = ((AbstractRememberMeServices)openIDFilter.getRememberMeServices()).getParameter();
}
}
public void setResolveHiddenInputs(Function<HttpServletRequest, Map<String, String>> resolveHiddenInputs) {
Assert.notNull(resolveHiddenInputs, "resolveHiddenInputs cannot be null");
this.resolveHiddenInputs = resolveHiddenInputs;
}
public boolean isEnabled() {
return this.formLoginEnabled || this.openIdEnabled || this.oauth2LoginEnabled || this.saml2LoginEnabled;
}
public void setLogoutSuccessUrl(String logoutSuccessUrl) {
this.logoutSuccessUrl = logoutSuccessUrl;
}
public String getLoginPageUrl() {
return this.loginPageUrl;
}
public void setLoginPageUrl(String loginPageUrl) {
this.loginPageUrl = loginPageUrl;
}
public void setFailureUrl(String failureUrl) {
this.failureUrl = failureUrl;
}
public void setFormLoginEnabled(boolean formLoginEnabled) {
this.formLoginEnabled = formLoginEnabled;
}
public void setOpenIdEnabled(boolean openIdEnabled) {
this.openIdEnabled = openIdEnabled;
}
public void setOauth2LoginEnabled(boolean oauth2LoginEnabled) {
this.oauth2LoginEnabled = oauth2LoginEnabled;
}
public void setSaml2LoginEnabled(boolean saml2LoginEnabled) {
this.saml2LoginEnabled = saml2LoginEnabled;
}
public void setAuthenticationUrl(String authenticationUrl) {
this.authenticationUrl = authenticationUrl;
}
public void setUsernameParameter(String usernameParameter) {
this.usernameParameter = usernameParameter;
}
public void setPasswordParameter(String passwordParameter) {
this.passwordParameter = passwordParameter;
}
public void setRememberMeParameter(String rememberMeParameter) {
this.rememberMeParameter = rememberMeParameter;
this.openIDrememberMeParameter = rememberMeParameter;
}
public void setOpenIDauthenticationUrl(String openIDauthenticationUrl) {
this.openIDauthenticationUrl = openIDauthenticationUrl;
}
public void setOpenIDusernameParameter(String openIDusernameParameter) {
this.openIDusernameParameter = openIDusernameParameter;
}
public void setOauth2AuthenticationUrlToClientName(Map<String, String> oauth2AuthenticationUrlToClientName) {
this.oauth2AuthenticationUrlToClientName = oauth2AuthenticationUrlToClientName;
}
public void setSaml2AuthenticationUrlToProviderName(Map<String, String> saml2AuthenticationUrlToProviderName) {
this.saml2AuthenticationUrlToProviderName = saml2AuthenticationUrlToProviderName;
}
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
this.doFilter((HttpServletRequest)request, (HttpServletResponse)response, chain);
}
private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
boolean loginError = this.isErrorPage(request);
boolean logoutSuccess = this.isLogoutSuccess(request);
if (!this.isLoginUrlRequest(request) && !loginError && !logoutSuccess) {
chain.doFilter(request, response);
} else {
String loginPageHtml = this.generateLoginPageHtml(request, loginError, logoutSuccess);
response.setContentType("text/html;charset=UTF-8");
response.setContentLength(loginPageHtml.getBytes(StandardCharsets.UTF_8).length);
response.getWriter().write(loginPageHtml);
}
}
private String generateLoginPageHtml(HttpServletRequest request, boolean loginError, boolean logoutSuccess) {
String errorMsg = "Invalid credentials";
if (loginError) {
HttpSession session = request.getSession(false);
if (session != null) {
AuthenticationException ex = (AuthenticationException)session.getAttribute("SPRING_SECURITY_LAST_EXCEPTION");
errorMsg = ex != null ? ex.getMessage() : "Invalid credentials";
}
}
String contextPath = request.getContextPath();
StringBuilder sb = new StringBuilder();
sb.append("<!DOCTYPE html>\n");
sb.append("<html lang=\"en\">\n");
sb.append(" <head>\n");
sb.append(" <meta charset=\"utf-8\">\n");
sb.append(" <meta name=\"viewport\" content=\"width=device-width, initial-scale=1, shrink-to-fit=no\">\n");
sb.append(" <meta name=\"description\" content=\"\">\n");
sb.append(" <meta name=\"author\" content=\"\">\n");
sb.append(" <title>Please sign in</title>\n");
sb.append(" <link href=\"https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-beta/css/bootstrap.min.css\" rel=\"stylesheet\" integrity=\"sha384-/Y6pD6FV/Vv2HJnA6t+vslU6fwYXjCFtcEpHbNJ0lyAFsXTsjBbfaDjzALeQsN6M\" crossorigin=\"anonymous\">\n");
sb.append(" <link href=\"https://getbootstrap.com/docs/4.0/examples/signin/signin.css\" rel=\"stylesheet\" crossorigin=\"anonymous\"/>\n");
sb.append(" </head>\n");
sb.append(" <body>\n");
sb.append(" <div class=\"container\">\n");
if (this.formLoginEnabled) {
sb.append(" <form class=\"form-signin\" method=\"post\" action=\"" + contextPath + this.authenticationUrl + "\">\n");
sb.append(" <h2 class=\"form-signin-heading\">Please sign in</h2>\n");
sb.append(createError(loginError, errorMsg) + createLogoutSuccess(logoutSuccess) + " <p>\n");
sb.append(" <label for=\"username\" class=\"sr-only\">Username</label>\n");
sb.append(" <input type=\"text\" id=\"username\" name=\"" + this.usernameParameter + "\" class=\"form-control\" placeholder=\"Username\" required autofocus>\n");
sb.append(" </p>\n");
sb.append(" <p>\n");
sb.append(" <label for=\"password\" class=\"sr-only\">Password</label>\n");
sb.append(" <input type=\"password\" id=\"password\" name=\"" + this.passwordParameter + "\" class=\"form-control\" placeholder=\"Password\" required>\n");
sb.append(" </p>\n");
sb.append(this.createRememberMe(this.rememberMeParameter) + this.renderHiddenInputs(request));
sb.append(" <button class=\"btn btn-lg btn-primary btn-block\" type=\"submit\">Sign in</button>\n");
sb.append(" </form>\n");
}
if (this.openIdEnabled) {
sb.append(" <form name=\"oidf\" class=\"form-signin\" method=\"post\" action=\"" + contextPath + this.openIDauthenticationUrl + "\">\n");
sb.append(" <h2 class=\"form-signin-heading\">Login with OpenID Identity</h2>\n");
sb.append(createError(loginError, errorMsg) + createLogoutSuccess(logoutSuccess) + " <p>\n");
sb.append(" <label for=\"username\" class=\"sr-only\">Identity</label>\n");
sb.append(" <input type=\"text\" id=\"username\" name=\"" + this.openIDusernameParameter + "\" class=\"form-control\" placeholder=\"Username\" required autofocus>\n");
sb.append(" </p>\n");
sb.append(this.createRememberMe(this.openIDrememberMeParameter) + this.renderHiddenInputs(request));
sb.append(" <button class=\"btn btn-lg btn-primary btn-block\" type=\"submit\">Sign in</button>\n");
sb.append(" </form>\n");
}
Iterator var7;
Map.Entry relyingPartyUrlToName;
String url;
String partyName;
if (this.oauth2LoginEnabled) {
sb.append("<h2 class=\"form-signin-heading\">Login with OAuth 2.0</h2>");
sb.append(createError(loginError, errorMsg));
sb.append(createLogoutSuccess(logoutSuccess));
sb.append("<table class=\"table table-striped\">\n");
var7 = this.oauth2AuthenticationUrlToClientName.entrySet().iterator();
while(var7.hasNext()) {
relyingPartyUrlToName = (Map.Entry)var7.next();
sb.append(" <tr><td>");
url = (String)relyingPartyUrlToName.getKey();
sb.append("<a href=\"").append(contextPath).append(url).append("\">");
partyName = HtmlUtils.htmlEscape((String)relyingPartyUrlToName.getValue());
sb.append(partyName);
sb.append("</a>");
sb.append("</td></tr>\n");
}
sb.append("</table>\n");
}
if (this.saml2LoginEnabled) {
sb.append("<h2 class=\"form-signin-heading\">Login with SAML 2.0</h2>");
sb.append(createError(loginError, errorMsg));
sb.append(createLogoutSuccess(logoutSuccess));
sb.append("<table class=\"table table-striped\">\n");
var7 = this.saml2AuthenticationUrlToProviderName.entrySet().iterator();
while(var7.hasNext()) {
relyingPartyUrlToName = (Map.Entry)var7.next();
sb.append(" <tr><td>");
url = (String)relyingPartyUrlToName.getKey();
sb.append("<a href=\"").append(contextPath).append(url).append("\">");
partyName = HtmlUtils.htmlEscape((String)relyingPartyUrlToName.getValue());
sb.append(partyName);
sb.append("</a>");
sb.append("</td></tr>\n");
}
sb.append("</table>\n");
}
sb.append("</div>\n");
sb.append("</body></html>");
return sb.toString();
}
private String renderHiddenInputs(HttpServletRequest request) {
StringBuilder sb = new StringBuilder();
Iterator var3 = ((Map)this.resolveHiddenInputs.apply(request)).entrySet().iterator();
while(var3.hasNext()) {
Map.Entry<String, String> input = (Map.Entry)var3.next();
sb.append("<input name=\"");
sb.append((String)input.getKey());
sb.append("\" type=\"hidden\" value=\"");
sb.append((String)input.getValue());
sb.append("\" />\n");
}
return sb.toString();
}
private String createRememberMe(String paramName) {
return paramName == null ? "" : "<p><input type='checkbox' name='" + paramName + "'/> Remember me on this computer.</p>\n";
}
private boolean isLogoutSuccess(HttpServletRequest request) {
return this.logoutSuccessUrl != null && this.matches(request, this.logoutSuccessUrl);
}
private boolean isLoginUrlRequest(HttpServletRequest request) {
return this.matches(request, this.loginPageUrl);
}
private boolean isErrorPage(HttpServletRequest request) {
return this.matches(request, this.failureUrl);
}
private static String createError(boolean isError, String message) {
return !isError ? "" : "<div class=\"alert alert-danger\" role=\"alert\">" + HtmlUtils.htmlEscape(message) + "</div>";
}
private static String createLogoutSuccess(boolean isLogoutSuccess) {
return !isLogoutSuccess ? "" : "<div class=\"alert alert-success\" role=\"alert\">You have been signed out</div>";
}
private boolean matches(HttpServletRequest request, String url) {
if ("GET".equals(request.getMethod()) && url != null) {
String uri = request.getRequestURI();
int pathParamIndex = uri.indexOf(59);
if (pathParamIndex > 0) {
uri = uri.substring(0, pathParamIndex);
}
if (request.getQueryString() != null) {
uri = uri + "?" + request.getQueryString();
}
return "".equals(request.getContextPath()) ? uri.equals(url) : uri.equals(request.getContextPath() + url);
} else {
return false;
}
}
}
3.3 AuthenticationFilter
1.SpringBootWebSecurityConfiguration的defaultSecurityFilterChain 方法表单登录
2.进入到HttpSecurity的formLogin方法,然后应用返回一个配置FormLoginConfigurer
3.getOrApply方法
4.处理登录为 FormLoginConfigurer 类中的空参构造器,调用父类构造方法传递给父类这个UsernamePasswordAuthenticationFilter,调用 UsernamePasswordAuthenticationFilter(基于用户名密码登录过滤器)这个类实例 ,同时执行usernameParameter方法和passwordParameter方法
4.父类构造方法,两个参数:1.认证过滤器;2.登录URL
5.usernameParameter方法 ,会调用父类的AbstractAuthenticationFilterConfigurer
getAuthenticationFilter方法
6.父类的getAuthenticationFilter方法会返回 刚才通过构造器传入的UsernamePasswordAuthenticationFilter
7.对filter的属性进行赋值
3.4 AuthenticationManager
1.通过请求断点的方式,可以得知请求会进入到UsernamePasswordAuthenticationFilter的attemptAuthentication方法,由于UsernamePasswordAuthenticationFilter是security提供的filter并不是原生的filter,所以没有dofilter方法。
2.获取请求和响应对象,判断请求方式POST,如果不是post方法会抛出一个异常提示当前请求方法不支持,如果是post方法则将username和password封装成为一个认证token对象UsernamePasswordAuthenticationToken,然后调用本类的getAuthenticationManager方法(继承于父类的AbstractAuthenticationProcessingFilter)来获取一个AuthenticationManager(用来认证)。
而AuthenticationManager的authenticate方法是一个接口中定义的方法
事实上会调用AuthenticationManager 的默认实现类ProviderManager重写的authenticate 方法
ProviderManager.java
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
Class<? extends Authentication> toTest = authentication.getClass();//变量保存了要进行身份验证的对象的类信息。
AuthenticationException lastException = null;// 变量保存了身份验证过程中捕获的异常
AuthenticationException parentException = null; //变量保存了身份验证过程中捕获的异常
Authentication result = null; //变量保存了身份验证成功的结果
Authentication parentResult = null; //变量保存了身份验证成功的结果
int currentPosition = 0; //记录当前正在验证的身份验证提供者的位置。
int size = this.providers.size(); //记录 providers 集合中的提供者数量
//for 循环遍历 providers 集合中的身份验证提供者进行身份验证
for (AuthenticationProvider provider : getProviders()) {
//检查当前的身份验证提供者provider是否支持要验证的对象的类(!provider.supports(toTest),不支持,遍历下一个
if (!provider.supports(toTest)) {
continue;
}
if (logger.isTraceEnabled()) {
logger.trace(LogMessage.format("Authenticating request with %s (%d/%d)",
provider.getClass().getSimpleName(), ++currentPosition, size));
}
//支持,就尝试使用当前的身份验证提供者进行身份验证(provider.authenticate(authentication))。
try {
//调用provider的authenticat方法认证
result = provider.authenticate(authentication);
if (result != null) {
//验证成功,将得到的结果拷贝到 result 中,并使用 copyDetails() 方法将验证请求中的详细信息拷贝到 result 中。然后跳出循环。
copyDetails(authentication, result);
break;
}
}
//如果验证过程中捕获到 AccountStatusException 或 InternalAuthenticationServiceException 异常,说明发生了账户状态异常或内部认证服务异常。这种情况下,将异常处理后抛出。
catch (AccountStatusException | InternalAuthenticationServiceException ex) {
prepareException(ex, authentication);
// SEC-546: Avoid polling additional providers if auth failure is due to
// invalid account status
throw ex;
}
//如果捕获到其他类型的 AuthenticationException 异常,将其保存到 lastException 变量中。
catch (AuthenticationException ex) {
lastException = ex;
}
}
//上述循环中没有成功验证的结果,并且存在父 AuthenticationManager,则尝试使用父 AuthenticationManager 进行身份验证。
if (result == null && this.parent != null) {
// Allow the parent to try.
try {
//如果存在父 AuthenticationManager,就尝试使用父 AuthenticationManager 进行身份验证 (parentResult = this.parent.authenticate(authentication))。
parentResult = this.parent.authenticate(authentication);
result = parentResult;
}
//如果父 AuthenticationManager 抛出 ProviderNotFoundException 异常,则忽略该异常。
catch (ProviderNotFoundException ex) {
// 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
}
//如果父 AuthenticationManager 抛出其他类型的 AuthenticationException 异常,将异常保存到 parentException 和 lastException 变量中。最后根据验证结果进行返回。
catch (AuthenticationException ex) {
parentException = ex;
lastException = ex;
}
}
//如果存在验证成功的结果 (result != null),则执行一些后续操作,比如根据配置决定是否擦除验证结果中的凭证信息、是否发布身份验证成功事件等。然后返回验证结果。
if (result != null) {
if (this.eraseCredentialsAfterAuthentication && (result instanceof CredentialsContainer)) {
// Authentication is complete. Remove credentials and other secret data
// from authentication
((CredentialsContainer) result).eraseCredentials();
}
// If the parent AuthenticationManager was attempted and successful then it
// will publish an AuthenticationSuccessEvent
// This check prevents a duplicate AuthenticationSuccessEvent if the parent
// AuthenticationManager already published it
if (parentResult == null) {
this.eventPublisher.publishAuthenticationSuccess(result);
}
return result;
}
// Parent was null, or didn't authenticate (or throw an exception).
if (lastException == null) {
lastException = new ProviderNotFoundException(this.messages.getMessage("ProviderManager.providerNotFound",
new Object[] { toTest.getName() }, "No AuthenticationProvider found for {0}"));
}
// If the parent AuthenticationManager was attempted and failed then it will
// publish an AbstractAuthenticationFailureEvent
// This check prevents a duplicate AbstractAuthenticationFailureEvent if the
// parent AuthenticationManager already published it
if (parentException == null) {
prepareException(lastException, authentication);
}
throw lastException;
}
Authentication
Authentication是一个接口,通过该接口可以获得用户相关信息、安全实体的标识以及认证请求的上下文信息等
在Spring Security中,有很多Authentication的实现类。如UsernamePasswordAuthenticationToken、AnonymousAuthenticationToken和RememberMeAuthenticationToken等等
通常不会被扩展,除非是为了支持某种特定类型的认证
public interface Authentication extends Principal, Serializable {
//权限结合,可使用AuthorityUtils.commaSeparatedStringToAuthorityList("admin, ROLE_ADMIN")返回字符串权限集合
Collection<? extends GrantedAuthority> getAuthorities();
//用户名密码认证时可以理解为密码
Object getCredentials();
//认证时包含的一些信息。如remoteAddress、sessionId
Object getDetails();
//用户名密码认证时可理解时用户名
Object getPrincipal();
//是否被认证,认证为true
boolean isAuthenticated();
//设置是否被认证
void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException;
}
3.5 AuthenticationProvider
其中的调用AuthenticationProvider的authenticate是一个接口中的方法,实际上是调用AbstractUserDetailsAuthenticationProvider的实现类DaoAuthenticationProvider的authenticate方法,由于子类没有重写父类的authenticate方法,所以执行的还是父类的AbstractUserDetailsAuthenticationProvider的authenticate方法。
AbstractUserDetailsAuthenticationProvider.java
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
//检查传入的 authentication 对象是否为 UsernamePasswordAuthenticationToken 的实例。如果不是,则抛出异常。
Assert.isInstanceOf(UsernamePasswordAuthenticationToken.class, authentication,
() -> this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.onlySupports",
"Only UsernamePasswordAuthenticationToken is supported"));
//确定要进行身份验证的用户名
String username = determineUsername(authentication);
boolean cacheWasUsed = true;
//使用缓存从 userCache 中获取用户的 UserDetails 对象。
UserDetails user = this.userCache.getUserFromCache(username);
if (user == null) {
cacheWasUsed = false;
//如果缓存中没有用户信息,则标记缓存未被使用,并调用 retrieveUser() 方法从数据源中获取用户信息。
try {
user = retrieveUser(username, (UsernamePasswordAuthenticationToken) authentication);
}
//无法找到用户信息(抛出 UsernameNotFoundException 异常),根据配置决定是否抛出具体的异常信息或统一抛出 BadCredentialsException 异常
catch (UsernameNotFoundException ex) {
this.logger.debug("Failed to find user '" + username + "'");
if (!this.hideUserNotFoundExceptions) {
throw ex;
}
throw new BadCredentialsException(this.messages
.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
}
Assert.notNull(user, "retrieveUser returned null - a violation of the interface contract");
}
//进行用户的预认证检查(preAuthenticationChecks)和额外的身份验证检查(additionalAuthenticationChecks)
try {
this.preAuthenticationChecks.check(user);
additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken) authentication);
}
catch (AuthenticationException ex) {
//身份验证过程中出现了异常,说明可能是缓存数据不一致,因此再次从数据源中获取用户信息并进行预认证检查和额外的身份验证检查
if (!cacheWasUsed) {
throw ex;
}
// 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);
this.preAuthenticationChecks.check(user);
additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken) authentication);
}
//进行用户的后认证检查
this.postAuthenticationChecks.check(user);
//如果缓存未被使用,则将用户信息放入缓存中
if (!cacheWasUsed) {
this.userCache.putUserInCache(user);
}
Object principalToReturn = user;
//根据配置决定返回的主体对象是完整的 UserDetails 还是仅包含用户名
if (this.forcePrincipalAsString) {
principalToReturn = user.getUsername();
}
//调用 createSuccessAuthentication() 方法创建验证成功的 Authentication 对象,并将其返回
return createSuccessAuthentication(principalToReturn, authentication, user);
}
3.6 UserDetailsService
其中的retrieveUser是当前类AbstractUserDetailsAuthenticationProvider的一个方法没有实现,所以实际调用的是AbstractUserDetailsAuthenticationProvider的子类DaoAuthenticationProvider的retrieveUser,
retrieveUser从用户详细信息服务(UserDetailsService)中获取用户的详细信息
DaoAuthenticationProvider.java
@Override
protected final UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication)
throws AuthenticationException {
//调用 prepareTimingAttackProtection() 方法来准备防止时序攻击的措施
prepareTimingAttackProtection();
try {
//从用户详细信息服务中根据用户名获取用户详细信息
UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username);
//获取到的用户详细信息为 null,抛出 InternalAuthenticationServiceException 异常,表示用户详细信息服务返回了一个违反接口规范的结果
if (loadedUser == null) {
throw new InternalAuthenticationServiceException(
"UserDetailsService returned null, which is an interface contract violation");
}
return loadedUser;
}
//如果抛出了 UsernameNotFoundException 异常,表明用户详细信息服务无法找到对应的用户,通过调用 mitigateAgainstTimingAttack(authentication) 方法来防止时序攻击,并把异常继续抛出。
catch (UsernameNotFoundException ex) {
mitigateAgainstTimingAttack(authentication);
throw ex;
}
catch (InternalAuthenticationServiceException ex) {
throw ex;
}
catch (Exception ex) {
throw new InternalAuthenticationServiceException(ex.getMessage(), ex);
}
}
UserDetails
UserDetails也是一个接口,主要封装用户名密码是否过期、是否可用等信息
public interface UserDetails extends Serializable {
//权限集合
Collection<? extends GrantedAuthority> getAuthorities();
//密码
String getPassword();
//用户名
String getUsername();
//用户名是否没有过期
boolean isAccountNonExpired();
//用户名是否没有锁定
boolean isAccountNonLocked();
//用户密码是否没有过期
boolean isCredentialsNonExpired();
//账号是否可用(可理解为是否删除)
boolean isEnabled();
}
通过断点的方式可以知道调用的实际是UserDetailsService的实现类InMemoryUserDetailsManager的loadUserByUsername方法
看到这里就知道默认实现是基于 InMemoryUserDetailsManager 这个类,也就是内存的实现!
UserDetailService
通过刚才源码分析也能得知 UserDetailService 是顶层父接口,接口中 loadUserByUserName 方法是用来在认证时进行用户名认证方法,默认实现使用是内存实现,如果想要修改数据库实现我们只需要自定义 UserDetailService 实现,最终返回 UserDetails 实例即可。
public interface UserDetailsService {
UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}
UserDetailServiceAutoConfigutation
这个源码非常多,这里梳理了关键部分:
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(AuthenticationManager.class)
@ConditionalOnBean(ObjectPostProcessor.class)
@ConditionalOnMissingBean(
value = { AuthenticationManager.class, AuthenticationProvider.class, UserDetailsService.class,
AuthenticationManagerResolver.class },
type = { "org.springframework.security.oauth2.jwt.JwtDecoder",
"org.springframework.security.oauth2.server.resource.introspection.OpaqueTokenIntrospector",
"org.springframework.security.oauth2.client.registration.ClientRegistrationRepository" })
public class UserDetailsServiceAutoConfiguration {
//....
@Bean
@Lazy
public InMemoryUserDetailsManager inMemoryUserDetailsManager(SecurityProperties properties,
ObjectProvider<PasswordEncoder> passwordEncoder) {
SecurityProperties.User user = properties.getUser();
List<String> roles = user.getRoles();
return new InMemoryUserDetailsManager(
User.withUsername(user.getName()).password(getOrDeducePassword(user, passwordEncoder.getIfAvailable()))
.roles(StringUtils.toStringArray(roles)).build());
}
//...
}
结论
-
从自动配置源码中得知当 classpath 下存在 AuthenticationManager 类;
-
当前项目中,系统没有提供 AuthenticationManager.class、 AuthenticationProvider.class、UserDetailsService.class、AuthenticationManagerResolver.class实例。
默认情况下都会满足,此时Spring Security会提供一个 InMemoryUserDetailManager 实例
@ConfigurationProperties(prefix = "spring.security")
public class SecurityProperties {
private final User user = new User();
public User getUser() {
return this.user;
}
//....
public static class User {
private String name = "user";
private String password = UUID.randomUUID().toString();
private List<String> roles = new ArrayList<>();
private boolean passwordGenerated = true;
//get set ...
}
}
这就是默认生成 user 以及 uuid 密码过程!
另外看明白源码之后,就知道只要在配置文件中加入如下配置可以对内存中用户和密码进行覆盖。
spring.security.user.name=root
spring.security.user.password=root
spring.security.user.roles=admin,users
重新启动项目后输入配置文件中的用户名和密码即可正常访问,原有的登录方式失效!
总结
三个认证类之间的关系
从上面分析中得知,AuthenticationManager 是认证的核心类,但实际上在底层真正认证时还离不开 ProviderManager 以及 AuthenticationProvider 。他们三者关系是样的呢?
- AuthenticationManager 是一个认证管理器,它定义了 Spring Security 过滤器要执行认证操作。
- ProviderManager是AuthenticationManager接口的实现类。Spring Security认证时默认使用就是 ProviderManager。
- AuthenticationProvider 就是针对不同的身份类型执行的具体的身份认证。
AuthenticationManager 与 ProviderManager :
ProviderManager 是 AuthenticationManager 的唯一实现,也是 SpringSecurity 默认使用实现。从这里不难看出默认情况下AuthenticationManager 就是一个ProviderManager。
ProviderManager 与 AuthenticationProvider:
ProviderManager 是 AuthenticationManager 的唯一实现,也是 SpringSecurity 默认使用实现。从这里不难看出默认情况下AuthenticationManager 就是一个ProviderManager。ProviderManager 与 AuthenticationProvider
在 Spring Seourity 中,允许系统同时支持多种不同的认证方式,例如同时支持用户名/密码认证、ReremberMe 认证、手机号码动态认证等,而不同的认证方式对应了不同的 AuthenticationProvider,所以一个完整的认证流程可能由多个AuthenticationProvider 来提供。
多个 AuthenticationProvider 将组成一个列表,这个列表将由ProviderManager 代理。换句话说,在ProviderManager 中存在一个AuthenticationProvider 列表,在ProviderManager 中遍历列表中的每一个AuthenticationProvider 去执行身份认证,最终得到认证结果。
ProviderManager 本身也可以再配置一个 AuthenticationManager 作为parent,这样当ProviderManager 认证失败之后,就可以进入到 parent 中再次进行认证。理论上来说,ProviderManager 的 parent 可以是任意类型的AuthenticationManager,但是通常都是由ProviderManager 来扮演 parent 的⻆色,也就是 ProviderManager 是ProviderManager 的 parent。
ProviderManager 本身也可以有多个,多个ProviderManager 共用同一个parent。有时,一个应用程序有受保护资源的逻辑组(例如,所有符合路径模式的网络资源,如/api/**),每个组可以有自己的专用 AuthenticationManager。通常,每个组都是一个ProviderManager,它们共享一个父级。然后,父级是一种 全局 资源,作为所有提供者的后备资源。
默认情况下AuthenticationProvider 是由 DaoAuthenticationProvider 类来实现认证的,在DaoAuthenticationProvider 认证时又通过 UserDetailsService 完成数据源的校验。
AuthenticationManager 是认证管理器,在 Spring Security 中有全局AuthenticationManager,也可以有局部AuthenticationManager。全局的AuthenticationManager用来对全局认证进行处理,局部的AuthenticationManager用来对某些特殊资源认证处理。当然无论是全局认证管理器还是局部认证管理器都是由ProviderManger 进行实现。 每一个ProviderManger中都代理一个AuthenticationProvider的列表,列表中每一个实现代表一种身份认证方式。认证时底层数据源需要调用 UserDetailService 来实现。