若无特殊配置登录认证会通过UsernamePasswordAuthenticationFilter
来处理
UsernamePasswordAuthenticationFilter
继承了抽象类AbstractAuthenticationProcessingFilter
public class UsernamePasswordAuthenticationFilter extends
AbstractAuthenticationProcessingFilter {
}
AbstractAuthenticationProcessingFilter
已经定义了doFilter
方法,UsernamePasswordAuthenticationFilter
对请求的拦截是通过其父类方法实现的。
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) req;
HttpServletResponse response = (HttpServletResponse) res;
if (!requiresAuthentication(request, response)) {
chain.doFilter(request, response);
return;
}
......
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);
}
doFilter
中核心便是那个attemptAuthentication()
方法了,这个AbstractAuthenticationProcessingFilter
的抽象方法被UsernamePasswordAuthenticationFilter
继承实现了
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
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(
username, password);
// Allow subclasses to set the "details" property
setDetails(request, authRequest);
// 获取AuthenticationManager对象,并对Token进行认证
return this.getAuthenticationManager().authenticate(authRequest);
}
UsernamePasswordAuthenticationFilter
在这个方法中基于用户的用户名和密码生成了token,并将这个token
传给AuthenticationManager
进行认证。Spring Security有许多AuthenticationManager
的实现类,通过Debug可以发现,默认情况下被set到UsernamePasswordAuthenticationFilter
中的AuthenticationManager
为ProviderManager
。
ProviderManager
对token进行认证的代码逻辑如下
public Authentication authenticate(Authentication authentication)
throws AuthenticationException {
Class<? extends Authentication> toTest = authentication.getClass();
AuthenticationException lastException = null;
Authentication result = null;
boolean debug = logger.isDebugEnabled();
// 调用getProviders()获取所有AuthenticationProvider
for (AuthenticationProvider provider : getProviders()) {
// 若这个provider不支持此类型的token,则continue
if (!provider.supports(toTest)) {
continue;
}
if (debug) {
logger.debug("Authentication attempt using "
+ provider.getClass().getName());
}
try {
// 使用这个provider对token进行认证
result = provider.authenticate(authentication);
if (result != null) {
// 将authentication的Details拷贝到result
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;
}
}
// result为null且这个ProviderManager的parent不为null
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;
}
}
// 如果result不为null
if (result != null) {
if (eraseCredentialsAfterAuthentication
&& (result instanceof CredentialsContainer)) {
// 认证完成. 从authentication中移除密码以及其他敏感信息
((CredentialsContainer) result).eraseCredentials();
}
eventPublisher.publishAuthenticationSuccess(result);
return result;
}
// 执行到这里,可能是因为parent为null或认证未通过或抛出了异常
// 如果未抛出exception,则是由于provider为null的原因
if (lastException == null) {
lastException = new ProviderNotFoundException(messages.getMessage(
"ProviderManager.providerNotFound",
new Object[] { toTest.getName() },
"No AuthenticationProvider found for {0}"));
}
prepareException(lastException, authentication);
throw lastException;
}
上面的代码中,若provider
对token的认证未通过,则会调用它的parentAuthenticationManager
执行authenticate()
那么这个过程究竟会经过哪些和AuthenticationProvider
呢?经过debug跟踪可以发现
第一个ProviderManager
有一个AuthenticationProvider
,它就是AnonymousAuthenticationProvider
,不支持AnonymousAuthenticationProvider
类型为UsernamePasswordAuthenticationToken
的Authentication
。因此会continue
到下一个foreach循环,又由于providers
列表中只有这一个provider
,因此会跳出for循环。判断if (result == null && parent != null)
,由于第一个ProviderManager
有parent,因此调用parent.authenticate(authentication)
对authentication进行认证。
第二个ProviderManager
也只有一个AuthenticationProvider
,这个Provider便是DaoAuthenticationProvider
,DaoAuthenticationProvider
支持UsernamePasswordAuthenticationToken
此类型的token认证,因此会执行它的authenticate()
方法对token进行认证。
DaoAuthenticationProvider
继承了抽象类AbstractUserDetailsAuthenticationProvider
public class DaoAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider {
}
DaoAuthenticationProvider
的authenticate
方法也是在其父类抽象类中定义的
public Authentication authenticate(Authentication authentication)
throws AuthenticationException {
Assert.isInstanceOf(UsernamePasswordAuthenticationToken.class, authentication,
() -> messages.getMessage(
"AbstractUserDetailsAuthenticationProvider.onlySupports",
"Only UsernamePasswordAuthenticationToken is supported"));
// 判断 username
String username = (authentication.getPrincipal() == null) ? "NONE_PROVIDED"
: authentication.getName();
boolean cacheWasUsed = true;
// 从userCache取用户详细
UserDetails user = this.userCache.getUserFromCache(username);
if (user == null) {
cacheWasUsed = false;
try {
// 从缓存取user失败,调用retrieveUser()取用户详细
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) {
// 将缓存置位false,再取用户信息进行一次认证
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;
// 如果要求PrincipalAsString是字符串,则将用户的用户名传入
if (forcePrincipalAsString) {
principalToReturn = user.getUsername();
}
// 创建成功的认证对象并返回
return createSuccessAuthentication(principalToReturn, authentication, user);
}
authenticate
方法会先从缓存取用户详细,若取不到,则会调用retrieveUser()
方法取用户详细
protected final UserDetails retrieveUser(String username,
UsernamePasswordAuthenticationToken authentication)
throws AuthenticationException {
prepareTimingAttackProtection();
try {
UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username);
if (loadedUser == null) {
throw new InternalAuthenticationServiceException(
"UserDetailsService returned null, which is an interface contract violation");
}
return loadedUser;
}
catch (UsernameNotFoundException ex) {
mitigateAgainstTimingAttack(authentication);
throw ex;
}
catch (InternalAuthenticationServiceException ex) {
throw ex;
}
catch (Exception ex) {
throw new InternalAuthenticationServiceException(ex.getMessage(), ex);
}
}
获取用户详细的方法是调用UserDetailsService.loadUserByUsername()
方法,UserDetailsService
是接口类,在自己的系统中,可以通过实现这个接口来定义如何获取UserDetails
在调用retrieveUser()
方法获得了用户详细后,会利用这个用户详细和登录信息进行比对
preAuthenticationChecks.check(user);
additionalAuthenticationChecks(user,(UsernamePasswordAuthenticationToken) authentication);
DefaultPreAuthenticationChecks
是UserDetailsChecker
的默认前置认证实现类
private UserDetailsChecker preAuthenticationChecks = new DefaultPreAuthenticationChecks();
private class DefaultPreAuthenticationChecks implements UserDetailsChecker {
public void check(UserDetails user) {
// 如果账号被锁定
if (!user.isAccountNonLocked()) {
logger.debug("User account is locked");
throw new LockedException(messages.getMessage(
"AbstractUserDetailsAuthenticationProvider.locked",
"User account is locked"));
}
// 如果账号不可用
if (!user.isEnabled()) {
logger.debug("User account is disabled");
throw new DisabledException(messages.getMessage(
"AbstractUserDetailsAuthenticationProvider.disabled",
"User is disabled"));
}
// 如果账号已过期
if (!user.isAccountNonExpired()) {
logger.debug("User account is expired");
throw new AccountExpiredException(messages.getMessage(
"AbstractUserDetailsAuthenticationProvider.expired",
"User account has expired"));
}
}
}
前置确认通过后,会进入additionalAuthenticationChecks()
方法进行额外的认证确认,这个抽象方法被DaoAuthenticationProvider
实现。
protected void additionalAuthenticationChecks(UserDetails userDetails,
UsernamePasswordAuthenticationToken authentication)
throws AuthenticationException {
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.matches(presentedPassword, userDetails.getPassword())) {
logger.debug("Authentication failed: password does not match stored value");
throw new BadCredentialsException(messages.getMessage(
"AbstractUserDetailsAuthenticationProvider.badCredentials",
"Bad credentials"));
}
}
其实就是判断密码是否匹配的过程,会调用用户指定的passwordEncoder
对UserDetails
的password
做解密,然后和客户端提交的密码做比对,以BCryptPasswordEncoder
为例
public boolean matches(CharSequence rawPassword, String encodedPassword) {
if (encodedPassword == null || encodedPassword.length() == 0) {
logger.warn("Empty encoded password");
return false;
}
if (!BCRYPT_PATTERN.matcher(encodedPassword).matches()) {
logger.warn("Encoded password does not look like BCrypt");
return false;
}
return BCrypt.checkpw(rawPassword.toString(), encodedPassword);
}
在前置认证和额外认证都通过之后,会进行后置认证,DefaultPostAuthenticationChecks
是UserDetailsChecker
的默认后置认证实现类
private class DefaultPostAuthenticationChecks implements UserDetailsChecker {
public void check(UserDetails user) {
// 判断密码是否过期
if (!user.isCredentialsNonExpired()) {
logger.debug("User account credentials have expired");
throw new CredentialsExpiredException(messages.getMessage(
"AbstractUserDetailsAuthenticationProvider.credentialsExpired",
"User credentials have expired"));
}
}
}
至此,认证成功后,DaoAuthenticationProvider
会生成成功的UsernamePasswordAuthenticationToken
并返回给ProviderManager
protected Authentication createSuccessAuthentication(Object principal,
Authentication authentication, UserDetails user) {
// Ensure we return the original credentials the user supplied,
// so subsequent attempts are successful even with encoded passwords.
// Also ensure we return the original getDetails(), so that future
// authentication events after cache expiry contain the details
UsernamePasswordAuthenticationToken result = new UsernamePasswordAuthenticationToken(
principal, authentication.getCredentials(),
authoritiesMapper.mapAuthorities(user.getAuthorities()));
result.setDetails(authentication.getDetails());
return result;
}
因为result
不为null
,因此跳出providers的遍历认证。认证完成后. ProviderManager
从authentication中移除密码以及其他敏感信息并返回给UsernamePasswordAuthenticationFilter
。
UsernamePasswordAuthenticationFilter
返回认证结果,从attemptAuthentication()
方法跳回doFilter()
接着执行session策略
sessionStrategy.onAuthentication(authResult, request, response);
CompositeSessionAuthenticationStrategy
是sessionStrategy
的具体实现
public void onAuthentication(Authentication authentication,
HttpServletRequest request, HttpServletResponse response)
throws SessionAuthenticationException {
// 遍历delegateStrategies
for (SessionAuthenticationStrategy delegate : this.delegateStrategies) {
if (this.logger.isDebugEnabled()) {
this.logger.debug("Delegating to " + delegate);
}
delegate.onAuthentication(authentication, request, response);
}
}
delegateStrategies的列表默认情况下只有一个,实现了SessionAuthenticationStrategy
的AbstractSessionFixationProtectionStrategy
/**
* Called when a user is newly authenticated.
* <p>
* If a session already exists, and matches the session Id from the client, a new
* session will be created, and the session attributes copied to it (if
* {@code migrateSessionAttributes} is set). If the client's requested session Id is
* invalid, nothing will be done, since there is no need to change the session Id if
* it doesn't match the current session.
* <p>
* If there is no session, no action is taken unless the {@code alwaysCreateSession}
* property is set, in which case a session will be created if one doesn't already
* exist.
*/
public void onAuthentication(Authentication authentication,
HttpServletRequest request, HttpServletResponse response) {
boolean hadSessionAlready = request.getSession(false) != null;
if (!hadSessionAlready && !alwaysCreateSession) {
// Session fixation isn't a problem if there's no session
return;
}
// Create new session if necessary
HttpSession session = request.getSession();
if (hadSessionAlready && request.isRequestedSessionIdValid()) {
String originalSessionId;
String newSessionId;
Object mutex = WebUtils.getSessionMutex(session);
synchronized (mutex) {
// We need to migrate to a new session
originalSessionId = session.getId();
session = applySessionFixation(request);
newSessionId = session.getId();
}
if (originalSessionId.equals(newSessionId)) {
logger.warn("Your servlet container did not change the session ID when a new session was created. You will"
+ " not be adequately protected against session-fixation attacks");
}
onSessionChange(originalSessionId, session, authentication);
}
}
默认情况下alwaysCreateSession
为false
,代码中只需要关注hadSessionAlready
这个字段即可
最后,执行doFilter()
中的最后一行代码successfulAuthentication(request, response, chain, authResult)
,successfulAuthentication
也是在UsernamePasswordAuthenticationFilter
的抽象父类中定义好的方法
protected void successfulAuthentication(HttpServletRequest request,
HttpServletResponse response, FilterChain chain, Authentication authResult)
throws IOException, ServletException {
if (logger.isDebugEnabled()) {
logger.debug("Authentication success. Updating SecurityContextHolder to contain: "
+ authResult);
}
// 向SecurityContextHolder放入认证信息
SecurityContextHolder.getContext().setAuthentication(authResult);
rememberMeServices.loginSuccess(request, response, authResult);
// Fire event
if (this.eventPublisher != null) {
eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent(
authResult, this.getClass()));
}
successHandler.onAuthenticationSuccess(request, response, authResult);
}
大体就是向SecurityContextHolder
中放入认证的信息,然后调用successHandler.onAuthenticationSuccess()
处理认证成功操作,successHandler的默认实现是SavedRequestAwareAuthenticationSuccessHandler
@Override
public void onAuthenticationSuccess(HttpServletRequest request,
HttpServletResponse response, Authentication authentication)
throws ServletException, IOException {
// 将请求存入请求缓存session中
SavedRequest savedRequest = requestCache.getRequest(request, response);
if (savedRequest == null) {
super.onAuthenticationSuccess(request, response, authentication);
return;
}
String targetUrlParameter = getTargetUrlParameter();
if (isAlwaysUseDefaultTargetUrl()
|| (targetUrlParameter != null && StringUtils.hasText(request
.getParameter(targetUrlParameter)))) {
requestCache.removeRequest(request, response);
super.onAuthenticationSuccess(request, response, authentication);
return;
}
clearAuthenticationAttributes(request);
// Use the DefaultSavedRequest URL
String targetUrl = savedRequest.getRedirectUrl();
logger.debug("Redirecting to DefaultSavedRequest Url: " + targetUrl);
getRedirectStrategy().sendRedirect(request, response, targetUrl);
}
默认不存session,因此if (savedRequest == null)
条件成立,调用super.onAuthenticationSuccess()
处理,然后return
SavedRequestAwareAuthenticationSuccessHandler
其父类是SimpleUrlAuthenticationSuccessHandler
public void onAuthenticationSuccess(HttpServletRequest request,
HttpServletResponse response, Authentication authentication)
throws IOException, ServletException {
handle(request, response, authentication);
clearAuthenticationAttributes(request);
}
handle()
方法代码如下,其实就是将页面重定向回登录跳转之前的请求页
protected void handle(HttpServletRequest request, HttpServletResponse response,
Authentication authentication) throws IOException, ServletException {
String targetUrl = determineTargetUrl(request, response);
if (response.isCommitted()) {
logger.debug("Response has already been committed. Unable to redirect to "
+ targetUrl);
return;
}
redirectStrategy.sendRedirect(request, response, targetUrl);
}
在handle()
方法执行结束以后,会执行clearAuthenticationAttributes()
/**
* Removes temporary authentication-related data which may have been stored in the
* session during the authentication process.
*/
protected final void clearAuthenticationAttributes(HttpServletRequest request) {
HttpSession session = request.getSession(false);
if (session == null) {
return;
}
session.removeAttribute(WebAttributes.AUTHENTICATION_EXCEPTION);
}
其实就是从session中移除认证的信息,至此登录处理逻辑结束,收工